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 argparse
import json
import urllib.request
import sys
import subprocess
import requests
import tarfile
import shutil
import logging
import pwd
import subprocess
from pathlib import Path
try:
import bcrypt
BCRYPT_AVAILABLE = True
except ImportError:
BCRYPT_AVAILABLE = False
SYSTEMD_SERVICE_PATH = "/etc/systemd/system/node_exporter.service"
NODE_EXPORTER_BIN = "/usr/local/bin/node_exporter"
NODE_EXPORTER_DIR = "/etc/node_exporter"
# 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')
# Konfiguracja logowania
logging.basicConfig(
filename=LOG_FILE,
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s'
)
def run_safe(cmd):
subprocess.run(cmd, shell=isinstance(cmd, str), check=True)
# ----------------- FUNKCJE POMOCNICZE -----------------
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():
def run_cmd(cmd, check=True):
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:
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):
if os.path.exists(NODE_EXPORTER_BIN):
if not force_update:
print("Binarka już istnieje. Użyj --update aby zaktualizować.")
return
subprocess.run(["systemctl", "stop", "node_exporter"], check=False)
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")
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)
os.chmod(NODE_EXPORTER_BIN, 0o755)
print("Zainstalowano node_exporter.")
def update_basic_auth_user(credential: str):
if not BCRYPT_AVAILABLE:
print("Błąd: brak modułu 'bcrypt'. Zainstaluj go poleceniem:\n pip install bcrypt")
def install():
if Path(BIN_TARGET).exists():
print("Node Exporter jest już zainstalowany.")
response = input("Czy chcesz przeinstalować z konfiguracją secured (TLS + Basic Auth)? [t/N]: ")
if response.lower() == 't':
uninstall()
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
if ":" not in credential:
print("Błąd: użyj formatu --set-password user:haslo")
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)
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
print(f"Aktualizacja z {local_version} do {latest_version}...")
user, plain_password = credential.split(":", 1)
config_path = os.path.join(NODE_EXPORTER_DIR, "config.yml")
if not os.path.exists(config_path):
print("Plik config.yml nie istnieje uruchom instalację z --secured najpierw.")
run_cmd(['systemctl', 'stop', 'node_exporter'])
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)
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
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()
new_lines = []
@ -98,10 +241,9 @@ def update_basic_auth_user(credential: str):
in_auth = True
new_lines.append(line)
continue
if in_auth and line.startswith(" ") and ":" in line:
current_user = line.split(":")[0].strip()
if current_user == user:
existing_user = line.split(":")[0].strip()
if existing_user == user:
new_lines.append(f" {user}: {hashed}\n")
user_updated = True
else:
@ -118,106 +260,58 @@ def update_basic_auth_user(credential: str):
if in_auth and not user_updated:
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)
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):
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
# ----------------- GŁÓWNY BLOK -----------------
[Service]
User=node_exporter
ExecStart={exec_line}
if __name__ == '__main__':
if os.geteuid() != 0:
print("Ten skrypt musi być uruchomiony jako root.")
sys.exit(1)
[Install]
WantedBy=default.target
"""
with open(SYSTEMD_SERVICE_PATH, "w") as f:
f.write(content)
if len(sys.argv) != 2:
print_help()
sys.exit(1)
arg = sys.argv[1]
def setup_ssl_and_auth():
os.makedirs(NODE_EXPORTER_DIR, exist_ok=True)
run_safe([
"openssl", "req", "-new", "-newkey", "rsa:4096", "-days", "3650", "-nodes", "-x509",
"-subj", "/C=PL/ST=X/L=X/O=linuxiarz.pl/CN=*.linuxiarz.pl",
"-keyout", f"{NODE_EXPORTER_DIR}/node_exporter.key",
"-out", f"{NODE_EXPORTER_DIR}/node_exporter.crt"
])
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(os.path.join(NODE_EXPORTER_DIR, "config.yml"), "w") as f:
f.write(config)
shutil.chown(NODE_EXPORTER_DIR, user="node_exporter", group="node_exporter")
for file in os.listdir(NODE_EXPORTER_DIR):
shutil.chown(os.path.join(NODE_EXPORTER_DIR, file), user="node_exporter", group="node_exporter")
def enable_and_start_service():
run_safe(["systemctl", "daemon-reload"])
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:
try:
if arg == '--install':
install()
elif arg == '--update':
update()
elif arg == '--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()
elif arg == '--setup':
setup()
elif arg == '--install-secured':
install()
setup_secured_config()
elif arg.startswith('--set-password='):
user_pass = arg.split('=')[1]
if ':' not in user_pass:
print("Użyj formatu --set-password=user:haslo")
sys.exit(1)
user, passwd = user_pass.split(':', 1)
change_password(user, passwd)
else:
print_help()
sys.exit(1)
except Exception as e:
logging.error(f"Błąd krytyczny: {e}")
print(f"Wystąpił błąd: {e}")
sys.exit(1)