Files
skrypty_narzedzia/node_exporter_manager.py
2025-06-08 00:08:37 +02:00

335 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
import os
import sys
import subprocess
import requests
import tarfile
import shutil
import logging
import pwd
from pathlib import Path
import bcrypt
# Stałe
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')
logging.basicConfig(
filename=LOG_FILE,
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s'
)
# ------------------ FUNKCJE ------------------
def run_cmd(cmd, check=True):
try:
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 detect_architecture():
arch = run_cmd(['uname', '-m'])
if arch in ['x86_64', 'amd64']:
return 'linux-amd64'
elif arch in ['aarch64', 'arm64']:
return 'linux-arm64'
elif arch.startswith('armv7') or arch.startswith('armv6'):
return 'linux-armv7'
else:
raise Exception(f"Nieobsługiwana architektura: {arch}")
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])
extract_dirname = filename.replace('.tar.gz', '')
extract_path = Path(extract_dirname)
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:
if sys.version_info >= (3, 12):
tar.extractall(path=download_path, filter='data')
else:
tar.extractall(path=download_path)
if not extract_path.is_dir():
raise Exception(f"Nie znaleziono rozpakowanego katalogu: {extract_path}")
return extract_path
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_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)
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)
run_cmd(['systemctl', 'daemon-reload'])
run_cmd(['systemctl', 'enable', '--now', 'node_exporter'])
def uninstall():
if Path(SERVICE_FILE).exists():
run_cmd(['systemctl', 'disable', '--now', 'node_exporter'])
os.remove(SERVICE_FILE)
if Path(BIN_TARGET).exists():
os.remove(BIN_TARGET)
try:
pwd.getpwnam(USER_NAME)
run_cmd(['userdel', USER_NAME])
except KeyError:
pass
if Path(USER_HOME).exists():
shutil.rmtree(USER_HOME)
def install():
if Path(BIN_TARGET).exists():
print("Node Exporter już zainstalowany. Użyj --update.")
return
arch = detect_architecture()
version, release = get_latest_version()
url = next(asset['browser_download_url'] for asset in release['assets'] if arch in asset['browser_download_url'])
extracted = download_and_extract(url)
install_binary(extracted)
create_user()
setup_service()
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}...")
arch = detect_architecture()
run_cmd(['systemctl', 'stop', 'node_exporter'])
url = next(asset['browser_download_url'] for asset in release['assets'] if arch 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():
source_path = Path(__file__).resolve()
target_path = Path('/usr/local/bin/node_exporter_manager.py')
if source_path != target_path:
shutil.copy(source_path, target_path)
target_path.chmod(0o755)
cron_line = f"15 3 * * * {target_path} --update >> /var/log/node_exporter_cron.log 2>&1"
try:
cron_result = run_cmd(['crontab', '-l'], check=False)
except Exception:
cron_result = ""
if cron_line not in cron_result:
with open('/tmp/node_exporter_cron', 'w') as f:
f.write(cron_result.strip() + '\n' + cron_line + '\n')
run_cmd(['crontab', '/tmp/node_exporter_cron'])
os.remove('/tmp/node_exporter_cron')
logrotate_path = '/etc/logrotate.d/node_exporter_manager'
logrotate_config = f"""{LOG_FILE} /var/log/node_exporter_cron.log {{
weekly
rotate 4
compress
missingok
notifempty
create 644 root root
}}
"""
with open(logrotate_path, 'w') as f:
f.write(logrotate_config)
# ------------------ ZABEZPIECZENIA ------------------
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()
with open(CONFIG_PATH, 'r') as f:
lines = f.readlines()
new_lines = []
in_auth = False
user_updated = False
for line in lines:
if line.strip().startswith("basic_auth_users:"):
in_auth = True
new_lines.append(line)
continue
if in_auth and line.startswith(" ") and ":" in line:
existing_user = line.split(":")[0].strip()
if existing_user == user:
new_lines.append(f" {user}: {hashed}\n")
user_updated = True
else:
new_lines.append(line)
elif in_auth and not line.startswith(" "):
if not user_updated:
new_lines.append(f" {user}: {hashed}\n")
user_updated = True
in_auth = False
new_lines.append(line)
else:
new_lines.append(line)
if in_auth and not user_updated:
new_lines.append(f" {user}: {hashed}\n")
with open(CONFIG_PATH, "w") as f:
f.writelines(new_lines)
print(f"Zmieniono hasło dla użytkownika '{user}'")
# ------------------ MAIN ------------------
if __name__ == '__main__':
if os.geteuid() != 0:
print("Ten skrypt musi być uruchomiony jako root.")
sys.exit(1)
if len(sys.argv) != 2:
print("""Użycie:
node_exporter_manager.py --install # Instalacja Node Exportera
node_exporter_manager.py --update # Aktualizacja do najnowszej wersji
node_exporter_manager.py --uninstall # Usunięcie Node Exportera
node_exporter_manager.py --setup # Konfiguracja automatycznych aktualizacji i logrotate
node_exporter_manager.py --install-secured # Instalacja z TLS + Basic Auth
node_exporter_manager.py --set-password=u:pass # Zmiana hasła basic auth (np. root:mojehaslo)
Uwaga: Uruchamiaj jako root!
""")
sys.exit(1)
try:
arg = sys.argv[1]
if arg == '--install':
install()
elif arg == '--update':
update()
elif arg == '--uninstall':
uninstall()
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)
u, p = user_pass.split(":", 1)
change_password(u, p)
else:
print(f"Nieznana opcja: {arg}")
sys.exit(1)
except Exception as e:
logging.error(f"Błąd krytyczny: {e}")
print(f"Wystąpił błąd: {e}")
sys.exit(1)