diff --git a/node_exporter_manager.py b/node_exporter_manager.py index 620fba9..6b95c18 100644 --- a/node_exporter_manager.py +++ b/node_exporter_manager.py @@ -15,14 +15,34 @@ import platform import distro # Stałe ścieżki i konfiguracja -BIN_TARGET = '/usr/local/bin/node_exporter' -SERVICE_FILE = '/etc/systemd/system/node_exporter.service' LOG_FILE = '/var/log/node_exporter_installer.log' USER_NAME = 'node_exporter' USER_HOME = '/var/lib/node_exporter' CONFIG_DIR = '/etc/node_exporter' CONFIG_PATH = os.path.join(CONFIG_DIR, 'config.yml') +# Mapowanie ścieżek i nazw usług dla różnych dystrybucji +DISTRO_BIN_PATHS = { + 'arch': '/usr/bin/prometheus-node-exporter', + 'opensuse': '/usr/bin/node_exporter', + 'suse': '/usr/bin/node_exporter', + 'default': '/usr/local/bin/node_exporter' +} + +DISTRO_SERVICE_NAMES = { + 'arch': 'prometheus-node-exporter.service', + 'opensuse': 'node_exporter.service', + 'suse': 'node_exporter.service', + 'default': 'node_exporter.service' +} + +DISTRO_SERVICE_PATHS = { + 'arch': '/usr/lib/systemd/system/prometheus-node-exporter.service', + 'opensuse': '/usr/lib/systemd/system/node_exporter.service', + 'suse': '/usr/lib/systemd/system/node_exporter.service', + 'default': '/etc/systemd/system/node_exporter.service' +} + # Konfiguracja logowania logging.basicConfig( filename=LOG_FILE, @@ -45,6 +65,34 @@ DRY_RUN = '--dry-run' in sys.argv # ----------------- FUNKCJE POMOCNICZE ----------------- +def get_distro_family(): + try: + os_id = distro.id().lower() + if os_id in ['ubuntu', 'debian']: + return 'debian' + elif os_id in ['arch', 'manjaro']: + return 'arch' + elif os_id in ['opensuse', 'suse']: + return 'suse' + elif os_id in ['centos', 'rhel', 'fedora']: + return 'rhel' + else: + return 'default' + except Exception: + return 'default' + +def get_bin_path(): + distro_family = get_distro_family() + return DISTRO_BIN_PATHS.get(distro_family, DISTRO_BIN_PATHS['default']) + +def get_service_name(): + distro_family = get_distro_family() + return DISTRO_SERVICE_NAMES.get(distro_family, DISTRO_SERVICE_NAMES['default']) + +def get_service_path(): + distro_family = get_distro_family() + return DISTRO_SERVICE_PATHS.get(distro_family, DISTRO_SERVICE_PATHS['default']) + def ensure_root(): if os.geteuid() != 0: print("Ten skrypt musi być uruchomiony jako root.") @@ -71,9 +119,10 @@ def get_latest_version(): raise def get_local_version(): - if Path(BIN_TARGET).exists(): + bin_path = get_bin_path() + if Path(bin_path).exists(): try: - output = run_cmd([BIN_TARGET, '--version']) + output = run_cmd([bin_path, '--version']) for line in output.splitlines(): if "version" in line: parts = line.split() @@ -132,38 +181,47 @@ def get_sha256_from_release(release, filename): def check_node_exporter(): try: - subprocess.run(['curl', '-f', 'http://localhost:9100/metrics'], check=True, stdout=subprocess.DEVNULL) + response = requests.get('http://localhost:9100/metrics', timeout=5) + response.raise_for_status() print("✅ Node Exporter działa poprawnie") - except subprocess.CalledProcessError: - print("❌ Node Exporter nie odpowiada — restart...") - run_cmd(['systemctl', 'restart', 'node_exporter']) + return True + except requests.RequestException as e: + print(f"❌ Node Exporter nie odpowiada — {str(e)} — restart...") + service_name = get_service_name() + run_cmd(['systemctl', 'restart', service_name]) + return False -def detect_os_family(): - try: - os_id = distro.id().lower() - if os_id in ['ubuntu', 'debian']: - return 'debian' - elif os_id in ['arch', 'manjaro']: - return 'arch' - elif os_id in ['opensuse', 'suse']: - return 'suse' - elif os_id in ['centos', 'rhel', 'fedora']: - return 'rhel' - else: - return 'unknown' - except Exception: - return 'unknown' +def install_dependencies(): + os_family = get_distro_family() + + if os_family == 'debian': + run_cmd(['apt-get', 'update']) + run_cmd(['apt-get', 'install', '-y', 'curl', 'openssl', 'bcrypt', 'python3-requests', 'python3-distro', 'python3-bcrypt', ]) + elif os_family == 'rhel': + run_cmd(['yum', 'install', '-y', 'curl', 'openssl', 'bcrypt']) + elif os_family == 'arch': + run_cmd(['pacman', '-Sy', '--noconfirm', 'curl', 'openssl', 'python-bcrypt']) + elif os_family == 'suse': + run_cmd(['zypper', '--non-interactive', 'install', 'curl', 'openssl', 'python3-bcrypt']) + else: + logging.warning("Nieznana dystrybucja - pomijam instalację zależności") # ----------------- INSTALACJA I KONFIGURACJA ----------------- def install_binary(extracted_dir): + bin_path = get_bin_path() src = Path(extracted_dir) / 'node_exporter' - if Path(BIN_TARGET).exists(): - shutil.copy(BIN_TARGET, BIN_TARGET + '.bak') - logging.info("Backup binarki zrobiony jako node_exporter.bak") - shutil.copy(src, BIN_TARGET) - os.chmod(BIN_TARGET, 0o755) - logging.info("Zainstalowano binarkę do /usr/local/bin") + + if Path(bin_path).exists(): + shutil.copy(bin_path, bin_path + '.bak') + logging.info(f"Backup binarki zrobiony jako {bin_path}.bak") + + # Utwórz katalog docelowy jeśli nie istnieje + Path(bin_path).parent.mkdir(parents=True, exist_ok=True) + + shutil.copy(src, bin_path) + os.chmod(bin_path, 0o755) + logging.info(f"Zainstalowano binarkę do {bin_path}") def create_user(): try: @@ -178,7 +236,11 @@ def create_user(): shutil.chown(home_path, user=USER_NAME, group=USER_NAME) logging.info(f"Utworzono katalog {USER_HOME} i przypisano właściciela.") -def setup_service(): +def setup_service(secured=False): + service_path = get_service_path() + service_name = get_service_name() + bin_path = get_bin_path() + service_content = f"""[Unit] Description=Node Exporter Wants=network-online.target @@ -188,19 +250,22 @@ After=network-online.target User={USER_NAME} Group={USER_NAME} WorkingDirectory={USER_HOME} -ExecStart={BIN_TARGET} +ExecStart={bin_path}{' --web.config.file="/etc/node_exporter/config.yml"' if secured else ''} Restart=on-failure [Install] WantedBy=default.target """ - with open(SERVICE_FILE, 'w') as f: + # Utwórz katalog docelowy jeśli nie istnieje + Path(service_path).parent.mkdir(parents=True, exist_ok=True) + + with open(service_path, 'w') as f: f.write(service_content) - logging.info("Zapisano konfigurację usługi systemd") + logging.info(f"Zapisano konfigurację usługi systemd w {service_path}") run_cmd(['systemctl', 'daemon-reload']) - run_cmd(['systemctl', 'enable', '--now', 'node_exporter']) - logging.info("Włączono i uruchomiono usługę node_exporter") + run_cmd(['systemctl', 'enable', '--now', service_name]) + logging.info(f"Włączono i uruchomiono usługę {service_name}") def setup_secured_config(): os.makedirs(CONFIG_DIR, exist_ok=True) @@ -224,6 +289,9 @@ tls_server_config: shutil.chown(CONFIG_DIR, user=USER_NAME, group=USER_NAME) for f_name in os.listdir(CONFIG_DIR): shutil.chown(os.path.join(CONFIG_DIR, f_name), user=USER_NAME, group=USER_NAME) + + setup_service(secured=True) + logging.info("Skonfigurowano zabezpieczoną wersję Node Exportera") def change_password(user, password): if not os.path.exists(CONFIG_PATH): @@ -268,34 +336,43 @@ def change_password(user, password): print(f"Zmieniono hasło dla użytkownika '{user}'") def uninstall(): - if Path(SERVICE_FILE).exists(): - run_cmd(['systemctl', 'disable', '--now', 'node_exporter']) - os.remove(SERVICE_FILE) - logging.info("Usunięto plik usługi i zatrzymano node_exporter") - if Path(BIN_TARGET).exists(): - os.remove(BIN_TARGET) - logging.info("Usunięto binarkę node_exporter") + service_path = get_service_path() + service_name = get_service_name() + bin_path = get_bin_path() + + if Path(service_path).exists(): + run_cmd(['systemctl', 'disable', '--now', service_name]) + os.remove(service_path) + logging.info(f"Usunięto plik usługi {service_path} i zatrzymano {service_name}") + + if Path(bin_path).exists(): + os.remove(bin_path) + logging.info(f"Usunięto binarkę {bin_path}") + try: pwd.getpwnam(USER_NAME) run_cmd(['userdel', USER_NAME]) logging.info("Usunięto użytkownika node_exporter") except KeyError: logging.info("Użytkownik już nie istnieje") + if Path(USER_HOME).exists(): shutil.rmtree(USER_HOME) logging.info("Usunięto katalog /var/lib/node_exporter") def install(): ensure_root() + bin_path = get_bin_path() - if Path(BIN_TARGET).exists(): + if Path(bin_path).exists(): print("Node Exporter już zainstalowany. Użyj --update.") return - + + install_dependencies() version, release = get_latest_version() url = next(asset['browser_download_url'] for asset in release['assets'] if 'linux-amd64.tar.gz' in asset['browser_download_url']) extracted = download_and_extract(url) - + # Weryfikacja sumy SHA256 filename = url.split('/')[-1] sha256_expected = get_sha256_from_release(release, filename) @@ -306,36 +383,44 @@ def install(): install_binary(extracted) create_user() - setup_service() + setup_service(secured='--install-secured' in sys.argv) logging.info("Instalacja zakończona") print("✅ Node Exporter został zainstalowany") def update(): ensure_root() + service_path = get_service_path() + service_name = get_service_name() + + # Sprawdź czy usługa istnieje + if not Path(service_path).exists(): + print("❌ Usługa node_exporter nie jest zainstalowana. Użyj --install") + return + local_version = get_local_version() latest_version, release = get_latest_version() - + # Sprawdź czy mamy wymusić aktualizację (--force lub --force-update) force_update = '--force' in sys.argv or '--force-update' in sys.argv - + if not force_update and local_version == latest_version: print(f"Node Exporter już aktualny ({local_version})") print("Użyj --update --force aby wymusić aktualizację") return print(f"Aktualizacja z {local_version} do {latest_version}...") - run_cmd(['systemctl', 'stop', 'node_exporter']) - + run_cmd(['systemctl', 'stop', service_name]) + # Pobierz odpowiedni plik dla architektury machine = platform.machine().lower() arch = ARCH_MAP.get(machine, 'amd64') # Domyślnie amd64 jeśli architektura nieznana url = next( - asset['browser_download_url'] for asset in release['assets'] + asset['browser_download_url'] for asset in release['assets'] if f'linux-{arch}.tar.gz' in asset['browser_download_url'] ) - + extracted = download_and_extract(url) - + # Weryfikacja sumy SHA256 filename = url.split('/')[-1] sha256_expected = get_sha256_from_release(release, filename) @@ -345,7 +430,7 @@ def update(): sys.exit(1) install_binary(extracted) - run_cmd(['systemctl', 'start', 'node_exporter']) + run_cmd(['systemctl', 'start', service_name]) check_node_exporter() logging.info(f"Zaktualizowano Node Exporter do wersji {latest_version}") @@ -403,11 +488,12 @@ def print_status(): else: print("❌ Node Exporter nie jest zainstalowany") - service_status = subprocess.run(['systemctl', 'is-active', 'node_exporter'], stdout=subprocess.PIPE) + service_name = get_service_name() + service_status = subprocess.run(['systemctl', 'is-active', service_name], stdout=subprocess.PIPE) if service_status.returncode == 0: - print("✔️ Usługa node_exporter działa") + print(f"✔️ Usługa {service_name} działa") else: - print("❌ Usługa node_exporter nie działa") + print(f"❌ Usługa {service_name} nie działa") if Path(CONFIG_PATH).exists(): print(f"✔️ Znaleziono config.yml: {CONFIG_PATH}") @@ -452,6 +538,7 @@ def main(): elif sys.argv[1] == '--install-secured': install() setup_secured_config() + print("✅ Node Exporter został zainstalowany w wersji zabezpieczonej") elif sys.argv[1].startswith('--set-password='): user_pass = sys.argv[1].split('=')[1] if ":" not in user_pass: @@ -461,7 +548,6 @@ def main(): change_password(u, p) elif sys.argv[1] == '--status': print_status() - else: print("Nieznany argument. Użyj --help") sys.exit(1)