snapshot
This commit is contained in:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "aim-inventory-manager"
|
||||
version = "2.1.2"
|
||||
version = "2.2.2"
|
||||
description = "AIM - Ansible Inventory Manager"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "2.1.2"
|
||||
__version__ = "2.2.2"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -25,6 +25,11 @@ class Config:
|
||||
ui_ascii: bool = False
|
||||
ui_output: str = "compact"
|
||||
|
||||
checkmk_agent_base_url: str = ""
|
||||
checkmk_agent_version: str = ""
|
||||
checkmk_agent_role_files_dir: Path = Path("/etc/ansible/roles/checkmk_agent/files")
|
||||
checkmk_monitoring_scripts_dir: Path = Path("/etc/checkmk_monitoring_scripts")
|
||||
|
||||
@property
|
||||
def inventory_dir(self) -> Path:
|
||||
return self.root_dir / "inventories"
|
||||
@@ -64,8 +69,19 @@ class Config:
|
||||
for setting in ("clear_screen", "ascii"):
|
||||
if setting in ui and not isinstance(ui[setting], bool):
|
||||
raise ValueError(f"ui.{setting} must be true or false")
|
||||
return cls(root, service_user, required_group, platform_groups,
|
||||
ui.get("clear_screen", True), ui.get("ascii", False), output)
|
||||
maintenance = data.get("maintenance", {}) or {}
|
||||
if not isinstance(maintenance, dict):
|
||||
raise ValueError("maintenance must be a mapping")
|
||||
return cls(
|
||||
root_dir=root, service_user=service_user, required_group=required_group,
|
||||
platform_groups=platform_groups,
|
||||
ui_clear_screen=ui.get("clear_screen", True),
|
||||
ui_ascii=ui.get("ascii", False), ui_output=output,
|
||||
checkmk_agent_base_url=str(maintenance.get("checkmk_agent_base_url", "")).strip(),
|
||||
checkmk_agent_version=str(maintenance.get("checkmk_agent_version", "")).strip(),
|
||||
checkmk_agent_role_files_dir=Path(str(maintenance.get("checkmk_agent_role_files_dir", "/etc/ansible/roles/checkmk_agent/files"))),
|
||||
checkmk_monitoring_scripts_dir=Path(str(maintenance.get("checkmk_monitoring_scripts_dir", "/etc/checkmk_monitoring_scripts"))),
|
||||
)
|
||||
except Exception as exc:
|
||||
raise ConfigurationError(f"Could not load {path}: {exc}") from exc
|
||||
|
||||
@@ -86,6 +102,15 @@ class Config:
|
||||
if not isinstance(ui, dict):
|
||||
raise ConfigurationError("Existing ui configuration is not a mapping")
|
||||
ui.update(clear_screen=self.ui_clear_screen, ascii=self.ui_ascii, output=self.ui_output)
|
||||
maintenance = data.setdefault("maintenance", {})
|
||||
if not isinstance(maintenance, dict):
|
||||
raise ConfigurationError("Existing maintenance configuration is not a mapping")
|
||||
maintenance.update(
|
||||
checkmk_agent_base_url=self.checkmk_agent_base_url,
|
||||
checkmk_agent_version=self.checkmk_agent_version,
|
||||
checkmk_agent_role_files_dir=str(self.checkmk_agent_role_files_dir),
|
||||
checkmk_monitoring_scripts_dir=str(self.checkmk_monitoring_scripts_dir),
|
||||
)
|
||||
fd, name = tempfile.mkstemp(prefix=".aim-config-", dir=self.root_dir)
|
||||
tmp = Path(name)
|
||||
try:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
"""Controller-local maintenance helpers."""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,116 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import tempfile
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
from aim.exceptions import AIMError
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PackageResult:
|
||||
label: str
|
||||
filename: str
|
||||
source_url: str
|
||||
size: int
|
||||
old_sha256: str | None
|
||||
new_sha256: str
|
||||
|
||||
|
||||
class CheckmkAgentUpdater:
|
||||
"""Download and atomically stage Checkmk agent packages on the controller."""
|
||||
|
||||
def __init__(self, target_dir: Path):
|
||||
self.target_dir = target_dir
|
||||
|
||||
@staticmethod
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open('rb') as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b''):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
@staticmethod
|
||||
def _specs(base_url: str, version: str) -> list[tuple[str, str, str]]:
|
||||
base = base_url.rstrip('/')
|
||||
return [
|
||||
('Windows MSI', f'{base}/windows/check_mk_agent.msi', 'check_mk_agent.msi'),
|
||||
('Debian / Ubuntu DEB', f'{base}/check-mk-agent_{version}_all.deb', 'check-mk-agent.deb'),
|
||||
('RHEL / Alma / Rocky RPM', f'{base}/check-mk-agent-{version}.noarch.rpm', 'check-mk-agent.rpm'),
|
||||
]
|
||||
|
||||
def update(self, base_url: str, version: str) -> list[PackageResult]:
|
||||
base_url = base_url.strip().rstrip('/')
|
||||
version = version.strip()
|
||||
if not base_url:
|
||||
raise AIMError('Checkmk agent base URL is required.')
|
||||
if not version:
|
||||
raise AIMError('Checkmk agent version is required.')
|
||||
if not re.fullmatch(r'\d+\.\d+\.\d+p\d+-\d+', version):
|
||||
raise AIMError(
|
||||
'Checkmk agent version must use the package format <major>.<minor>.<patch>p<patchlevel>-<revision> '
|
||||
'(example: 2.4.0p21-1).'
|
||||
)
|
||||
if not base_url.startswith(('https://', 'http://')):
|
||||
raise AIMError('Checkmk agent base URL must start with https:// or http://.')
|
||||
if not base_url.endswith('/check_mk/agents'):
|
||||
raise AIMError('Checkmk agent base URL must end with /check_mk/agents.')
|
||||
|
||||
specs = self._specs(base_url, version)
|
||||
downloaded: list[tuple[str, str, str, Path, int, str]] = []
|
||||
with tempfile.TemporaryDirectory(prefix='aim-checkmk-agent-') as tmp_name:
|
||||
tmp = Path(tmp_name)
|
||||
for label, url, filename in specs:
|
||||
destination = tmp / filename
|
||||
try:
|
||||
request = Request(url, headers={'User-Agent': 'bitformer-AIM/CheckmkAgentUpdater'})
|
||||
with urlopen(request, timeout=60) as response, destination.open('wb') as output:
|
||||
shutil.copyfileobj(response, output)
|
||||
except (HTTPError, URLError, TimeoutError, OSError) as exc:
|
||||
raise AIMError(f'Could not download {label} from {url}: {exc}') from exc
|
||||
size = destination.stat().st_size
|
||||
if size <= 0:
|
||||
raise AIMError(f'Downloaded {label} is empty: {url}')
|
||||
downloaded.append((label, url, filename, destination, size, self._sha256(destination)))
|
||||
|
||||
try:
|
||||
self.target_dir.mkdir(parents=True, exist_ok=True)
|
||||
except OSError as exc:
|
||||
raise AIMError(f'Could not create Checkmk role files directory {self.target_dir}: {exc}') from exc
|
||||
|
||||
staged: list[tuple[str, str, str, Path, Path, int, str, str | None]] = []
|
||||
try:
|
||||
# Stage every file in the destination filesystem before replacing anything.
|
||||
for label, url, filename, source, size, new_hash in downloaded:
|
||||
target = self.target_dir / filename
|
||||
old_hash = self._sha256(target) if target.exists() else None
|
||||
fd, stage_name = tempfile.mkstemp(prefix=f'.{filename}.aim-', dir=self.target_dir)
|
||||
stage = Path(stage_name)
|
||||
try:
|
||||
with os.fdopen(fd, 'wb') as output, source.open('rb') as input_stream:
|
||||
shutil.copyfileobj(input_stream, output)
|
||||
output.flush()
|
||||
os.fsync(output.fileno())
|
||||
stage.chmod(0o644)
|
||||
except Exception:
|
||||
stage.unlink(missing_ok=True)
|
||||
raise
|
||||
staged.append((label, url, filename, stage, target, size, new_hash, old_hash))
|
||||
|
||||
results: list[PackageResult] = []
|
||||
for label, url, filename, stage, target, size, new_hash, old_hash in staged:
|
||||
os.replace(stage, target)
|
||||
results.append(PackageResult(label, filename, url, size, old_hash, new_hash))
|
||||
return results
|
||||
except OSError as exc:
|
||||
raise AIMError(f'Could not stage Checkmk agent packages in {self.target_dir}: {exc}') from exc
|
||||
finally:
|
||||
for _label, _url, _filename, stage, _target, _size, _new_hash, _old_hash in staged:
|
||||
stage.unlink(missing_ok=True)
|
||||
@@ -0,0 +1,75 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
from aim.exceptions import AIMError
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RepositoryState:
|
||||
path: Path
|
||||
branch: str
|
||||
upstream: str
|
||||
head: str
|
||||
tracked_dirty: bool
|
||||
untracked_count: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SyncResult:
|
||||
before: str
|
||||
after: str
|
||||
upstream: str
|
||||
changed: bool
|
||||
stat: str
|
||||
|
||||
|
||||
class ShadowRepository:
|
||||
"""Manage a checkout that intentionally mirrors its configured upstream."""
|
||||
|
||||
def __init__(self, path: Path):
|
||||
self.path = path
|
||||
|
||||
def _git(self, *args: str, check: bool = True) -> str:
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
['git', '-C', str(self.path), *args],
|
||||
text=True, capture_output=True, check=False,
|
||||
)
|
||||
except OSError as exc:
|
||||
raise AIMError(f'Could not execute git: {exc}') from exc
|
||||
output = (proc.stdout or '').strip()
|
||||
error = (proc.stderr or '').strip()
|
||||
if check and proc.returncode != 0:
|
||||
detail = error or output or f'exit code {proc.returncode}'
|
||||
raise AIMError(f'git {" ".join(args)} failed: {detail}')
|
||||
return output
|
||||
|
||||
def inspect(self) -> RepositoryState:
|
||||
if not self.path.is_dir():
|
||||
raise AIMError(f'Monitoring scripts directory does not exist: {self.path}')
|
||||
if self._git('rev-parse', '--is-inside-work-tree') != 'true':
|
||||
raise AIMError(f'Not a Git working tree: {self.path}')
|
||||
branch = self._git('branch', '--show-current') or '(detached HEAD)'
|
||||
if branch == '(detached HEAD)':
|
||||
raise AIMError('The monitoring scripts shadow repository is in detached HEAD state.')
|
||||
upstream = self._git('rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}')
|
||||
head = self._git('rev-parse', 'HEAD')
|
||||
tracked = self._git('status', '--porcelain', '--untracked-files=no')
|
||||
untracked = self._git('ls-files', '--others', '--exclude-standard')
|
||||
return RepositoryState(self.path, branch, upstream, head, bool(tracked.strip()),
|
||||
len([line for line in untracked.splitlines() if line.strip()]))
|
||||
|
||||
def sync(self) -> SyncResult:
|
||||
before = self._git('rev-parse', 'HEAD')
|
||||
upstream = self._git('rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}')
|
||||
self._git('fetch', '--prune')
|
||||
remote_head = self._git('rev-parse', '@{u}')
|
||||
stat = '' if before == remote_head else self._git('diff', '--stat', f'{before}..{remote_head}', check=False)
|
||||
self._git('reset', '--hard', '@{u}')
|
||||
after = self._git('rev-parse', 'HEAD')
|
||||
if after != remote_head:
|
||||
raise AIMError(f'Repository reset did not reach upstream {upstream}. Expected {remote_head}, got {after}.')
|
||||
return SyncResult(before, after, upstream, before != after, stat)
|
||||
Binary file not shown.
Binary file not shown.
@@ -21,22 +21,114 @@ class PlaybookSpec:
|
||||
|
||||
|
||||
PLAYBOOKS: tuple[PlaybookSpec, ...] = (
|
||||
PlaybookSpec("checkmk_cleanup", "Cleanup CheckMK", "checkmk_cleanup.yml", "CheckMK", ("linux", "windows"), ("checkmk_cleanup_enabled=true",)),
|
||||
PlaybookSpec("checkmk_install_agent", "Install CheckMK Agent", "checkmk_install_agent.yml", "CheckMK", ("linux", "windows")),
|
||||
PlaybookSpec("checkmk_update_config", "Update CheckMK Config", "checkmk_update_config.yml", "CheckMK", ("linux", "windows")),
|
||||
PlaybookSpec("debug_ping", "Ping", "debug_ping.yml", "Debug", ("linux", "windows")),
|
||||
PlaybookSpec("debug_server_role_selection", "Server Role Selection", "debug_server_role_selection.yml", "Debug", ("linux", "windows")),
|
||||
PlaybookSpec("debug_disk_usage", "Disk Usage", "debug_disk_usage.yml", "Debug", ("windows",)),
|
||||
PlaybookSpec("patch_os", "Patch OS", "patch_os.yml", "Maintenance", ("linux", "windows")),
|
||||
PlaybookSpec("reboot_system", "Reboot System", "reboot_system.yml", "Maintenance", ("linux", "windows")),
|
||||
PlaybookSpec("backup_eventlog", "Backup Event Log", "backup_eventlog.yml", "Maintenance", ("windows",)),
|
||||
PlaybookSpec("start_stopped_services", "Start Stopped Services", "start_stopped_services.yml", "Maintenance", ("windows",)),
|
||||
PlaybookSpec("configure_sophos_initial", "Initial Bitformer Config", "configure_sophos_initial_bitformer_config.yml", "Sophos XGS", ("sophosxgs",), ask_pass=True, require_vault=True),
|
||||
PlaybookSpec("configure_sophosxgs", "Configure Sophos XGS", "configure_sophosxgs.yml", "Sophos XGS", ("sophosxgs",), customer_specific=True, ask_pass=True, require_vault=True),
|
||||
# CheckMK
|
||||
PlaybookSpec(
|
||||
"checkmk_cleanup",
|
||||
"Cleanup CheckMK",
|
||||
"checkmk_cleanup.yml",
|
||||
"CheckMK",
|
||||
("linux", "windows"),
|
||||
("checkmk_cleanup_enabled=true",),
|
||||
),
|
||||
PlaybookSpec(
|
||||
"checkmk_deploy",
|
||||
"Deploy CheckMK",
|
||||
"checkmk_deploy.yml",
|
||||
"CheckMK",
|
||||
("linux", "windows"),
|
||||
),
|
||||
PlaybookSpec(
|
||||
"checkmk_update_config",
|
||||
"Update CheckMK Config",
|
||||
"checkmk_update_config.yml",
|
||||
"CheckMK",
|
||||
("windows",),
|
||||
),
|
||||
PlaybookSpec(
|
||||
"checkmk_update_scripts",
|
||||
"Update CheckMK Scripts",
|
||||
"checkmk_update_scripts.yml",
|
||||
"CheckMK",
|
||||
("linux", "windows"),
|
||||
),
|
||||
|
||||
# Debug
|
||||
PlaybookSpec(
|
||||
"debug_ping",
|
||||
"Ping",
|
||||
"debug_ping.yml",
|
||||
"Debug",
|
||||
("linux", "windows"),
|
||||
),
|
||||
PlaybookSpec(
|
||||
"debug_server_role_selection",
|
||||
"Server Role Selection",
|
||||
"debug_server_role_selection.yml",
|
||||
"Debug",
|
||||
("linux", "windows"),
|
||||
),
|
||||
PlaybookSpec(
|
||||
"debug_disk_usage",
|
||||
"Disk Usage",
|
||||
"debug_disk_usage.yml",
|
||||
"Debug",
|
||||
("linux", "windows"),
|
||||
),
|
||||
|
||||
# Maintenance
|
||||
PlaybookSpec(
|
||||
"maintenance_patch_os",
|
||||
"Patch OS",
|
||||
"maintenance_patch_os.yml",
|
||||
"Maintenance",
|
||||
("linux", "windows"),
|
||||
),
|
||||
PlaybookSpec(
|
||||
"maintenance_reboot",
|
||||
"Reboot System",
|
||||
"maintenance_reboot.yml",
|
||||
"Maintenance",
|
||||
("linux", "windows"),
|
||||
),
|
||||
PlaybookSpec(
|
||||
"maintenance_backup_event_log",
|
||||
"Backup System Logs",
|
||||
"maintenance_backup_event_log.yml",
|
||||
"Maintenance",
|
||||
("linux", "windows"),
|
||||
),
|
||||
PlaybookSpec(
|
||||
"maintenance_start_stopped_services",
|
||||
"Start Stopped Services",
|
||||
"maintenance_start_stopped_services.yml",
|
||||
"Maintenance",
|
||||
("linux", "windows"),
|
||||
),
|
||||
|
||||
# Sophos XGS
|
||||
# PlaybookSpec(
|
||||
# "configure_sophos_initial",
|
||||
# "Initial Bitformer Config",
|
||||
# "configure_sophos_initial_bitformer_config.yml",
|
||||
# "Sophos XGS",
|
||||
# ("sophosxgs",),
|
||||
# ask_pass=True,
|
||||
# require_vault=True,
|
||||
# ),
|
||||
# PlaybookSpec(
|
||||
# "configure_sophosxgs",
|
||||
# "Configure Sophos XGS",
|
||||
# "configure_sophosxgs.yml",
|
||||
# "Sophos XGS",
|
||||
# ("sophosxgs",),
|
||||
# customer_specific=True,
|
||||
# ask_pass=True,
|
||||
# require_vault=True,
|
||||
# ),
|
||||
)
|
||||
|
||||
CATEGORY_ORDER = ("CheckMK", "Debug", "Maintenance", "Sophos XGS")
|
||||
|
||||
# CATEGORY_ORDER = ("CheckMK", "Debug", "Maintenance", "Sophos XGS")
|
||||
CATEGORY_ORDER = ("CheckMK", "Debug", "Maintenance")
|
||||
|
||||
class PlaybookManager:
|
||||
def __init__(self, customers, config):
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -235,6 +235,10 @@ class AdministrationScreens:
|
||||
MenuItem('6', 'Screen clearing', 'Enabled' if draft.ui_clear_screen else 'Disabled'),
|
||||
MenuItem('7', 'Character style', 'ASCII' if draft.ui_ascii else 'Unicode (terminal-dependent)'),
|
||||
MenuItem('8', 'Command output', draft.ui_output),
|
||||
MenuItem('9', 'Checkmk agent base URL', draft.checkmk_agent_base_url or '(not set)'),
|
||||
MenuItem('10', 'Checkmk agent version', draft.checkmk_agent_version or '(not set)'),
|
||||
MenuItem('11', 'Checkmk agent role files', str(draft.checkmk_agent_role_files_dir)),
|
||||
MenuItem('12', 'Checkmk monitoring scripts', str(draft.checkmk_monitoring_scripts_dir)),
|
||||
])
|
||||
if choice == '0':
|
||||
if not dirty or self.confirm('Discard unsaved settings and go back?', default=False):
|
||||
@@ -264,9 +268,27 @@ class AdministrationScreens:
|
||||
draft.ui_ascii = not draft.ui_ascii
|
||||
elif choice == '8':
|
||||
draft.ui_output = 'live' if draft.ui_output == 'compact' else 'compact'
|
||||
elif choice == '9':
|
||||
draft.checkmk_agent_base_url = self.optional_value('Checkmk agent base URL', draft.checkmk_agent_base_url).rstrip('/')
|
||||
elif choice == '10':
|
||||
draft.checkmk_agent_version = self.optional_value('Checkmk agent version', draft.checkmk_agent_version)
|
||||
elif choice == '11':
|
||||
value = Path(self.required_value('Checkmk agent role files directory', str(draft.checkmk_agent_role_files_dir))).expanduser()
|
||||
if not value.is_absolute():
|
||||
raise ValueError('Use an absolute Checkmk agent role files path.')
|
||||
draft.checkmk_agent_role_files_dir = value
|
||||
elif choice == '12':
|
||||
value = Path(self.required_value('Checkmk monitoring scripts directory', str(draft.checkmk_monitoring_scripts_dir))).expanduser()
|
||||
if not value.is_absolute():
|
||||
raise ValueError('Use an absolute monitoring scripts path.')
|
||||
draft.checkmk_monitoring_scripts_dir = value
|
||||
elif choice == '5':
|
||||
self.ui.review('Save application settings', [('File', draft.config_path), ('Root directory', draft.root_dir),
|
||||
('Service account', draft.service_user), ('Authorized group', draft.required_group)],
|
||||
('Service account', draft.service_user), ('Authorized group', draft.required_group),
|
||||
('Checkmk agent source', draft.checkmk_agent_base_url or '(not set)'),
|
||||
('Checkmk agent version', draft.checkmk_agent_version or '(not set)'),
|
||||
('Checkmk role files', draft.checkmk_agent_role_files_dir),
|
||||
('Monitoring scripts', draft.checkmk_monitoring_scripts_dir)],
|
||||
warning='Changing defaults does not migrate existing inventory variables, users or SSH keys.')
|
||||
if self.confirm('Save these settings?', default=False):
|
||||
draft.save()
|
||||
|
||||
@@ -34,12 +34,13 @@ from aim.ui.access import AccessScreens
|
||||
from aim.ui.playbooks import PlaybookScreens
|
||||
from aim.ui.administration import AdministrationScreens
|
||||
from aim.ui.targets import TargetScreens
|
||||
from aim.ui.maintenance import MaintenanceScreens
|
||||
|
||||
_STYLE = re.compile(r'\[/?(?:bold|dim|red|green|yellow|cyan|magenta)(?: [^\]]+)?\]')
|
||||
|
||||
|
||||
class App(CustomerScreens, HostScreens, AccessScreens, PlaybookScreens,
|
||||
AdministrationScreens, TargetScreens):
|
||||
AdministrationScreens, TargetScreens, MaintenanceScreens):
|
||||
def __init__(self, config: Config | None = None, *, console: Console | None = None,
|
||||
no_clear: bool = False, plain: bool = False, live_output: bool = False):
|
||||
self.config = config or Config.load()
|
||||
@@ -274,7 +275,8 @@ class App(CustomerScreens, HostScreens, AccessScreens, PlaybookScreens,
|
||||
MenuItem('2', 'Open customer', 'Hosts, access, Vault and playbooks'),
|
||||
MenuItem('3', 'Browse customers', 'Local inventory overview'),
|
||||
MenuItem('4', 'Delete customer', 'Remove the complete customer inventory', True),
|
||||
MenuItem('5', 'Settings', 'Application and display preferences'),
|
||||
MenuItem('5', 'Maintenance', 'Controller-local Checkmk artifacts and script synchronization'),
|
||||
MenuItem('6', 'Settings', 'Application and display preferences'),
|
||||
], back='Exit')
|
||||
if choice == '0':
|
||||
return 0
|
||||
@@ -287,6 +289,8 @@ class App(CustomerScreens, HostScreens, AccessScreens, PlaybookScreens,
|
||||
elif choice == '4':
|
||||
self.call(self.delete_customer)
|
||||
elif choice == '5':
|
||||
self.call(self.maintenance_menu)
|
||||
elif choice == '6':
|
||||
self.call(self.config_menu)
|
||||
except EOFError:
|
||||
return 0
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from aim.exceptions import AIMError
|
||||
from aim.maintenance.checkmk import CheckmkAgentUpdater
|
||||
from aim.maintenance.repository import ShadowRepository
|
||||
from aim.ui.components import MenuItem
|
||||
|
||||
|
||||
class MaintenanceScreens:
|
||||
def maintenance_menu(self) -> None:
|
||||
while True:
|
||||
self.clear()
|
||||
self.header('AIM > Maintenance', 'Controller-local maintenance. No customer inventory is modified.')
|
||||
choice = self.menu([
|
||||
MenuItem('1', 'Update Checkmk agent packages', 'Download MSI / DEB / RPM into the checkmk_agent role'),
|
||||
MenuItem('2', 'Sync Checkmk monitoring scripts', 'Reset the managed shadow checkout to its configured upstream'),
|
||||
])
|
||||
if choice == '0':
|
||||
return
|
||||
if choice == '1':
|
||||
self.call(self.update_checkmk_agents)
|
||||
elif choice == '2':
|
||||
self.call(self.sync_checkmk_scripts)
|
||||
|
||||
def update_checkmk_agents(self) -> None:
|
||||
self.clear()
|
||||
self.header('AIM > Maintenance > Checkmk Agent Packages',
|
||||
'Downloads all package formats first; role files are replaced only after every download succeeds.')
|
||||
self.ui.details([
|
||||
('Expected base URL', 'https://<server>/<site>/check_mk/agents'),
|
||||
('URL ends at', '/check_mk/agents'),
|
||||
('Example', 'https://monitoring.example.de/monitoring/check_mk/agents'),
|
||||
('Version format', '<major>.<minor>.<patch>p<patchlevel>-<revision>'),
|
||||
('Version example', '2.4.0p21-1'),
|
||||
], title='Checkmk agent source')
|
||||
base_url = self.required_value('Checkmk agent base URL', self.config.checkmk_agent_base_url or None).rstrip('/')
|
||||
version = self.required_value('Checkmk agent version', self.config.checkmk_agent_version or None)
|
||||
target = Path(self.config.checkmk_agent_role_files_dir)
|
||||
self.ui.review('Update Checkmk agent packages', [
|
||||
('Base URL', base_url),
|
||||
('Version', version),
|
||||
('Target', target),
|
||||
('Windows source', f'{base_url}/windows/check_mk_agent.msi'),
|
||||
('Debian source', f'{base_url}/check-mk-agent_{version}_all.deb'),
|
||||
('RPM source', f'{base_url}/check-mk-agent-{version}.noarch.rpm'),
|
||||
], warning='Existing role packages are replaced only after all three downloads have completed successfully.')
|
||||
if not self.confirm('Update these local Checkmk agent packages?', default=False):
|
||||
return
|
||||
|
||||
updater = CheckmkAgentUpdater(target)
|
||||
self.ui.result('Downloading Checkmk agent packages...', level='info')
|
||||
results = updater.update(base_url, version)
|
||||
rows = []
|
||||
for result in results:
|
||||
old = result.old_sha256[:12] if result.old_sha256 else '(new file)'
|
||||
new = result.new_sha256[:12]
|
||||
rows.append((result.label, f'{result.filename} | {result.size} bytes | {old} -> {new}'))
|
||||
self.ui.details(rows, title='Updated packages')
|
||||
self.ui.result('All Checkmk agent packages were updated successfully.', title='Maintenance completed')
|
||||
if base_url != self.config.checkmk_agent_base_url or version != self.config.checkmk_agent_version:
|
||||
self.ui.result('Tip: save the base URL/version in Settings to use them as future defaults.', level='info')
|
||||
self.pause()
|
||||
|
||||
def sync_checkmk_scripts(self) -> None:
|
||||
self.clear()
|
||||
self.header('AIM > Maintenance > Checkmk Monitoring Scripts',
|
||||
'Managed shadow checkout: tracked local changes are intentionally discarded; untracked files are retained.')
|
||||
repo = ShadowRepository(Path(self.config.checkmk_monitoring_scripts_dir))
|
||||
state = repo.inspect()
|
||||
self.ui.review('Sync Checkmk monitoring scripts', [
|
||||
('Repository', state.path),
|
||||
('Branch', state.branch),
|
||||
('Upstream', state.upstream),
|
||||
('Current commit', state.head[:12]),
|
||||
('Tracked local changes', 'YES - will be discarded' if state.tracked_dirty else 'No'),
|
||||
('Untracked files', f'{state.untracked_count} (retained)'),
|
||||
('Operation', 'git fetch --prune; git reset --hard @{u}'),
|
||||
], warning=('Tracked local modifications will be discarded because this directory is a managed shadow copy.'
|
||||
if state.tracked_dirty else 'The checkout will be reset exactly to its configured upstream.'))
|
||||
if not self.confirm('Sync the monitoring scripts shadow copy?', default=False):
|
||||
return
|
||||
self.ui.result('Fetching and synchronizing monitoring scripts...', level='info')
|
||||
result = repo.sync()
|
||||
self.ui.details([
|
||||
('Upstream', result.upstream),
|
||||
('Before', result.before[:12]),
|
||||
('After', result.after[:12]),
|
||||
('Result', 'Updated' if result.changed else 'Already current'),
|
||||
('Changed files', result.stat or 'No upstream file changes'),
|
||||
], title='Repository sync')
|
||||
self.ui.result('Checkmk monitoring scripts are synchronized with the configured upstream.', title='Maintenance completed')
|
||||
self.pause()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user