Update node_exporter_installer.py

This commit is contained in:
gru
2025-05-27 13:58:30 +02:00
parent 291d398e1d
commit 54f10c8973

View File

@ -1,92 +1,235 @@
#!/usr/bin/env python3
import os import os
import argparse import sys
import json import subprocess
import urllib.request import requests
import tarfile import tarfile
import shutil import shutil
import logging
import pwd import pwd
import subprocess
from pathlib import Path from pathlib import Path
import bcrypt
try: # Stałe ścieżki i konfiguracja
import bcrypt BIN_TARGET = '/usr/local/bin/node_exporter'
BCRYPT_AVAILABLE = True SERVICE_FILE = '/etc/systemd/system/node_exporter.service'
except ImportError: LOG_FILE = '/var/log/node_exporter_installer.log'
BCRYPT_AVAILABLE = False USER_NAME = 'node_exporter'
USER_HOME = '/var/lib/node_exporter'
CONFIG_DIR = '/etc/node_exporter'
CONFIG_PATH = os.path.join(CONFIG_DIR, 'config.yml')
SYSTEMD_SERVICE_PATH = "/etc/systemd/system/node_exporter.service" # Konfiguracja logowania
NODE_EXPORTER_BIN = "/usr/local/bin/node_exporter" logging.basicConfig(
NODE_EXPORTER_DIR = "/etc/node_exporter" filename=LOG_FILE,
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s'
)
# ----------------- FUNKCJE POMOCNICZE -----------------
def run_safe(cmd): def run_cmd(cmd, check=True):
subprocess.run(cmd, shell=isinstance(cmd, str), check=True)
def fetch_latest_download_url():
url = "https://api.github.com/repos/prometheus/node_exporter/releases/latest"
with urllib.request.urlopen(url) as response:
data = json.loads(response.read().decode())
for asset in data["assets"]:
if "linux-amd64" in asset["browser_download_url"]:
return asset["browser_download_url"]
raise RuntimeError("Nie znaleziono odpowiedniego URL do pobrania")
def download_and_extract(url, extract_path="/tmp"):
file_name = url.split("/")[-1]
download_path = os.path.join(extract_path, file_name)
print(f"Pobieranie: {url}")
urllib.request.urlretrieve(url, download_path)
with tarfile.open(download_path, "r:gz") as tar:
tar.extractall(path=extract_path)
for entry in os.listdir(extract_path):
full_path = os.path.join(extract_path, entry)
if entry.startswith("node_exporter") and os.path.isdir(full_path):
return full_path
raise RuntimeError("Nie znaleziono rozpakowanego katalogu node_exporter")
def ensure_node_exporter_user():
try: try:
pwd.getpwnam("node_exporter") result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=check, text=True)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
logging.error(f"Błąd komendy: {cmd}{e.stderr}")
raise
def get_latest_version():
try:
r = requests.get('https://api.github.com/repos/prometheus/node_exporter/releases/latest', timeout=10)
r.raise_for_status()
return r.json()['tag_name'].lstrip('v'), r.json()
except requests.RequestException as e:
logging.error(f"Błąd pobierania wersji z GitHub: {e}")
raise
def get_local_version():
if Path(BIN_TARGET).exists():
try:
output = run_cmd([BIN_TARGET, '--version'])
for line in output.splitlines():
if "version" in line:
parts = line.split()
if len(parts) >= 3:
return parts[2]
except Exception as e:
logging.warning(f"Nie udało się odczytać wersji lokalnej: {e}")
return None
def download_and_extract(url, download_path='/tmp'):
filename = os.path.join(download_path, url.split('/')[-1])
try:
with requests.get(url, stream=True, timeout=30) as r:
r.raise_for_status()
with open(filename, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
except requests.RequestException as e:
logging.error(f"Błąd pobierania pliku: {e}")
raise
with tarfile.open(filename) as tar:
tar.extractall(path=download_path, filter='data')
return next(Path(download_path).glob('node_exporter-*'))
# ----------------- INSTALACJA I KONFIGURACJA -----------------
def install_binary(extracted_dir):
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")
def create_user():
try:
pwd.getpwnam(USER_NAME)
logging.info("Użytkownik node_exporter już istnieje.")
except KeyError: except KeyError:
run_safe(["useradd", "-rs", "/bin/false", "node_exporter"]) run_cmd(['useradd', '--system', '--home', USER_HOME, '--shell', '/bin/false', USER_NAME])
logging.info("Utworzono użytkownika node_exporter")
home_path = Path(USER_HOME)
home_path.mkdir(parents=True, exist_ok=True)
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():
service_content = f"""[Unit]
Description=Node Exporter
Wants=network-online.target
After=network-online.target
[Service]
User={USER_NAME}
Group={USER_NAME}
WorkingDirectory={USER_HOME}
ExecStart={BIN_TARGET}
Restart=on-failure
[Install]
WantedBy=default.target
"""
with open(SERVICE_FILE, 'w') as f:
f.write(service_content)
logging.info("Zapisano konfigurację usługi systemd")
run_cmd(['systemctl', 'daemon-reload'])
run_cmd(['systemctl', 'enable', '--now', 'node_exporter'])
logging.info("Włączono i uruchomiono usługę node_exporter")
def write_systemd_service(secured=False):
exec_line = f'{BIN_TARGET} --web.config.file="{CONFIG_PATH}"' if secured else BIN_TARGET
content = f"""[Unit]
Description=Node Exporter
Wants=network-online.target
After=network-online.target
[Service]
User={USER_NAME}
ExecStart={exec_line}
[Install]
WantedBy=default.target
"""
with open(SERVICE_FILE, "w") as f:
f.write(content)
def install_binary(source_path, force_update=False): def uninstall():
if os.path.exists(NODE_EXPORTER_BIN): if Path(SERVICE_FILE).exists():
if not force_update: run_cmd(['systemctl', 'disable', '--now', 'node_exporter'])
print("Binarka już istnieje. Użyj --update aby zaktualizować.") os.remove(SERVICE_FILE)
return logging.info("Usunięto plik usługi i zatrzymano node_exporter")
subprocess.run(["systemctl", "stop", "node_exporter"], check=False) if Path(BIN_TARGET).exists():
os.remove(BIN_TARGET)
logging.info("Usunięto binarkę node_exporter")
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")
shutil.copy(os.path.join(source_path, "node_exporter"), NODE_EXPORTER_BIN) def install():
os.chmod(NODE_EXPORTER_BIN, 0o755) if Path(BIN_TARGET).exists():
print("Zainstalowano node_exporter.") print("Node Exporter jest już zainstalowany.")
response = input("Czy chcesz przeinstalować z konfiguracją secured (TLS + Basic Auth)? [t/N]: ")
def update_basic_auth_user(credential: str): if response.lower() == 't':
if not BCRYPT_AVAILABLE: uninstall()
print("Błąd: brak modułu 'bcrypt'. Zainstaluj go poleceniem:\n pip install bcrypt") install()
setup_secured_config()
write_systemd_service(secured=True)
run_cmd(['systemctl', 'daemon-reexec'])
run_cmd(['systemctl', 'restart', 'node_exporter'])
print("✅ Node Exporter przeinstalowany z konfiguracją secured.")
else:
print(" Instalacja przerwana - bez zmian.")
return return
if ":" not in credential: version, release = get_latest_version()
print("Błąd: użyj formatu --set-password user:haslo") 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)
install_binary(extracted)
create_user()
write_systemd_service(secured=False)
run_cmd(['systemctl', 'daemon-reload'])
run_cmd(['systemctl', 'enable', '--now', 'node_exporter'])
logging.info("Instalacja zakończona")
def update():
local_version = get_local_version()
latest_version, release = get_latest_version()
if local_version == latest_version:
print(f"Node Exporter już aktualny ({local_version})")
return return
print(f"Aktualizacja z {local_version} do {latest_version}...")
user, plain_password = credential.split(":", 1) run_cmd(['systemctl', 'stop', 'node_exporter'])
config_path = os.path.join(NODE_EXPORTER_DIR, "config.yml") url = next(asset['browser_download_url'] for asset in release['assets'] if 'linux-amd64.tar.gz' in asset['browser_download_url'])
if not os.path.exists(config_path): extracted = download_and_extract(url)
print("Plik config.yml nie istnieje uruchom instalację z --secured najpierw.") install_binary(extracted)
run_cmd(['systemctl', 'start', 'node_exporter'])
logging.info(f"Zaktualizowano Node Exporter do wersji {latest_version}")
def setup_secured_config():
os.makedirs(CONFIG_DIR, exist_ok=True)
subprocess.run([
"openssl", "req", "-new", "-newkey", "rsa:4096", "-days", "3650", "-nodes", "-x509",
"-subj", "/C=PL/ST=X/L=X/O=secure/CN=localhost",
"-keyout", f"{CONFIG_DIR}/node_exporter.key",
"-out", f"{CONFIG_DIR}/node_exporter.crt"
], check=True)
config = """basic_auth_users:
root: $2y$10$SNr5iyJMvqiecOx6tXgDTuBpxyd40Byp2j.iBM5lR/oQnlpi8nAje
tls_server_config:
cert_file: node_exporter.crt
key_file: node_exporter.key
"""
with open(CONFIG_PATH, "w") as f:
f.write(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)
def change_password(user, password):
if not os.path.exists(CONFIG_PATH):
print("Brak pliku config.yml najpierw użyj --install-secured")
return return
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
hashed = bcrypt.hashpw(plain_password.encode(), bcrypt.gensalt()).decode() with open(CONFIG_PATH, 'r') as f:
with open(config_path, "r") as f:
lines = f.readlines() lines = f.readlines()
new_lines = [] new_lines = []
@ -98,10 +241,9 @@ def update_basic_auth_user(credential: str):
in_auth = True in_auth = True
new_lines.append(line) new_lines.append(line)
continue continue
if in_auth and line.startswith(" ") and ":" in line: if in_auth and line.startswith(" ") and ":" in line:
current_user = line.split(":")[0].strip() existing_user = line.split(":")[0].strip()
if current_user == user: if existing_user == user:
new_lines.append(f" {user}: {hashed}\n") new_lines.append(f" {user}: {hashed}\n")
user_updated = True user_updated = True
else: else:
@ -118,106 +260,58 @@ def update_basic_auth_user(credential: str):
if in_auth and not user_updated: if in_auth and not user_updated:
new_lines.append(f" {user}: {hashed}\n") new_lines.append(f" {user}: {hashed}\n")
with open(config_path, "w") as f: with open(CONFIG_PATH, "w") as f:
f.writelines(new_lines) f.writelines(new_lines)
print(f"Zmieniono hasło dla użytkownika '{user}'.") print(f"Zmieniono hasło dla użytkownika '{user}'")
def print_help():
print("""
Użycie:
node_exporter_manager.py --install # Instaluje node_exporter i uruchamia usługę
node_exporter_manager.py --update # Aktualizuje node_exporter do najnowszej wersji (jeśli potrzeba)
node_exporter_manager.py --uninstall # Usuwa node_exporter, usługę, użytkownika
node_exporter_manager.py --setup # Instaluje ten skrypt do /usr/local/bin, dodaje CRON i logrotate
node_exporter_manager.py --install-secured # Instalacja z TLS + basic auth (certyfikat + config.yml)
node_exporter_manager.py --set-password=user:haslo # Zmiana hasła w config.yml
""")
def write_systemd_service(secured=False): # ----------------- GŁÓWNY BLOK -----------------
exec_line = f'{NODE_EXPORTER_BIN} --web.config.file="{NODE_EXPORTER_DIR}/config.yml"' if secured else NODE_EXPORTER_BIN
content = f"""[Unit]
Description=Node Exporter
Wants=network-online.target
After=network-online.target
[Service] if __name__ == '__main__':
User=node_exporter if os.geteuid() != 0:
ExecStart={exec_line} print("Ten skrypt musi być uruchomiony jako root.")
sys.exit(1)
[Install] if len(sys.argv) != 2:
WantedBy=default.target print_help()
""" sys.exit(1)
with open(SYSTEMD_SERVICE_PATH, "w") as f:
f.write(content)
arg = sys.argv[1]
def setup_ssl_and_auth(): try:
os.makedirs(NODE_EXPORTER_DIR, exist_ok=True) if arg == '--install':
run_safe([ install()
"openssl", "req", "-new", "-newkey", "rsa:4096", "-days", "3650", "-nodes", "-x509", elif arg == '--update':
"-subj", "/C=PL/ST=X/L=X/O=linuxiarz.pl/CN=*.linuxiarz.pl", update()
"-keyout", f"{NODE_EXPORTER_DIR}/node_exporter.key", elif arg == '--uninstall':
"-out", f"{NODE_EXPORTER_DIR}/node_exporter.crt" uninstall()
]) elif arg == '--setup':
setup()
config = """basic_auth_users: elif arg == '--install-secured':
root: $2y$10$SNr5iyJMvqiecOx6tXgDTuBpxyd40Byp2j.iBM5lR/oQnlpi8nAje install()
setup_secured_config()
tls_server_config: elif arg.startswith('--set-password='):
cert_file: node_exporter.crt user_pass = arg.split('=')[1]
key_file: node_exporter.key if ':' not in user_pass:
""" print("Użyj formatu --set-password=user:haslo")
with open(os.path.join(NODE_EXPORTER_DIR, "config.yml"), "w") as f: sys.exit(1)
f.write(config) user, passwd = user_pass.split(':', 1)
change_password(user, passwd)
shutil.chown(NODE_EXPORTER_DIR, user="node_exporter", group="node_exporter") else:
for file in os.listdir(NODE_EXPORTER_DIR): print_help()
shutil.chown(os.path.join(NODE_EXPORTER_DIR, file), user="node_exporter", group="node_exporter") sys.exit(1)
except Exception as e:
logging.error(f"Błąd krytyczny: {e}")
def enable_and_start_service(): print(f"Wystąpił błąd: {e}")
run_safe(["systemctl", "daemon-reload"]) sys.exit(1)
run_safe(["systemctl", "enable", "--now", "node_exporter"])
def uninstall():
subprocess.run(["systemctl", "stop", "node_exporter"], check=False)
subprocess.run(["systemctl", "disable", "node_exporter"], check=False)
if os.path.exists(SYSTEMD_SERVICE_PATH):
os.remove(SYSTEMD_SERVICE_PATH)
if os.path.exists(NODE_EXPORTER_BIN):
os.remove(NODE_EXPORTER_BIN)
if os.path.isdir(NODE_EXPORTER_DIR):
shutil.rmtree(NODE_EXPORTER_DIR)
run_safe(["systemctl", "daemon-reload"])
print("Node Exporter odinstalowany.")
def main():
parser = argparse.ArgumentParser(description="Installer for Prometheus Node Exporter")
parser.add_argument("--secured", action="store_true", help="Enable TLS and basic auth")
parser.add_argument("--update", action="store_true", help="Force update of node_exporter binary")
parser.add_argument("--uninstall", action="store_true", help="Uninstall node_exporter and clean files")
parser.add_argument("--set-password", type=str, help="Ustaw basic auth (format: user:haslo), działa tylko z --secured")
args = parser.parse_args()
if args.uninstall:
uninstall()
return
url = fetch_latest_download_url()
extracted_dir = download_and_extract(url)
ensure_node_exporter_user()
install_binary(extracted_dir, force_update=args.update)
if args.secured:
setup_ssl_and_auth()
write_systemd_service(secured=args.secured)
enable_and_start_service()
if args.set_password:
update_basic_auth_user(args.set_password)
print("Instalacja zakończona.")
if __name__ == "__main__":
main()