Compare commits
4 Commits
db60b5a2ea
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3dfc80b782 | |||
| d095887d2e | |||
| 7c24c7ba7f | |||
| 343fe9b22f |
@@ -0,0 +1,4 @@
|
||||
**/bak.yml
|
||||
**/vault.yml
|
||||
**/__pycache__
|
||||
**/tests
|
||||
@@ -0,0 +1,72 @@
|
||||
# AIM: Ansible Inventory Manager
|
||||
|
||||
**Current candidate: 3.3.0rc4. Canonical Ansible Core: 2.19.11. Service/wire/event API: 1.0.**
|
||||
|
||||
AIM is an independent controller product with a built-in terminal (`aim`), a machine
|
||||
interface (`aimctl`) and an additive Python facade (`aim.services.v1`). Add-ons consume
|
||||
Core contracts; they do not patch Core, emulate its console or own execution semantics.
|
||||
|
||||
## This release
|
||||
|
||||
The new `aim_output_v1` publisher convention and catalog-owned schemas expose purposeful
|
||||
operation data independently of task progress and per-target outcomes. Nine operations
|
||||
now publish reports: role detection, disk usage, event exports, service recovery, OS
|
||||
patching, Checkmk cleanup, user-config reading, agent installation and config updating.
|
||||
|
||||
Results are finite typed data, not raw Ansible stdout/debug/module dictionaries. Existing
|
||||
nonreporting operations keep `operation_result: null`. Both summary and detail clients
|
||||
receive final reports. The terminal retains native output; native Ansible retains its
|
||||
own execution behavior and does not become a Core API consumer.
|
||||
|
||||
3.3.0rc4 makes Windows patching wave-based around the native `ansible.windows.win_updates`
|
||||
orchestration. AIM submits the currently selected category set as one Windows Update wave
|
||||
with `reboot: false`, lets the module/WUA process that wave, and evaluates reboot policy only
|
||||
after the wave returns. A reboot ends the run by default; another post-reboot wave requires
|
||||
explicit `os_patching_rescan_after_reboot: true`. AIM no longer implements a per-update
|
||||
scheduler. Per-update result/HRESULT evidence is still normalized into `patch_summary_v1`.
|
||||
|
||||
## Install or update
|
||||
|
||||
Distribute the complete `AIM-Ansible-3.3.0rc4.zip` and matching `.zip.sha256` from a trusted
|
||||
channel. A checksum checks integrity, not publisher authenticity. No Git or patch workflow.
|
||||
|
||||
```bash
|
||||
cd /var/tmp
|
||||
sha256sum -c AIM-Ansible-3.3.0rc4.zip.sha256
|
||||
unzip AIM-Ansible-3.3.0rc4.zip
|
||||
cd aim-core-3.3.0rc4
|
||||
sudo python3 deploy/deploy.py update --dry-run
|
||||
# Review the plan, then stop active jobs and source writers before applying.
|
||||
sudo python3 deploy/deploy.py update --apply --quiesced
|
||||
hash -r
|
||||
aim --version
|
||||
aimctl --version
|
||||
aimctl capabilities
|
||||
```
|
||||
|
||||
The deployer discovers the existing AIM interpreter from its recognized launcher. Only
|
||||
supply `--aim-python /absolute/venv/bin/python` when discovery needs an explicit known path;
|
||||
never pass an empty shell variable. Provision AIM's declared dependencies separately for
|
||||
fresh installation, then use `install` instead of `update`. The deployer does not install
|
||||
packages, modify services or enable external execution.
|
||||
|
||||
Existing `scripts/aim.yml`, inventories, Vaults, keys, environments, add-ons and customer
|
||||
assets stay. The six explicitly retired Core documents listed in the deployment guide
|
||||
are removed with recovery copies; unknown operator documents are not purged.
|
||||
|
||||
## Canonical documentation
|
||||
|
||||
Start at [the documentation index](scripts/docs/README.md). There is one current document
|
||||
per topic, one current validation record and one current sanity checklist. Historical
|
||||
changes remain only in [CHANGELOG](scripts/CHANGELOG.md), not competing release guides.
|
||||
|
||||
- [Release notes](scripts/docs/RELEASE_NOTES.md) and [validation](scripts/docs/VALIDATION.md)
|
||||
- [Fresh installation](scripts/docs/INSTALLATION.md), [deployment/recovery](deploy/README.md), and [controller sanity tests](scripts/docs/SANITY.md)
|
||||
- [Authoritative Core guide](AGENTS.md) and [add-on guide](ADDON_AGENTS.md)
|
||||
- [API contract](scripts/docs/ADDON_API.md), [operation results](scripts/docs/OPERATION_RESULTS.md), [handoff](scripts/docs/RELEASE_HANDOFF.md)
|
||||
- [Playbooks](scripts/docs/PLAYBOOKS.md), [Checkmk settings](scripts/docs/CHECKMK.md), [executor staging](scripts/docs/EXECUTOR_STAGING.md)
|
||||
|
||||
**Acceptance:** implementation/local tests do not certify this candidate on Windows,
|
||||
Linux package managers or a deployed service sandbox. Previous controller successes
|
||||
are historical evidence, not new test passes. External execution is still opt-in and
|
||||
requires the authorized execution account, collection access and writable staging.
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
# AIM core ZIP deployment - 3.3.0rc8
|
||||
|
||||
This standard-library operational helper installs a complete source release and
|
||||
its two source-bound command launchers. No Git, patch files, release manifest,
|
||||
package installation, add-on inspection or account/group migration is used.
|
||||
Python 3.11+ on Linux is required. Development validators are not distributed.
|
||||
|
||||
## Verify and preview
|
||||
|
||||
Obtain the ZIP and its SHA-256 sidecar through a trusted channel. The sidecar is an
|
||||
integrity check, not a publisher signature. Stage outside the installation tree:
|
||||
|
||||
```bash
|
||||
cd /var/tmp
|
||||
sha256sum -c AIM-Ansible-3.3.0rc8.zip.sha256
|
||||
unzip AIM-Ansible-3.3.0rc8.zip
|
||||
cd aim-core-3.3.0rc8
|
||||
sudo python3 deploy/deploy.py update --dry-run
|
||||
```
|
||||
|
||||
The target defaults to `/etc/ansible`. `update` requires existing core source;
|
||||
`install` is for a fresh tree. Preview is the default without `--apply`.
|
||||
Do not extract over the live installation. Source and target must not overlap;
|
||||
symlink paths and unexpected source files are rejected.
|
||||
|
||||
3.3.0rc8 checks the existing **AIM** Python interpreter before making changes. It can
|
||||
infer it only from an unambiguous installed `aim` Python shebang. It does not assume
|
||||
that `sudo python3`, the Ansible interpreter or an add-on environment contains AIM's
|
||||
dependencies. It preserves a virtual environment's Python path without resolving
|
||||
its symlink to the system Python.
|
||||
|
||||
When discovery is unavailable (for example sudo has a different PATH), provide the
|
||||
already identified AIM interpreter explicitly. In the operator test session,
|
||||
`AIM_PYTHON` is the interpreter that successfully ran `scripts/aimctl.py`:
|
||||
|
||||
```bash
|
||||
test -x "$AIM_PYTHON" || { echo 'Set AIM_PYTHON to the existing AIM interpreter first.'; exit 1; }
|
||||
sudo python3 deploy/deploy.py update --aim-python "$AIM_PYTHON" --dry-run
|
||||
```
|
||||
|
||||
Shell wrappers and `#!/usr/bin/env ...` shebangs are not guessed. The interpreter
|
||||
must be an absolute executable Python 3.11+ path with no spaces (up to 120 characters),
|
||||
and must already contain the dependencies from `scripts/pyproject.toml`.
|
||||
|
||||
## Command directory and multiple installations
|
||||
|
||||
The preview prints the chosen interpreter, command directory, source operations
|
||||
and `@launchers/aim` / `@launchers/aimctl` operations. `@launchers` is a journal
|
||||
identifier, not a directory shipped in the source archive.
|
||||
|
||||
By default the command directory is the existing `aim` command's parent directory,
|
||||
or `/usr/local/bin` when no command exists and an interpreter was supplied. Override
|
||||
with `--bin-dir /absolute/command/directory`. A nondefault `--target` **requires** its
|
||||
own explicit `--bin-dir` so development deployment cannot silently replace production
|
||||
commands. Use the same chosen interpreter/directory on preview and apply.
|
||||
|
||||
Only recognized AIM Python entry scripts or AIM-managed launchers for this target
|
||||
may be replaced. Unrelated programs, unsafe symlink command destinations and launchers for
|
||||
another installation are refused. There is no automatic force-overwrite escape hatch.
|
||||
Choose a reviewed unused command directory when the existing layout is nonstandard.
|
||||
Ensure the selected directory is on the intended operator's PATH; aliases and another
|
||||
installation earlier on PATH remain the operator's responsibility.
|
||||
|
||||
## Apply while quiesced
|
||||
|
||||
Stop new jobs and exit active AIM/Ansible sessions. `--quiesced` acknowledges that
|
||||
source writers/runners have been stopped; it does not discover, kill or pause jobs.
|
||||
|
||||
```bash
|
||||
sudo python3 deploy/deploy.py update --apply --quiesced
|
||||
# Include the same --aim-python and --bin-dir options used in the preview, if any.
|
||||
hash -r
|
||||
command -v aim
|
||||
command -v aimctl
|
||||
aim --version
|
||||
aimctl --version
|
||||
aimctl capabilities
|
||||
```
|
||||
|
||||
The helper verifies the installed `aim --version` and `aimctl capabilities` against
|
||||
the source release before reporting success. It uses an explicit installed config
|
||||
path for its capability check. It does not invoke Ansible or contact managed hosts.
|
||||
Both launchers import the deployed `scripts/src/aim` source with the selected AIM
|
||||
Python. Old launchers and core source are included in protected recovery data.
|
||||
|
||||
A stable deployment lock prevents another cooperating deployment. Each source file
|
||||
is replaced atomically, preserving existing UID/GID/mode/extended attributes.
|
||||
New source files use 0644; new launchers use 0755, and recognized existing launchers
|
||||
retain their metadata with executable bits enabled. Directories use normal caller
|
||||
ownership/inheritance. This is not a recursive ownership-policy migration.
|
||||
|
||||
Recovery defaults to `/var/backups/aim-core`; the helper prints the exact private
|
||||
0700 recovery directory. `--backup-dir /private/path` selects another root outside
|
||||
source and installation. Keep sufficient space and apply your retention policy.
|
||||
|
||||
Obsolete files inside `scripts/src/aim/` and the explicitly retired Core documents below are pruned. Unknown customer files in
|
||||
other locations are retained. Add-on code must use its own namespace, not the core
|
||||
Python namespace.
|
||||
|
||||
**Preserved when present:** operator `scripts/aim.yml`, customer `.aim.yml`, inventories,
|
||||
Vaults, SSH keys, add-ons and their state/configuration, environments, external assets,
|
||||
unknown playbooks/roles and staged agent files. No dependencies, services, accounts,
|
||||
LDAP/local group memberships, remote credentials or add-on version gates are modified.
|
||||
Missing new settings are not automatically inserted into an operator's existing YAML.
|
||||
|
||||
## Python package and runtime scope
|
||||
|
||||
These launchers are source-bound entry points, not a pip reinstall. The helper does
|
||||
not change installed wheel/distribution metadata or upgrade packages. Machine clients
|
||||
calling the installed `aimctl` reach the deployed source. In-process Python clients
|
||||
must also resolve `aim` to this source (for example an existing editable installation
|
||||
or an explicitly configured source import path), not an old separately installed
|
||||
wheel. Verify `aim.__file__` and `aim.__version__` in that client's own environment.
|
||||
No add-on's Python environment is changed by core deployment.
|
||||
|
||||
For a fresh installation, provision an AIM Python 3.11+ environment and its declared
|
||||
`ruamel.yaml`/`rich` dependencies first, then pass that interpreter to `install`.
|
||||
Provide the separate canonical Ansible Core **2.19.11** runtime and approved collections
|
||||
from `requirements-controller.txt` / `requirements.yml` explicitly. The helper does
|
||||
not install from the network or mutate a system-managed Ansible installation.
|
||||
|
||||
For a nondefault controller root, maintain that installation's operator configuration
|
||||
and use `aimctl --config /absolute/root/scripts/aim.yml ...` when necessary. Existing
|
||||
terminal configuration discovery is unchanged; creating another launcher does not
|
||||
silently redirect a terminal's global configuration.
|
||||
|
||||
## Opt-in API execution
|
||||
|
||||
Follow `scripts/docs/SANITY.md` (historical evidence is in
|
||||
`scripts/docs/VALIDATION.md`). External execution remains disabled by
|
||||
default and is not enabled by deployment, readiness or the launcher smoke test.
|
||||
Preserve the existing YAML and merge only deliberately approved settings:
|
||||
|
||||
```yaml
|
||||
addons:
|
||||
execution_enabled: true
|
||||
runtime:
|
||||
ansible_playbook: /usr/bin/ansible-playbook
|
||||
```
|
||||
|
||||
The executable path is the operator's approved native runtime, not an assumption
|
||||
for every installation. Worker authorization, filesystem/key access, collections,
|
||||
connection dependencies and same-UID execution still apply. Do not make private keys
|
||||
group-readable to bypass an unsupported cross-user deployment.
|
||||
|
||||
## Recovery and interruption
|
||||
|
||||
Ordinary application/launcher-check failures attempt to restore touched source and
|
||||
launchers. The update is not a whole-tree atomic transaction: power loss or SIGKILL
|
||||
can leave partial source. Keep jobs stopped until recovery/version checks complete.
|
||||
Recovery journals contain intent, hashes and original metadata; they are local
|
||||
recovery records, not a distributed release manifest or inventory backup.
|
||||
|
||||
Use this 3.3.0rc8 deployer and the printed recovery directory; include the same target
|
||||
for a nondefault installation. Launcher paths are recorded in the journal.
|
||||
|
||||
```bash
|
||||
sudo python3 deploy/deploy.py rollback --from-backup /var/backups/aim-core/RECOVERY-DIRECTORY --dry-run
|
||||
sudo python3 deploy/deploy.py rollback --from-backup /var/backups/aim-core/RECOVERY-DIRECTORY --apply --quiesced
|
||||
hash -r
|
||||
aim --version
|
||||
```
|
||||
|
||||
Rollback validates installed/recovery hashes and refuses to overwrite subsequently
|
||||
modified source or launchers. Existing aim.yml is never rolled back or removed,
|
||||
even after a fresh install. Empty directories may remain. A newly created aimctl
|
||||
launcher is removed when restoring a previous release that had no such launcher.
|
||||
|
||||
No remote Ansible action, dependency installation, account/group change or add-on
|
||||
state is reversed. Keep independent backups of runtime/customer data.
|
||||
|
||||
## Required executor staging (independently provisioned units)
|
||||
|
||||
Read `scripts/docs/EXECUTOR_STAGING.md` in the installed tree. New add-on executor
|
||||
installers should provision owner-only staging and a narrow directory write
|
||||
exception by default, then run `aimctl staging-check` inside the actual unit at
|
||||
startup. Ordinary `sudo -u` success does not reproduce mount/syscall restrictions.
|
||||
|
||||
The source deployer does not inspect/edit/restart services. It preserves the
|
||||
working exception already applied by the operator. The automatic Core preflight
|
||||
is on by default, but cannot make a read-only mount writable. Do not remove the
|
||||
exception, broaden home write access or recursively chown anything during this
|
||||
update. The new startup command is available after the normal launcher refresh.
|
||||
|
||||
## Documentation consolidation in 3.3.0rc8
|
||||
|
||||
Current docs have stable topic names with one validation record and one acceptance
|
||||
checklist. Upgrade removes only these explicitly retired Core filenames (with
|
||||
protected rollback copies), including locally modified versions of those exact files:
|
||||
|
||||
- scripts/docs/RC19_HANDOFF.md
|
||||
- scripts/docs/SANITY_3.2.0.md
|
||||
- scripts/docs/SANITY_3.2.1rc2.md
|
||||
- scripts/docs/VERIFICATION.md
|
||||
- scripts/docs/LOCAL_VALIDATION.md
|
||||
- scripts/docs/CONTROLLER_ACCEPTANCE.md
|
||||
|
||||
Review REMOVE entries before applying. Move any operator notes out of these retired
|
||||
Core-owned names before deployment. Unknown Markdown files and other operator
|
||||
documents are preserved. Rollback restores retired document bytes/metadata through
|
||||
the existing recovery journal. No recursive docs purge or runtime deletion occurs.
|
||||
|
||||
The new playbooks/filter_plugins/*.py files and playbooks/schemas/*.yml files are
|
||||
Core-owned reporting source. They are deployed with the playbooks; no add-on Python
|
||||
environment or collection is modified.
|
||||
@@ -0,0 +1,593 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Operational full-source install/update, not a Git patcher or release validator.
|
||||
|
||||
Python 3.11+, standard library only. Does not install dependencies, change groups,
|
||||
start services, touch inventory/add-on data, or replace operator configuration.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
from dataclasses import dataclass
|
||||
import base64
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, timezone
|
||||
import fcntl
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import tomllib
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
class DeploymentError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def no_symlink(path: Path):
|
||||
for part in (path, *path.parents):
|
||||
if part.is_symlink():
|
||||
raise DeploymentError('Refusing a symlink in a deployment path: ' + str(part))
|
||||
|
||||
|
||||
def no_symlink_parents(path: Path):
|
||||
for part in path.parents:
|
||||
if part.is_symlink():
|
||||
raise DeploymentError('Refusing a symlink in a deployment parent path: ' + str(part))
|
||||
|
||||
|
||||
def canonical(path: Path) -> Path:
|
||||
# Check before normalization so an intermediate symlink cannot disappear.
|
||||
no_symlink(path.absolute())
|
||||
return Path(os.path.abspath(path))
|
||||
|
||||
|
||||
@contextmanager
|
||||
def deployment_lock(target: Path):
|
||||
lock = target / '.aim-core-deploy.lock'
|
||||
fd = os.open(lock, os.O_WRONLY | os.O_NONBLOCK | os.O_CREAT | os.O_CLOEXEC | getattr(os, 'O_NOFOLLOW', 0), 0o600)
|
||||
try:
|
||||
if not stat.S_ISREG(os.fstat(fd).st_mode):
|
||||
raise DeploymentError('Deployment lock is not a regular file.')
|
||||
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
yield
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
|
||||
def allowed(relative: Path) -> bool:
|
||||
parts = relative.parts
|
||||
if relative.as_posix() in {'requirements.yml', 'requirements-controller.txt', 'deploy/deploy.py', 'deploy/README.md'}:
|
||||
return True
|
||||
if len(parts) >= 3 and parts[:3] == ('scripts', 'src', 'aim'):
|
||||
return relative.suffix == '.py' or relative.as_posix() == 'scripts/src/aim/integrations/ansible.cfg'
|
||||
if len(parts) >= 3 and parts[:2] == ('scripts', 'docs'):
|
||||
return relative.suffix in ('.md', '.json')
|
||||
if len(parts) == 2 and parts[0] == 'scripts':
|
||||
return parts[1] in {'pyproject.toml', 'aim.yml', 'AIM-WinRM-OneTime.ps1', 'aimctl.py'}
|
||||
if len(parts) == 3 and parts[:2] == ('playbooks', 'filter_plugins'):
|
||||
return relative.suffix == '.py'
|
||||
if len(parts) >= 2 and parts[0] == 'playbooks':
|
||||
return relative.suffix in ('.yml', '.yaml', '.md')
|
||||
if len(parts) >= 3 and parts[0] == 'roles':
|
||||
if relative.name == 'README.md':
|
||||
return True
|
||||
return len(parts) >= 4 and parts[2] in ('tasks', 'defaults', 'handlers', 'meta', 'vars', 'templates') and relative.suffix in ('.yml', '.yaml', '.j2')
|
||||
return False
|
||||
|
||||
|
||||
def source_files(source: Path) -> dict[str, Path]:
|
||||
result = {}
|
||||
for parent, dirs, files in os.walk(source, followlinks=False):
|
||||
dirs[:] = sorted(d for d in dirs if d != '__pycache__')
|
||||
for d in dirs:
|
||||
no_symlink(Path(parent) / d)
|
||||
for name in sorted(files):
|
||||
path = Path(parent) / name
|
||||
relative = path.relative_to(source)
|
||||
if path.suffix == '.pyc':
|
||||
continue
|
||||
if path.is_symlink() or not path.is_file() or not allowed(relative):
|
||||
raise DeploymentError('Unexpected source entry; refusing deployment: ' + str(relative))
|
||||
result[relative.as_posix()] = path
|
||||
required = {'scripts/src/aim/__init__.py', 'scripts/pyproject.toml', 'scripts/docs/AGENTS.md', 'scripts/docs/ADDON_AGENTS.md', 'playbooks/aim_catalog.yml'}
|
||||
if not required.issubset(result):
|
||||
raise DeploymentError('Not a complete AIM replacement source tree.')
|
||||
return result
|
||||
|
||||
|
||||
def digest(path: Path | bytes) -> str:
|
||||
if isinstance(path, bytes):
|
||||
return hashlib.sha256(path).hexdigest()
|
||||
with path.open('rb') as stream:
|
||||
return hashlib.file_digest(stream, 'sha256').hexdigest()
|
||||
|
||||
|
||||
LAUNCHER_MARKER = '# AIM core managed launcher v1'
|
||||
|
||||
|
||||
def _launcher_digest(path: Path) -> str | None:
|
||||
if path.is_symlink():
|
||||
return digest(('symlink:' + os.readlink(path)).encode('utf-8'))
|
||||
if path.exists():
|
||||
return digest(path)
|
||||
return None
|
||||
|
||||
|
||||
def _read_launcher_text(path: Path) -> str:
|
||||
candidate = path.resolve(strict=True) if path.is_symlink() else path
|
||||
if not candidate.is_file() or candidate.stat().st_size > 65536:
|
||||
raise DeploymentError('A non-launcher occupies ' + str(path))
|
||||
try:
|
||||
return candidate.read_text(encoding='utf-8')
|
||||
except UnicodeDecodeError:
|
||||
raise DeploymentError('Refusing to replace an unrecognized launcher: ' + str(path)) from None
|
||||
|
||||
|
||||
def _restore_symlink(destination: Path, target: str):
|
||||
no_symlink_parents(destination)
|
||||
temporary = destination.parent / ('.aim-link-' + next(tempfile._get_candidate_names()))
|
||||
try:
|
||||
os.symlink(target, temporary)
|
||||
os.replace(temporary, destination)
|
||||
dfd = os.open(destination.parent, os.O_RDONLY | os.O_DIRECTORY)
|
||||
try:
|
||||
os.fsync(dfd)
|
||||
finally:
|
||||
os.close(dfd)
|
||||
finally:
|
||||
temporary.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def _launcher_path(item, target: Path, launcher_dir: Path | None = None):
|
||||
relative = Path(item['path'])
|
||||
if item.get('kind') == 'launcher':
|
||||
if launcher_dir is None or relative.parts not in (('@launchers', 'aim'), ('@launchers', 'aimctl')):
|
||||
raise DeploymentError('Invalid launcher recovery path.')
|
||||
directory = canonical(launcher_dir)
|
||||
if not directory.is_absolute() or directory == Path('/'):
|
||||
raise DeploymentError('Invalid launcher directory.')
|
||||
destination = directory / relative.name
|
||||
else:
|
||||
if relative.is_absolute() or '..' in relative.parts or relative.parts[:1] == ('@launchers',):
|
||||
raise DeploymentError('Invalid core source path.')
|
||||
destination = target / relative
|
||||
if item.get('kind') == 'launcher':
|
||||
no_symlink_parents(destination)
|
||||
else:
|
||||
no_symlink(destination)
|
||||
return destination
|
||||
|
||||
|
||||
def _command(args, *, timeout=20):
|
||||
env = os.environ.copy()
|
||||
for name in ('PYTHONPATH', 'PYTHONHOME', 'PYTHONSTARTUP', 'PYTHONINSPECT'):
|
||||
env.pop(name, None)
|
||||
env['PYTHONDONTWRITEBYTECODE'] = '1'
|
||||
try:
|
||||
result = subprocess.run(args, stdin=subprocess.DEVNULL, capture_output=True,
|
||||
text=True, timeout=timeout, env=env, cwd='/')
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
raise DeploymentError('AIM interpreter/launcher check could not complete; no dependency installation is attempted.') from None
|
||||
if result.returncode:
|
||||
raise DeploymentError('AIM interpreter/launcher check failed. Supply the existing AIM environment with --aim-python; ensure its Python 3.11+, ruamel.yaml and rich dependencies and operator configuration are usable.')
|
||||
return result.stdout
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EntryPoints:
|
||||
python: Path
|
||||
directory: Path
|
||||
target: Path
|
||||
|
||||
def contents(self):
|
||||
result = {}
|
||||
for name, module in (('aim', 'aim.__main__'), ('aimctl', 'aim.ctl')):
|
||||
metadata = json.dumps({'target': str(self.target), 'python': str(self.python), 'command': name}, sort_keys=True)
|
||||
content = (f'#!{self.python}\n{LAUNCHER_MARKER}\n# {metadata}\n'
|
||||
'import sys\n'
|
||||
'sys.dont_write_bytecode = True\n'
|
||||
f'sys.path.insert(0, {str(self.target / "scripts/src")!r})\n'
|
||||
f'from {module} import main\n'
|
||||
'if __name__ == "__main__":\n raise SystemExit(main())\n')
|
||||
result['@launchers/' + name] = content.encode('utf-8')
|
||||
return result
|
||||
|
||||
def verify_python(self, source):
|
||||
# An explicit interpreter is an administrator-selected executable, not
|
||||
# user-controlled input to an elevated web wrapper. Preserve venv symlinks.
|
||||
expected = tomllib.loads((source / 'scripts/pyproject.toml').read_text())['project']['version']
|
||||
code = ('import sys,json; assert sys.version_info >= (3,11); '
|
||||
f'sys.path.insert(0, {str(source / "scripts/src")!r}); '
|
||||
'import ruamel.yaml,rich,aim; from aim.ui.app import App; '
|
||||
'from aim.services.v1 import AimService; from aim.ctl import main; '
|
||||
'print(json.dumps({"version":aim.__version__}))')
|
||||
value = json.loads(_command([str(self.python), '-I', '-B', '-c', code]))
|
||||
if value.get('version') != expected:
|
||||
raise DeploymentError('Extracted source/package versions disagree.')
|
||||
return expected
|
||||
|
||||
def verify_installed(self, version):
|
||||
got = _command([str(self.directory / 'aim'), '--version']).strip()
|
||||
value = json.loads(_command([str(self.directory / 'aimctl'), '--config',
|
||||
str(self.target / 'scripts/aim.yml'), 'capabilities']))
|
||||
if got != 'AIM ' + version or not value.get('ok') or value.get('result', {}).get('core_version') != version:
|
||||
raise DeploymentError('Installed AIM/aimctl launchers do not report the deployed core version.')
|
||||
print('PASS installed aim --version and aimctl capabilities (' + version + ').')
|
||||
|
||||
|
||||
def entry_points(target: Path, python: Path | None, directory: Path | None) -> EntryPoints:
|
||||
existing = shutil.which('aim')
|
||||
if target != Path('/etc/ansible') and directory is None:
|
||||
raise DeploymentError('A nondefault --target requires an explicit --bin-dir to avoid replacing another installation\'s commands.')
|
||||
if python is None:
|
||||
if not existing:
|
||||
raise DeploymentError('Cannot discover the AIM interpreter. Supply --aim-python /absolute/path/to/the/existing/AIM/bin/python.')
|
||||
with Path(existing).open(encoding='utf-8') as stream:
|
||||
first = stream.readline().strip()
|
||||
if not first.startswith('#!/') or len(first[2:].split()) != 1 or Path(first[2:]).name not in ('python', 'python3', 'python3.11', 'python3.12', 'python3.13', 'python3.14'):
|
||||
raise DeploymentError('The existing aim launcher has no unambiguous Python shebang. Supply --aim-python explicitly; shell/env wrappers are not guessed.')
|
||||
python = Path(first[2:])
|
||||
if not python.is_absolute() or not python.is_file() or not os.access(python, os.X_OK) or any(c.isspace() for c in str(python)) or len(str(python)) > 120:
|
||||
raise DeploymentError('--aim-python must be an executable absolute, space-free Python path (maximum 120 characters). Venv symlinks are supported.')
|
||||
python = Path(os.path.abspath(python)) # do NOT resolve a venv symlink to the system Python
|
||||
directory = directory if directory is not None else (Path(existing).parent if existing else Path('/usr/local/bin'))
|
||||
if not directory.is_absolute():
|
||||
raise DeploymentError('--bin-dir must be absolute.')
|
||||
directory = canonical(directory)
|
||||
if directory == Path('/') or directory == target or directory.is_relative_to(target / 'scripts/src'):
|
||||
raise DeploymentError('Use a dedicated command directory, not the root or core source namespace.')
|
||||
if directory.exists() and not directory.is_dir():
|
||||
raise DeploymentError('The command directory is not a directory.')
|
||||
for name, module in (('aim', 'aim.__main__'), ('aimctl', 'aim.ctl')):
|
||||
dest = directory / name
|
||||
no_symlink_parents(dest)
|
||||
if dest.exists() or dest.is_symlink():
|
||||
if dest.is_symlink():
|
||||
try:
|
||||
resolved = dest.resolve(strict=True)
|
||||
except (OSError, RuntimeError):
|
||||
raise DeploymentError('Refusing a broken launcher symlink: ' + str(dest)) from None
|
||||
if not resolved.is_file():
|
||||
raise DeploymentError('Launcher symlink does not resolve to a regular file: ' + str(dest))
|
||||
text = _read_launcher_text(dest)
|
||||
if LAUNCHER_MARKER in text:
|
||||
try:
|
||||
info = json.loads(text.splitlines()[2][2:])
|
||||
except (ValueError, IndexError):
|
||||
raise DeploymentError('Invalid AIM launcher metadata: ' + str(dest)) from None
|
||||
if info.get('target') != str(target):
|
||||
raise DeploymentError('AIM launcher belongs to another installation; choose its own --bin-dir.')
|
||||
elif f'from {module} import main' not in text:
|
||||
raise DeploymentError('Refusing to replace an unrecognized launcher. Choose a reviewed --bin-dir: ' + str(dest))
|
||||
return EntryPoints(python, directory, target)
|
||||
|
||||
|
||||
def plan(source, target, mode, *, entrypoints=None):
|
||||
source, target = canonical(source), canonical(target)
|
||||
no_symlink(source)
|
||||
no_symlink(target)
|
||||
if target == Path('/') or source == target or source.is_relative_to(target) or target.is_relative_to(source):
|
||||
raise DeploymentError('Use separate extracted-source and installation directories; never / as target.')
|
||||
if mode == 'update' and not (target / 'scripts/src/aim/__init__.py').is_file():
|
||||
raise DeploymentError('Existing AIM source was not found; use install for a fresh tree.')
|
||||
files = source_files(source)
|
||||
operations = []
|
||||
for relative, src in sorted(files.items()):
|
||||
dest = target / relative
|
||||
no_symlink(dest)
|
||||
if dest.exists() and not dest.is_file():
|
||||
raise DeploymentError('A non-file occupies a core destination: ' + relative)
|
||||
if relative == 'scripts/aim.yml' and dest.exists():
|
||||
continue # operator-owned, always preserved, even on first install
|
||||
if dest.exists() and digest(src) == digest(dest):
|
||||
continue
|
||||
operations.append({'path': relative, 'action': 'replace' if dest.exists() else 'create',
|
||||
'old_sha256': digest(dest) if dest.exists() else None,
|
||||
'new_sha256': digest(src)})
|
||||
# Only the Python core namespace is an authoritative replaceable directory.
|
||||
# Other unlisted playbooks/roles/files remain operator-owned additions.
|
||||
core = target / 'scripts/src/aim'
|
||||
if core.exists():
|
||||
for parent, dirs, names in os.walk(core, followlinks=False):
|
||||
for d in dirs:
|
||||
no_symlink(Path(parent) / d)
|
||||
for name in names:
|
||||
existing = Path(parent) / name
|
||||
no_symlink(existing)
|
||||
relative = existing.relative_to(target).as_posix()
|
||||
if relative not in files:
|
||||
if not existing.is_file():
|
||||
raise DeploymentError('Unsupported core namespace entry: ' + relative)
|
||||
operations.append({'path': relative, 'action': 'remove', 'old_sha256': digest(existing), 'new_sha256': None})
|
||||
# Explicitly retired Core document names only; unknown operator documents stay.
|
||||
# Removal is previewed and recorded in the same protected rollback journal.
|
||||
retired_docs = (
|
||||
# Root/scripts-root AIM-owned docs moved into scripts/docs.
|
||||
# Component-local docs (deploy/README.md, role READMEs, playbook docs) stay beside their code.
|
||||
'AGENTS.md', 'ADDON_AGENTS.md', 'scripts/CHANGELOG.md',
|
||||
'scripts/docs/RC19_HANDOFF.md', 'scripts/docs/SANITY_3.2.0.md',
|
||||
'scripts/docs/SANITY_3.2.1rc2.md', 'scripts/docs/VERIFICATION.md',
|
||||
'scripts/docs/LOCAL_VALIDATION.md', 'scripts/docs/CONTROLLER_ACCEPTANCE.md',
|
||||
)
|
||||
for relative in retired_docs:
|
||||
existing = target / relative
|
||||
no_symlink(existing)
|
||||
if existing.exists() and relative not in files:
|
||||
if not existing.is_file():
|
||||
raise DeploymentError('Non-file occupies a retired document: ' + relative)
|
||||
operations.append({'path': relative, 'action': 'remove',
|
||||
'old_sha256': digest(existing), 'new_sha256': None})
|
||||
if entrypoints is not None:
|
||||
if entrypoints.target != target or entrypoints.directory == source or entrypoints.directory.is_relative_to(source):
|
||||
raise DeploymentError('Launcher target/directory conflicts with the extracted source.')
|
||||
for relative, content in entrypoints.contents().items():
|
||||
dest = _launcher_path({'path': relative, 'kind': 'launcher'}, target, entrypoints.directory)
|
||||
files[relative] = content
|
||||
current = _launcher_digest(dest)
|
||||
if not dest.is_symlink() and dest.exists() and current == digest(content) and os.access(dest, os.X_OK):
|
||||
continue
|
||||
operations.append({'path': relative, 'kind': 'launcher', 'action': 'replace' if (dest.exists() or dest.is_symlink()) else 'create',
|
||||
'old_sha256': current, 'new_sha256': digest(content)})
|
||||
return files, operations
|
||||
|
||||
|
||||
def atomic_file(source: Path | bytes, destination: Path, *, metadata=None, executable=False, allow_replace_symlink=False):
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
if allow_replace_symlink:
|
||||
no_symlink_parents(destination)
|
||||
else:
|
||||
no_symlink(destination)
|
||||
fd, filename = tempfile.mkstemp(prefix='.aim-install-', dir=destination.parent)
|
||||
staged = Path(filename)
|
||||
try:
|
||||
with os.fdopen(fd, 'wb') as stream:
|
||||
if isinstance(source, bytes):
|
||||
stream.write(source)
|
||||
else:
|
||||
with source.open('rb') as incoming:
|
||||
shutil.copyfileobj(incoming, stream)
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
if metadata is None and destination.exists() and not destination.is_symlink():
|
||||
old = destination.stat()
|
||||
os.chown(staged, old.st_uid, old.st_gid)
|
||||
# Preserve ACLs/attributes, not the old file timestamp.
|
||||
for key in os.listxattr(destination):
|
||||
os.setxattr(staged, key, os.getxattr(destination, key))
|
||||
staged.chmod(stat.S_IMODE(old.st_mode) | (0o111 if executable else 0))
|
||||
elif metadata is not None:
|
||||
os.chown(staged, metadata['uid'], metadata['gid'])
|
||||
for key, value in metadata.get('xattrs', {}).items():
|
||||
os.setxattr(staged, key, base64.b64decode(value, validate=True))
|
||||
staged.chmod(metadata['mode'])
|
||||
else:
|
||||
staged.chmod(0o755 if executable else 0o644)
|
||||
os.replace(staged, destination)
|
||||
dfd = os.open(destination.parent, os.O_RDONLY | os.O_DIRECTORY)
|
||||
try:
|
||||
os.fsync(dfd)
|
||||
finally:
|
||||
os.close(dfd)
|
||||
finally:
|
||||
staged.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def apply(source, target, mode, backup_root, *, entrypoints=None):
|
||||
source, target, backup_root = canonical(source), canonical(target), canonical(backup_root)
|
||||
files, operations = plan(source, target, mode, entrypoints=entrypoints)
|
||||
version = entrypoints.verify_python(source) if entrypoints else None
|
||||
launcher_dir = entrypoints.directory if entrypoints else None
|
||||
if not operations:
|
||||
if entrypoints:
|
||||
entrypoints.verify_installed(version)
|
||||
print('AIM source and selected entry points are already current; operator configuration was preserved.')
|
||||
return None
|
||||
no_symlink(backup_root)
|
||||
if backup_root == target or backup_root.is_relative_to(target) or backup_root == source or backup_root.is_relative_to(source):
|
||||
raise DeploymentError('Backups must be outside the installation and extracted source trees.')
|
||||
backup_root.mkdir(parents=True, exist_ok=True)
|
||||
backup = Path(tempfile.mkdtemp(prefix=datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ-'), dir=backup_root))
|
||||
backup.chmod(0o700)
|
||||
record = {'target': str(target), 'state': 'preparing', 'entries': [], 'created_directories': [],
|
||||
'format': 2, 'launcher_dir': str(launcher_dir) if launcher_dir else None}
|
||||
for operation in operations:
|
||||
dest = _launcher_path(operation, target, launcher_dir)
|
||||
item = dict(operation)
|
||||
if dest.is_symlink():
|
||||
item['old_kind'] = 'symlink'
|
||||
item['link_target'] = os.readlink(dest)
|
||||
elif dest.exists():
|
||||
st = dest.stat()
|
||||
item['old_kind'] = 'file'
|
||||
item['metadata'] = dict(uid=st.st_uid, gid=st.st_gid, mode=stat.S_IMODE(st.st_mode),
|
||||
xattrs={key: base64.b64encode(os.getxattr(dest, key)).decode('ascii') for key in os.listxattr(dest)})
|
||||
recovery = backup / 'files' / operation['path']
|
||||
recovery.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(dest, recovery)
|
||||
recovery.chmod(0o600)
|
||||
record['entries'].append(item)
|
||||
def save_record():
|
||||
fd, temporary = tempfile.mkstemp(prefix='.recovery-', dir=backup)
|
||||
try:
|
||||
with os.fdopen(fd, 'w', encoding='utf-8') as stream:
|
||||
json.dump(record, stream, indent=2)
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(temporary, backup / 'recovery.json')
|
||||
dfd = os.open(backup, os.O_RDONLY | os.O_DIRECTORY)
|
||||
try:
|
||||
os.fsync(dfd)
|
||||
finally:
|
||||
os.close(dfd)
|
||||
finally:
|
||||
Path(temporary).unlink(missing_ok=True)
|
||||
save_record()
|
||||
try:
|
||||
record['state'] = 'applying'
|
||||
save_record()
|
||||
for item in record['entries']:
|
||||
dest = _launcher_path(item, target, launcher_dir)
|
||||
current = _launcher_digest(dest) if item.get('kind') == 'launcher' else (digest(dest) if dest.exists() else None)
|
||||
if current != item['old_sha256']:
|
||||
raise DeploymentError('Source changed during installation; stop writers and retry.')
|
||||
item['started'] = True
|
||||
save_record() # persist intent before changing any installed file
|
||||
if item['action'] == 'remove':
|
||||
dest.unlink()
|
||||
else:
|
||||
if digest(files[item['path']]) != item['new_sha256']:
|
||||
raise DeploymentError('Extracted release source changed during installation.')
|
||||
missing_dirs = []
|
||||
parent = dest.parent
|
||||
while not parent.exists():
|
||||
missing_dirs.append(str(parent))
|
||||
parent = parent.parent
|
||||
record['created_directories'].extend(missing_dirs)
|
||||
atomic_file(files[item['path']], dest, executable=item.get('kind') == 'launcher',
|
||||
allow_replace_symlink=item.get('kind') == 'launcher')
|
||||
item['applied'] = True
|
||||
save_record()
|
||||
if entrypoints:
|
||||
entrypoints.verify_installed(version)
|
||||
record['state'] = 'completed'
|
||||
save_record()
|
||||
print('AIM core source installed. Recovery directory: ' + str(backup))
|
||||
print('Preserved existing scripts/aim.yml, inventories, Vaults, keys, add-ons, virtual environments and unlisted customer files.')
|
||||
print('No dependencies, OS identities, permissions policy or services were provisioned.')
|
||||
if entrypoints:
|
||||
print('AIM and aimctl launchers: ' + str(entrypoints.directory))
|
||||
print('Ensure this directory is in the operator PATH; use hash -r in existing shells.')
|
||||
return backup
|
||||
except BaseException:
|
||||
# Ordinary failures can restore the source files already touched. A power
|
||||
# loss/kill -9 is not an atomic whole-tree transaction: retain recovery data.
|
||||
for item in reversed(record['entries']):
|
||||
if not item.get('started') or item['path'] == 'scripts/aim.yml':
|
||||
continue
|
||||
dest = _launcher_path(item, target, launcher_dir)
|
||||
now = _launcher_digest(dest) if item.get('kind') == 'launcher' else (digest(dest) if dest.exists() else None)
|
||||
if now == item['old_sha256'] and not (item.get('kind') == 'launcher' and dest.exists() and
|
||||
stat.S_IMODE(dest.stat().st_mode) != item.get('metadata', {}).get('mode')):
|
||||
continue
|
||||
if now != item['new_sha256']:
|
||||
raise DeploymentError('A concurrently modified file prevented rollback; use the protected recovery directory.')
|
||||
if item['old_sha256'] is None:
|
||||
dest.unlink(missing_ok=True)
|
||||
elif item.get('old_kind') == 'symlink':
|
||||
_restore_symlink(dest, item['link_target'])
|
||||
else:
|
||||
atomic_file(backup / 'files' / item['path'], dest, metadata=item['metadata'])
|
||||
record['state'] = 'rolled_back_after_error'
|
||||
save_record()
|
||||
raise
|
||||
|
||||
|
||||
def rollback(backup: Path, target: Path, *, execute: bool):
|
||||
backup, target = canonical(backup), canonical(target)
|
||||
path = backup / 'recovery.json'
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise DeploymentError('Recovery metadata is missing or unsafe.')
|
||||
record = json.loads(path.read_text(encoding='utf-8'))
|
||||
if record.get('target') != str(target) or record.get('state') not in ('completed', 'applying'):
|
||||
raise DeploymentError('Recovery target/state does not match this installation.')
|
||||
actions = []
|
||||
launcher_dir = Path(record['launcher_dir']) if record.get('launcher_dir') else None
|
||||
for item in record['entries']:
|
||||
if not (item.get('started') or item.get('applied')):
|
||||
continue
|
||||
relative = Path(item['path'])
|
||||
if relative.is_absolute() or '..' in relative.parts or relative.as_posix() == 'scripts/aim.yml':
|
||||
# Initial install may have created aim.yml: never delete an operator's
|
||||
# subsequent configuration through rollback. Always preserve this file.
|
||||
if relative.as_posix() == 'scripts/aim.yml':
|
||||
continue
|
||||
raise DeploymentError('Unsafe recovery path.')
|
||||
if item.get('kind') != 'launcher' and not allowed(relative) and relative.parts[:3] != ('scripts', 'src', 'aim'):
|
||||
raise DeploymentError('Recovery may only address the core source namespace.')
|
||||
dest = _launcher_path(item, target, launcher_dir)
|
||||
now = _launcher_digest(dest) if item.get('kind') == 'launcher' else (digest(dest) if dest.exists() else None)
|
||||
if now == item['old_sha256'] and not (item.get('kind') == 'launcher' and dest.exists() and
|
||||
stat.S_IMODE(dest.stat().st_mode) != item.get('metadata', {}).get('mode')):
|
||||
continue # interruption before publication, or an already restored entry
|
||||
if now != item['new_sha256']:
|
||||
raise DeploymentError('Installed core source changed since deployment; refusing to overwrite it during rollback: ' + str(relative))
|
||||
if item['old_sha256'] is not None and item.get('old_kind') != 'symlink':
|
||||
recovered = backup / 'files' / relative
|
||||
no_symlink(recovered)
|
||||
if digest(recovered) != item['old_sha256']:
|
||||
raise DeploymentError('Recovery file checksum mismatch.')
|
||||
actions.append(item)
|
||||
print('Rollback source files: ' + str(len(actions)))
|
||||
if not execute:
|
||||
print('Dry run only. Use --apply --quiesced to restore these source files.')
|
||||
return
|
||||
for item in reversed(actions):
|
||||
dest = _launcher_path(item, target, launcher_dir)
|
||||
if item['old_sha256'] is None:
|
||||
dest.unlink(missing_ok=True)
|
||||
elif item.get('old_kind') == 'symlink':
|
||||
_restore_symlink(dest, item['link_target'])
|
||||
else:
|
||||
atomic_file(backup / 'files' / item['path'], dest, metadata=item['metadata'])
|
||||
print('Core source and recorded launchers restored; no remote Ansible work was reversed. Use hash -r and verify aim/aimctl for the restored release.')
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('operation', choices=('install', 'update', 'rollback'))
|
||||
parser.add_argument('--target', type=Path, default=Path('/etc/ansible'))
|
||||
parser.add_argument('--backup-dir', type=Path, default=Path('/var/backups/aim-core'))
|
||||
parser.add_argument('--from-backup', type=Path)
|
||||
parser.add_argument('--aim-python', type=Path, help='Existing AIM interpreter; inferred only from an unambiguous installed aim Python shebang')
|
||||
parser.add_argument('--bin-dir', type=Path, help='Install aim and aimctl here; defaults to the existing aim command directory or /usr/local/bin')
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument('--apply', action='store_true', help='Apply the planned core-source replacement')
|
||||
group.add_argument('--dry-run', action='store_true', help='Preview only (the default)')
|
||||
parser.add_argument('--quiesced', action='store_true', help='Confirm CLI/add-on jobs and other source writers have been stopped')
|
||||
args = parser.parse_args(argv)
|
||||
try:
|
||||
if not args.target.is_absolute():
|
||||
raise DeploymentError('An absolute non-root installation directory is required.')
|
||||
args.target = canonical(args.target)
|
||||
if args.target == Path('/'):
|
||||
raise DeploymentError('An absolute non-root installation directory is required.')
|
||||
if args.apply and not args.quiesced:
|
||||
raise DeploymentError('Stop active CLI/add-on jobs and retry with --apply --quiesced.')
|
||||
if args.operation == 'rollback':
|
||||
if not args.from_backup:
|
||||
raise DeploymentError('rollback requires --from-backup.')
|
||||
if args.apply:
|
||||
with deployment_lock(args.target):
|
||||
rollback(args.from_backup, args.target, execute=True)
|
||||
else:
|
||||
rollback(args.from_backup, args.target, execute=False)
|
||||
return 0
|
||||
source = Path(__file__).absolute().parents[1]
|
||||
entrypoints = entry_points(args.target, args.aim_python, args.bin_dir)
|
||||
entrypoints.verify_python(source)
|
||||
_, operations = plan(source, args.target, args.operation, entrypoints=entrypoints)
|
||||
print('Target: ' + str(args.target))
|
||||
print('AIM interpreter: ' + str(entrypoints.python))
|
||||
print('Command directory: ' + str(entrypoints.directory))
|
||||
for operation in operations:
|
||||
print(operation['action'].upper() + ' ' + operation['path'])
|
||||
print('Planned file operations: ' + str(len(operations)))
|
||||
print('KEEP existing scripts/aim.yml and all unlisted runtime/add-on/customer files.')
|
||||
if not args.apply:
|
||||
print('Dry run only. Apply from this extracted archive with --apply --quiesced.')
|
||||
return 0
|
||||
args.target.mkdir(parents=True, exist_ok=True)
|
||||
with deployment_lock(args.target):
|
||||
apply(source, args.target, args.operation, args.backup_dir.absolute(), entrypoints=entrypoints)
|
||||
return 0
|
||||
except (DeploymentError, OSError, ValueError, KeyError, TypeError) as exc:
|
||||
print('AIM deployment stopped: ' + str(exc), file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,90 @@
|
||||
# AIM Changelog
|
||||
|
||||
## 2.1.x
|
||||
|
||||
### 2.1.2
|
||||
|
||||
- Removed the experimental ASCII shield.
|
||||
- Retained bitformer orange `#ff7a00` accent.
|
||||
- Added compact `bitformer · AIM · Ansible Inventory Manager` header.
|
||||
- Preserved `p / n` pagination convention.
|
||||
- No intended Ansible behavior changes.
|
||||
|
||||
### 2.1.1
|
||||
|
||||
- Changed the primary UI accent to bitformer orange.
|
||||
- Introduced temporary ASCII branding experiment.
|
||||
- Standardized pagination on `p / n`.
|
||||
|
||||
### 2.1.0
|
||||
|
||||
- Major operator-console UI/UX refactor.
|
||||
- Split presentation into focused UI modules.
|
||||
- Added customer overview/dashboard.
|
||||
- Unified selectors, review screens, result presentation, pagination
|
||||
and filtering.
|
||||
- Kept interactive commands live where password/editor interaction is
|
||||
required.
|
||||
|
||||
## 2.0.x
|
||||
|
||||
- Added domain WinRM GPO rollout.
|
||||
- Added malformed-YAML handling and multiple GPO/AD/GPP reliability
|
||||
hotfixes.
|
||||
- Corrected Windows local-account credential handling to be
|
||||
host-specific.
|
||||
- Corrected new-customer domain UPN defaults.
|
||||
- Final 2.0.7 GPP Scheduled Task XML fix removed the invalid inner
|
||||
`LogonType` element and added immediate registration
|
||||
execution/retries.
|
||||
|
||||
## 1.9.x
|
||||
|
||||
- Added semantic Git-style template/Vault validation.
|
||||
- Added non-destructive template consolidation.
|
||||
- Consolidated playbook categories and generic multi-select behavior.
|
||||
|
||||
## 1.8.x
|
||||
|
||||
- Added structured Vault template creation.
|
||||
- Added Windows credential models.
|
||||
- Added SSH-agent/private-key-passphrase integration.
|
||||
- Added direct multi-select workflows.
|
||||
|
||||
## 1.7.x
|
||||
|
||||
- Added customer domain/network defaults.
|
||||
- Added batch Windows member-server domain access.
|
||||
|
||||
## 1.6.x
|
||||
|
||||
- Added/expanded automated Sophos configuration.
|
||||
- Improved lock cleanup and Sophos menu organization.
|
||||
|
||||
## 1.5.x
|
||||
|
||||
- Added Sophos defaults, host variables and customer-specific playbook
|
||||
support.
|
||||
|
||||
## 1.4.x
|
||||
|
||||
- Split Windows domain-account and member-server access workflows.
|
||||
|
||||
## 1.3.x
|
||||
|
||||
- Added local/domain Windows account flows and safer credential
|
||||
transport.
|
||||
|
||||
## 1.2.x
|
||||
|
||||
- Added Windows WinRM service-account bootstrap and localized
|
||||
Administrators handling.
|
||||
|
||||
## 1.1.x
|
||||
|
||||
- Added curated playbooks, routine local-only inventory validation,
|
||||
empty default host-vars files and Enter=`0` navigation behavior.
|
||||
|
||||
## 1.0.0
|
||||
|
||||
- Initial consolidated Python AIM base.
|
||||
@@ -0,0 +1,67 @@
|
||||
# AIM Development Guide
|
||||
|
||||
## Project root
|
||||
|
||||
``` text
|
||||
/etc/ansible/scripts/
|
||||
├── pyproject.toml
|
||||
└── src/
|
||||
└── aim/
|
||||
```
|
||||
|
||||
The environment is installed editable:
|
||||
|
||||
``` bash
|
||||
/etc/ansible/.venv/bin/python -m pip install -e /etc/ansible/scripts
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
Backend areas include inventory loading/writing/validation, customer
|
||||
management, backup/session recovery, locking, permissions, playbooks,
|
||||
SSH, Vault, Sophos and WinRM.
|
||||
|
||||
The 2.1 UI is split into focused modules under `src/aim/ui/`, including
|
||||
shared components, selection, execution, customers, hosts, access,
|
||||
playbooks, administration and target selection.
|
||||
|
||||
## UI conventions
|
||||
|
||||
- Orange `#ff7a00` is the AIM/bitformer accent.
|
||||
- Green = success.
|
||||
- Yellow = warning.
|
||||
- Red = failure.
|
||||
- No ASCII logo.
|
||||
- Header identity: `bitformer · AIM · Ansible Inventory Manager`.
|
||||
- Pagination: `p / n`.
|
||||
- Numbered navigation: Enter/`0` = Back or Cancel.
|
||||
- Multi-select: number toggles; Enter reviews; `0` cancels.
|
||||
|
||||
Opening menus should not unexpectedly run Ansible, contact hosts,
|
||||
decrypt Vaults or prompt for passwords.
|
||||
|
||||
## Inventory invariants
|
||||
|
||||
`hosts.yml` is authoritative. Preserve arbitrary valid YAML/custom
|
||||
keys/comments where possible.
|
||||
|
||||
Writes should be validated and atomic, with stale-write/concurrency
|
||||
protection and a session recovery backup.
|
||||
|
||||
Do not change unrelated Ansible connection/authentication parameters as
|
||||
part of feature work.
|
||||
|
||||
## Packaging
|
||||
|
||||
Production packages should contain runtime source and metadata, without
|
||||
caches, `.pyc`, test artifacts, legacy Bash implementations or developer
|
||||
notes unless explicitly requested.
|
||||
|
||||
A release archive should expose `pyproject.toml` and `src/` at its root
|
||||
rather than adding an extra wrapper directory.
|
||||
|
||||
## Versioning
|
||||
|
||||
Use a patch release for contained fixes and a feature/minor release for
|
||||
larger functional changes. Update package metadata and the changelog
|
||||
together.
|
||||
@@ -0,0 +1,122 @@
|
||||
# AIM Inventory Model
|
||||
|
||||
## Source of truth
|
||||
|
||||
The authoritative inventory is:
|
||||
|
||||
``` text
|
||||
/etc/ansible/inventories/<customer>/hosts.yml
|
||||
```
|
||||
|
||||
AIM does not use `.hosts.tsv` as a secondary database and does not
|
||||
reverse-sync TSV data into YAML.
|
||||
|
||||
AIM uses round-trip YAML handling so valid manually maintained
|
||||
structures/comments can be preserved where possible.
|
||||
|
||||
## Platform groups
|
||||
|
||||
Default top-level platform groups:
|
||||
|
||||
``` text
|
||||
linux
|
||||
windows
|
||||
sophosxgs
|
||||
pfsense
|
||||
```
|
||||
|
||||
Platform remains top-level because it determines connection semantics
|
||||
such as SSH, WinRM or HTTPAPI.
|
||||
|
||||
Hosts may have multiple memberships and optional one-level functional
|
||||
subgroups.
|
||||
|
||||
## Linux
|
||||
|
||||
Linux group variables normally include the SSH connection and service
|
||||
account. The customer SSH key directory is:
|
||||
|
||||
``` text
|
||||
group_vars/linux/.ssh/
|
||||
```
|
||||
|
||||
not:
|
||||
|
||||
``` text
|
||||
group_vars/linux/files/.ssh/
|
||||
```
|
||||
|
||||
`ansible_ssh_pass` may remain configured as a legacy
|
||||
remote-login-password fallback. A private-key passphrase is a separate
|
||||
secret.
|
||||
|
||||
## Windows
|
||||
|
||||
Domain-joined Windows hosts normally inherit the group-level service
|
||||
identity/password.
|
||||
|
||||
A local-account host can override credentials in:
|
||||
|
||||
``` text
|
||||
host_vars/<fqdn>/main.yml
|
||||
```
|
||||
|
||||
Shared local example:
|
||||
|
||||
``` yaml
|
||||
ansible_user: svc_bf-ansible
|
||||
ansible_password: "{{ vault_windows_local_ansible_password }}"
|
||||
```
|
||||
|
||||
Host-specific example:
|
||||
|
||||
``` yaml
|
||||
ansible_user: svc_bf-ansible
|
||||
ansible_password: "{{ vault_ansible_password_server01_example_lan }}"
|
||||
```
|
||||
|
||||
## Host vars
|
||||
|
||||
Every newly managed host has:
|
||||
|
||||
``` text
|
||||
host_vars/<fqdn>/main.yml
|
||||
```
|
||||
|
||||
Existing host-vars content is not blindly overwritten.
|
||||
|
||||
## Customer defaults
|
||||
|
||||
AIM customer defaults live in:
|
||||
|
||||
``` text
|
||||
/etc/ansible/inventories/<customer>/.aim.yml
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
``` yaml
|
||||
domain_suffix: bfmiglabor.lan
|
||||
network_address: 10.20.30.0
|
||||
netmask: 255.255.255.0
|
||||
ad_dns_domain: intra.company.de
|
||||
ad_netbios_domain: COMPANY
|
||||
```
|
||||
|
||||
The AD DNS domain is used for service-account UPNs. NetBIOS remains
|
||||
metadata/legacy naming information.
|
||||
|
||||
## Safe writes
|
||||
|
||||
Inventory mutations use the conceptual sequence:
|
||||
|
||||
``` text
|
||||
candidate temp file
|
||||
→ local YAML validation
|
||||
→ compare
|
||||
→ session backup
|
||||
→ atomic replace
|
||||
```
|
||||
|
||||
AIM also protects against stale/concurrent writes and uses an inventory
|
||||
lock.
|
||||
@@ -0,0 +1,104 @@
|
||||
# AIM Operations Guide
|
||||
|
||||
## Navigation
|
||||
|
||||
AIM is organized around a customer context:
|
||||
|
||||
``` text
|
||||
Customer
|
||||
├── Hosts Management
|
||||
├── Access Management
|
||||
├── Vault Management
|
||||
├── Group Variables
|
||||
├── Host Variables
|
||||
├── Playbooks
|
||||
└── Administration
|
||||
```
|
||||
|
||||
For numbered navigation menus, Enter or `0` means Back/Cancel; at the
|
||||
main menu it means Exit. Single selectors use Enter/`0` to cancel.
|
||||
Multi-select uses numbers to toggle, Enter to review, and `0` to cancel.
|
||||
Paginated views use `p / n` for Previous / Next.
|
||||
|
||||
## Customer overview
|
||||
|
||||
Opening a customer should provide local information without unexpectedly
|
||||
contacting hosts, running Ansible or decrypting Vaults. The dashboard
|
||||
includes inventory YAML state, Vault presence, host/platform counts and
|
||||
available customer defaults.
|
||||
|
||||
## Hosts
|
||||
|
||||
`hosts.yml` is the source of truth.
|
||||
|
||||
Creating a host also ensures:
|
||||
|
||||
``` text
|
||||
host_vars/<fqdn>/main.yml
|
||||
```
|
||||
|
||||
For ordinary non-Sophos hosts this file may be empty. Existing host
|
||||
variable files are not overwritten. Removing a host also removes its
|
||||
corresponding host-vars directory.
|
||||
|
||||
Routine add/update/remove operations perform local YAML validation and
|
||||
do not request the Vault password.
|
||||
|
||||
## Access Management
|
||||
|
||||
Linux access manages SSH keys/service-user access.
|
||||
|
||||
Windows access includes temporary WinRM testing, local account creation,
|
||||
domain account creation/repair, member-server domain access, domain
|
||||
WinRM GPO rollout and configured-service-user testing.
|
||||
|
||||
See `WINDOWS.md` for the Windows model.
|
||||
|
||||
## Vault Management
|
||||
|
||||
Vault operations include information, create, edit and delete.
|
||||
|
||||
A new Vault is populated as plaintext with mode `0600`, YAML-validated,
|
||||
then encrypted using:
|
||||
|
||||
``` bash
|
||||
ansible-vault encrypt --vault-id <customer>@prompt vault.yml
|
||||
```
|
||||
|
||||
A failed encryption must not leave populated plaintext secrets behind.
|
||||
|
||||
## Variables
|
||||
|
||||
Group and host variable views are generally operator-readable without
|
||||
AIM rewriting arbitrary custom configuration. AIM only changes values in
|
||||
workflows explicitly designed to do so.
|
||||
|
||||
Template consolidation is explicit and non-destructive: missing AIM
|
||||
defaults/comments can be added, while existing non-empty values and
|
||||
custom keys are retained.
|
||||
|
||||
## Playbooks
|
||||
|
||||
Curated categories include CheckMK, Debug, Maintenance and Sophos XGS.
|
||||
Compatible host/group selection is used to build the Ansible `--limit`.
|
||||
|
||||
Interactive playbooks and operations that require password/Vault prompts
|
||||
retain live terminal access.
|
||||
|
||||
## Administration
|
||||
|
||||
Administration includes inventory validation, template consolidation,
|
||||
recovery and customer defaults.
|
||||
|
||||
Explicit inventory validation performs YAML parsing and
|
||||
`ansible-inventory`. When a customer Vault exists, validation uses the
|
||||
customer's Vault identity and may prompt for its password.
|
||||
|
||||
AIM maintains one pre-change inventory recovery backup per inventory per
|
||||
AIM process/session:
|
||||
|
||||
``` text
|
||||
hosts.aim-session.bak.yml
|
||||
```
|
||||
|
||||
Restores are explicit and YAML-validated.
|
||||
@@ -0,0 +1,69 @@
|
||||
# AIM Recovery Guide
|
||||
|
||||
## Principle
|
||||
|
||||
Git protects source code, not AIM's ignored runtime secrets.
|
||||
|
||||
A `git reset --hard` or fresh checkout cannot restore ignored data such
|
||||
as inventory Vaults, SSH keys or a Python virtual environment.
|
||||
|
||||
## Critical persistent data
|
||||
|
||||
Back up separately:
|
||||
|
||||
``` text
|
||||
/etc/ansible/inventories/<customer>/group_vars/all/vault.yml
|
||||
/etc/ansible/inventories/<customer>/group_vars/linux/.ssh/
|
||||
```
|
||||
|
||||
Other customer inventory YAML should also be covered by the system
|
||||
backup policy.
|
||||
|
||||
## Rebuildable data
|
||||
|
||||
Do not recover `.venv` from Git. Rebuild it:
|
||||
|
||||
``` bash
|
||||
sudo rm -rf /etc/ansible/.venv
|
||||
sudo python3 -m venv /etc/ansible/.venv
|
||||
sudo /etc/ansible/.venv/bin/python -m pip install --upgrade pip setuptools wheel
|
||||
sudo /etc/ansible/.venv/bin/python -m pip install -e /etc/ansible/scripts
|
||||
sudo ln -sfn /etc/ansible/.venv/bin/aim /usr/local/bin/aim
|
||||
```
|
||||
|
||||
## Recover Vaults and SSH keys from a filesystem backup
|
||||
|
||||
When restoring from a backup tree, copy only files that do not already
|
||||
exist in the active inventory. Never overwrite surviving current secrets
|
||||
as part of a bulk recovery.
|
||||
|
||||
See `../install.md` for the current dry-run and restore commands.
|
||||
|
||||
## AIM session inventory backup
|
||||
|
||||
AIM can create:
|
||||
|
||||
``` text
|
||||
hosts.aim-session.bak.yml
|
||||
```
|
||||
|
||||
This is a pre-change recovery aid, not the primary backup strategy for
|
||||
customer secrets.
|
||||
|
||||
Restore from it only through an explicit recovery decision after
|
||||
validating the relevant inventory state.
|
||||
|
||||
## Post-recovery validation
|
||||
|
||||
Before deleting the backup source:
|
||||
|
||||
1. Confirm Vault files exist for expected customers.
|
||||
2. Confirm SSH private/public key files and permissions.
|
||||
3. Test Vault decryption/access for representative customers.
|
||||
4. Test Linux SSH authentication.
|
||||
5. Test Windows WinRM for representative domain/local credential
|
||||
models.
|
||||
6. Test any other customer-specific access that depends on recovered
|
||||
secrets.
|
||||
|
||||
Keep the filesystem backup until these checks pass.
|
||||
@@ -0,0 +1,58 @@
|
||||
# AIM Sensitive Data and Security Notes
|
||||
|
||||
## Sensitive files
|
||||
|
||||
AIM deliberately keeps secrets outside Git.
|
||||
|
||||
Important locations include:
|
||||
|
||||
``` text
|
||||
/etc/ansible/inventories/<customer>/group_vars/all/vault.yml
|
||||
/etc/ansible/inventories/<customer>/group_vars/linux/.ssh/
|
||||
```
|
||||
|
||||
Vault files may contain Windows and Linux authentication material.
|
||||
`.ssh` directories may contain private keys that cannot be regenerated
|
||||
without coordinating key rotation on managed systems.
|
||||
|
||||
## Backup requirement
|
||||
|
||||
Git is not a backup for ignored secrets.
|
||||
|
||||
System backup procedures must include customer Vaults and SSH key
|
||||
material. Recovery should preserve existing current files and restore
|
||||
only missing data unless an operator explicitly chooses otherwise.
|
||||
|
||||
## Vault handling
|
||||
|
||||
AIM encrypts newly created Vaults with `ansible-vault`. Plaintext
|
||||
populated Vault data should not remain on disk after a failed encryption
|
||||
attempt.
|
||||
|
||||
Semantic Vault comparison must not print secret values. It should
|
||||
compare key existence/state rather than exposing plaintext.
|
||||
|
||||
## SSH key handling
|
||||
|
||||
Private-key passphrases and remote SSH passwords are separate concepts.
|
||||
|
||||
The Linux private key location is:
|
||||
|
||||
``` text
|
||||
group_vars/linux/.ssh/svc_bf-ansible
|
||||
```
|
||||
|
||||
Private keys should have restrictive filesystem permissions.
|
||||
|
||||
## Authorization
|
||||
|
||||
AIM authorization is based on membership in a configured group resolved
|
||||
through NSS/SSSD/winbind/local group services. The configured group name
|
||||
is authoritative; numeric GIDs may differ across hosts.
|
||||
|
||||
Operators should verify effective membership with:
|
||||
|
||||
``` bash
|
||||
getent group '<required-group>'
|
||||
id
|
||||
```
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
# Windows, WinRM and Active Directory
|
||||
|
||||
## Service account
|
||||
|
||||
The standard service account is:
|
||||
|
||||
``` text
|
||||
svc_bf-ansible
|
||||
```
|
||||
|
||||
For an AD domain, the configured identity is normally the UPN:
|
||||
|
||||
``` text
|
||||
svc_bf-ansible@<ad_dns_domain>
|
||||
```
|
||||
|
||||
The normal domain-account Vault variable is:
|
||||
|
||||
``` yaml
|
||||
vault_windows_ansible_password: "..."
|
||||
```
|
||||
|
||||
The shared-local-account Vault variable is:
|
||||
|
||||
``` yaml
|
||||
vault_windows_local_ansible_password: "..."
|
||||
```
|
||||
|
||||
## Windows credential models
|
||||
|
||||
New Windows hosts can use:
|
||||
|
||||
1. Domain service account
|
||||
2. Shared local service account
|
||||
3. Host-specific local service account
|
||||
|
||||
Local credential choices are host-specific overrides and must not
|
||||
rewrite `group_vars/windows/main.yml` for every Windows machine.
|
||||
|
||||
## Domain account creation
|
||||
|
||||
AIM can create/repair the domain service identity and add it to the
|
||||
appropriate built-in Administrators context used by the current design.
|
||||
It does not make the account a Domain Admin.
|
||||
|
||||
Administrative bootstrap credentials should only be requested where
|
||||
genuinely required.
|
||||
|
||||
## Member-server access
|
||||
|
||||
AIM can grant the existing domain service identity local Administrators
|
||||
membership on selected member servers. Domain Controllers are
|
||||
rejected/skipped for this workflow.
|
||||
|
||||
## Domain WinRM GPO rollout
|
||||
|
||||
The rollout uses one prepared, already-manageable Domain Controller as
|
||||
its administration point.
|
||||
|
||||
The managed objects are:
|
||||
|
||||
``` text
|
||||
AD group: GG_bitformer_Ansible_Admins
|
||||
GPO: bitformer - Ansible WinRM
|
||||
Task: bitformer - Configure Ansible WinRM
|
||||
```
|
||||
|
||||
The GPO configures the local Administrators membership, deploys the
|
||||
WinRM setup payload/scheduled task, configures HTTPS WinRM and firewall
|
||||
access, and verifies the resulting state.
|
||||
|
||||
### Multiple target OUs
|
||||
|
||||
A single GPO should be linked to multiple selected OUs rather than
|
||||
creating a separate GPO for Servers, Clients, etc.
|
||||
|
||||
Example:
|
||||
|
||||
``` text
|
||||
bitformer - Ansible WinRM
|
||||
├── OU=Servers,DC=intra,DC=company,DC=de
|
||||
└── OU=Clients,DC=intra,DC=company,DC=de
|
||||
```
|
||||
|
||||
The OU selector should therefore support multi-selection.
|
||||
|
||||
GPO link management is **additive and idempotent**:
|
||||
|
||||
- Selected OU already linked: keep/repair as appropriate.
|
||||
- Selected OU not linked: create the link.
|
||||
- Unselected OU: do nothing.
|
||||
|
||||
Selecting only `Servers` on a later run must **not** imply that an
|
||||
existing `Clients` link should be removed.
|
||||
|
||||
Link removal should be an explicit operation if/when AIM implements it.
|
||||
|
||||
### Child OUs
|
||||
|
||||
AIM links the GPO to the selected OU. It should not create redundant
|
||||
links on every descendant OU merely to emulate inheritance. Normal Group
|
||||
Policy inheritance handles descendants unless AD policy configuration
|
||||
changes that behavior.
|
||||
|
||||
### Domain Controllers
|
||||
|
||||
The Domain Controllers OU must remain unavailable/rejected for the
|
||||
normal member-machine WinRM rollout.
|
||||
|
||||
## WinRM payload
|
||||
|
||||
The payload ensures WinRM is running, configures/reuses a suitable
|
||||
certificate or creates a self-signed Server Authentication certificate,
|
||||
creates the HTTPS listener, allows TCP/5986 and verifies the final
|
||||
state.
|
||||
|
||||
The scheduled task runs immediately after registration and can retry
|
||||
periodically. After successful verification it disables itself.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Useful client-side checks include:
|
||||
|
||||
``` powershell
|
||||
gpupdate /force
|
||||
gpresult /h C:\Temp\gpresult.html
|
||||
Get-Service WinRM
|
||||
winrm enumerate winrm/config/listener
|
||||
Get-ScheduledTask -TaskName "bitformer - Configure Ansible WinRM"
|
||||
```
|
||||
|
||||
Also inspect Group Policy operational logs and Task Scheduler events
|
||||
when Group Policy Preferences reports a task import failure.
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
# AIM Installation Guide
|
||||
|
||||
This guide installs AIM with `/etc/ansible/scripts` as the project root.
|
||||
|
||||
## Layout
|
||||
|
||||
``` text
|
||||
/etc/ansible/scripts/
|
||||
├── pyproject.toml
|
||||
└── src/
|
||||
└── aim/
|
||||
|
||||
/etc/ansible/.venv/
|
||||
/usr/local/bin/aim -> /etc/ansible/.venv/bin/aim
|
||||
```
|
||||
|
||||
The virtual environment is intentionally not stored in Git.
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
On Debian/Ubuntu:
|
||||
|
||||
``` bash
|
||||
sudo apt update
|
||||
sudo apt install -y python3 python3-venv python3-pip
|
||||
```
|
||||
|
||||
## 2. Rebuild the virtual environment
|
||||
|
||||
``` bash
|
||||
sudo rm -rf /etc/ansible/.venv
|
||||
sudo python3 -m venv /etc/ansible/.venv
|
||||
sudo /etc/ansible/.venv/bin/python -m pip install --upgrade pip setuptools wheel
|
||||
```
|
||||
|
||||
## 3. Install AIM
|
||||
|
||||
``` bash
|
||||
sudo /etc/ansible/.venv/bin/python -m pip install -e /etc/ansible/scripts
|
||||
```
|
||||
|
||||
Verify:
|
||||
|
||||
``` bash
|
||||
ls -l /etc/ansible/.venv/bin/aim
|
||||
/etc/ansible/.venv/bin/python -m pip check
|
||||
```
|
||||
|
||||
## 4. Restore the system-wide command
|
||||
|
||||
``` bash
|
||||
sudo ln -sfn /etc/ansible/.venv/bin/aim /usr/local/bin/aim
|
||||
```
|
||||
|
||||
Verify:
|
||||
|
||||
``` bash
|
||||
ls -l /usr/local/bin/aim
|
||||
aim --version
|
||||
```
|
||||
|
||||
## 5. Authorization group / using another GID
|
||||
|
||||
AIM authorization is group-based rather than root-based. The default
|
||||
required group is:
|
||||
|
||||
``` text
|
||||
srv_debsansible01_admins@bitformer.lan
|
||||
```
|
||||
|
||||
AIM resolves the configured **group name** through the operating
|
||||
system's NSS layer (for example local groups, SSSD or winbind). The
|
||||
operator must have that group active as a primary or supplementary
|
||||
group.
|
||||
|
||||
Do not hard-code a numeric GID into AIM merely because a particular
|
||||
server uses a different GID. Numeric GIDs can differ between systems;
|
||||
configure the appropriate group name and let NSS resolve its GID.
|
||||
|
||||
Inspect the default group and its resolved GID:
|
||||
|
||||
``` bash
|
||||
getent group 'srv_debsansible01_admins@bitformer.lan'
|
||||
```
|
||||
|
||||
Check the current user's active groups/GIDs:
|
||||
|
||||
``` bash
|
||||
id
|
||||
id -G
|
||||
id -Gn
|
||||
```
|
||||
|
||||
If another authorization group should be used, change AIM's **Required
|
||||
Group** through:
|
||||
|
||||
``` text
|
||||
Global Config
|
||||
└── Required Group
|
||||
```
|
||||
|
||||
For example, if the local/AD-backed group is:
|
||||
|
||||
``` text
|
||||
ansible_operators
|
||||
```
|
||||
|
||||
verify it first:
|
||||
|
||||
``` bash
|
||||
getent group ansible_operators
|
||||
```
|
||||
|
||||
Then configure `ansible_operators` as AIM's Required Group. AIM will use
|
||||
the GID returned by NSS for that group.
|
||||
|
||||
After adding a user to a group, the login session may need to be renewed
|
||||
before the supplementary group becomes active. Verify with `id` before
|
||||
troubleshooting AIM authorization.
|
||||
|
||||
### Same group name, different GID
|
||||
|
||||
This is supported. For example, one server may resolve:
|
||||
|
||||
``` text
|
||||
ansible_operators:x:1200:...
|
||||
```
|
||||
|
||||
and another may resolve:
|
||||
|
||||
``` text
|
||||
ansible_operators:x:48001:...
|
||||
```
|
||||
|
||||
AIM should be configured with `ansible_operators`, not `1200` or
|
||||
`48001`.
|
||||
|
||||
### Different group name
|
||||
|
||||
Configure the alternative group name in AIM Global Config and confirm:
|
||||
|
||||
``` bash
|
||||
getent group '<group-name>'
|
||||
id
|
||||
```
|
||||
|
||||
If `getent` cannot resolve the group, fix NSS/SSSD/winbind/local group
|
||||
resolution first.
|
||||
|
||||
## 6. Git-ignored runtime data
|
||||
|
||||
Important ignored data includes:
|
||||
|
||||
``` gitignore
|
||||
.vscode/*
|
||||
.venv/*
|
||||
.ansible/*
|
||||
secure/*
|
||||
secure/keys/*
|
||||
vault.yml
|
||||
.ssh
|
||||
*.msi
|
||||
*.deb
|
||||
*.rpm
|
||||
**/.hosts.tsv
|
||||
**/hosts.yml.aim-session.bak
|
||||
**/hosts.aim-session.bak.yml
|
||||
*.bak
|
||||
```
|
||||
|
||||
For AIM inventory recovery, the critical persistent data is `vault.yml`
|
||||
and inventory `.ssh/` content. Do not restore `.venv`; rebuild it.
|
||||
|
||||
## 7. Recover missing Vaults and SSH keys
|
||||
|
||||
Example restored backup:
|
||||
|
||||
``` text
|
||||
/etc/ansible/inventories_RESTORED_20260915_152127
|
||||
```
|
||||
|
||||
Active inventories:
|
||||
|
||||
``` text
|
||||
/etc/ansible/inventories
|
||||
```
|
||||
|
||||
### Dry run
|
||||
|
||||
``` bash
|
||||
BACKUP="/etc/ansible/inventories_RESTORED_20260915_152127"
|
||||
TARGET="/etc/ansible/inventories"
|
||||
|
||||
echo "=== vault.yml ==="
|
||||
sudo find "$BACKUP" -type f -name 'vault.yml' -print0 |
|
||||
while IFS= read -r -d '' src; do
|
||||
rel="${src#"$BACKUP"/}"
|
||||
dst="$TARGET/$rel"
|
||||
if [ -e "$dst" ]; then
|
||||
echo "KEEP $dst"
|
||||
else
|
||||
echo "RESTORE $src -> $dst"
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
echo "=== .ssh files ==="
|
||||
sudo find "$BACKUP" -path '*/.ssh/*' -type f -print0 |
|
||||
while IFS= read -r -d '' src; do
|
||||
rel="${src#"$BACKUP"/}"
|
||||
dst="$TARGET/$rel"
|
||||
if [ -e "$dst" ]; then
|
||||
echo "KEEP $dst"
|
||||
else
|
||||
echo "RESTORE $src -> $dst"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
### Restore
|
||||
|
||||
``` bash
|
||||
BACKUP="/etc/ansible/inventories_RESTORED_20260915_152127"
|
||||
TARGET="/etc/ansible/inventories"
|
||||
|
||||
sudo find "$BACKUP" -type f -name 'vault.yml' -print0 |
|
||||
while IFS= read -r -d '' src; do
|
||||
rel="${src#"$BACKUP"/}"
|
||||
dst="$TARGET/$rel"
|
||||
if [ -e "$dst" ]; then
|
||||
echo "KEEP $dst"
|
||||
continue
|
||||
fi
|
||||
sudo mkdir -p "$(dirname "$dst")"
|
||||
sudo cp -a "$src" "$dst"
|
||||
echo "RESTORED $dst"
|
||||
done
|
||||
|
||||
sudo find "$BACKUP" -path '*/.ssh/*' -type f -print0 |
|
||||
while IFS= read -r -d '' src; do
|
||||
rel="${src#"$BACKUP"/}"
|
||||
dst="$TARGET/$rel"
|
||||
if [ -e "$dst" ]; then
|
||||
echo "KEEP $dst"
|
||||
continue
|
||||
fi
|
||||
sudo mkdir -p "$(dirname "$dst")"
|
||||
sudo cp -a "$src" "$dst"
|
||||
echo "RESTORED $dst"
|
||||
done
|
||||
```
|
||||
|
||||
`cp -a` preserves ownership, permissions and timestamps. Existing active
|
||||
files are never overwritten.
|
||||
|
||||
Verify:
|
||||
|
||||
``` bash
|
||||
sudo find /etc/ansible/inventories -type f -name 'vault.yml' -print | sort
|
||||
sudo find /etc/ansible/inventories -path '*/.ssh/*' -type f -print | sort
|
||||
sudo find /etc/ansible/inventories -path '*/.ssh/*' -type f -exec ls -l {} \;
|
||||
```
|
||||
|
||||
Keep the restored backup until Vault and SSH access have been tested.
|
||||
|
||||
## 8. Final verification
|
||||
|
||||
``` bash
|
||||
aim --version
|
||||
/etc/ansible/.venv/bin/python -m pip check
|
||||
aim
|
||||
```
|
||||
|
||||
## Updating AIM
|
||||
|
||||
AIM is installed editable from `/etc/ansible/scripts`.
|
||||
|
||||
If its entry point needs recreation:
|
||||
|
||||
``` bash
|
||||
sudo /etc/ansible/.venv/bin/python -m pip install -e /etc/ansible/scripts
|
||||
sudo ln -sfn /etc/ansible/.venv/bin/aim /usr/local/bin/aim
|
||||
```
|
||||
|
||||
Customer data under `/etc/ansible/inventories` is separate from the AIM
|
||||
source installation.
|
||||
@@ -0,0 +1,5 @@
|
||||
domain_suffix: desq-gaming.lan
|
||||
network_address: 192.168.20.0
|
||||
netmask: 255.255.255.0
|
||||
ad_dns_domain: ''
|
||||
ad_netbios_domain: ''
|
||||
@@ -0,0 +1 @@
|
||||
# group_vars/all/main.yml for desq_gaming
|
||||
@@ -0,0 +1,8 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABDCE2LLoV
|
||||
73yy1kzqzlRMRAAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIFdQAWVjarXDVETr
|
||||
Lk4DepHUkaDNVJl41IZuCUCDKLM9AAAAoJwWeclw1w1YC5uWBbb1JaMH2q9fa1YDSvg4Gs
|
||||
bNuZM8UEmh2pYkOBBPmL3mbTkcq2igF5IbJarhTzfebKCj9hMI3tXPyK9c6torPwA5uOiy
|
||||
NuQ1jUcAuAJ+wN9jzKwYpE54GaOAJiGEVippJREkF1X49iGwtB51zWJ9qFXotJmHOO55ap
|
||||
cjwWPtAwuqHIO9b2gfikiFlF4IJqKHFBz7m/Q=
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFdQAWVjarXDVETrLk4DepHUkaDNVJl41IZuCUCDKLM9 svc_bf-ansible@desq_gaming
|
||||
@@ -0,0 +1,7 @@
|
||||
# Linux / SSH variables
|
||||
ansible_connection: ssh
|
||||
ansible_user: svc_bf-ansible
|
||||
ansible_private_key_file:
|
||||
/etc/ansible/inventories/desq_gaming/group_vars/linux/.ssh/svc_bf-ansible
|
||||
ansible_ssh_pass: '{{ ansible_vault_linux_ssh_pass }}'
|
||||
ansible_become_method: sudo
|
||||
@@ -0,0 +1,2 @@
|
||||
# pfSense-specific variables
|
||||
{}
|
||||
@@ -0,0 +1,7 @@
|
||||
# SophosXGS-specific variables
|
||||
#ansible_password: "{{ ansible_password }}" # Passwort wird aus --ask-pass übernommen
|
||||
ansible_user: admin
|
||||
ansible_connection: ansible.netcommon.httpapi
|
||||
ansible_httpapi_validate_certs: false
|
||||
ansible_httpapi_port: 4444
|
||||
ansible_network_os: sophos.sophos_firewall.sfos
|
||||
@@ -0,0 +1,7 @@
|
||||
# Windows / WinRM variables
|
||||
ansible_connection: winrm
|
||||
ansible_port: 5986
|
||||
ansible_winrm_transport: ntlm
|
||||
ansible_winrm_server_cert_validation: ignore
|
||||
ansible_user: svc_bf-ansible
|
||||
ansible_password: '{{ vault_windows_ansible_password }}'
|
||||
@@ -0,0 +1,3 @@
|
||||
# AIM-managed host-specific Windows local service account credentials.
|
||||
ansible_user: svc_bf-ansible
|
||||
ansible_password: '{{ vault_ansible_password_desktop_robert_desq_gaming_lan }}'
|
||||
@@ -0,0 +1,99 @@
|
||||
all:
|
||||
children:
|
||||
desq_gaming:
|
||||
children:
|
||||
linux:
|
||||
children:
|
||||
networking:
|
||||
hosts:
|
||||
dewenpm01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.3
|
||||
dewedns02.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.2
|
||||
dewesrv-unifi02.desq-gaming.lan:
|
||||
ansible_host: 192.168.99.5
|
||||
|
||||
backup:
|
||||
hosts:
|
||||
dewepbs01.desq-gaming.lan:
|
||||
ansible_host: 192.168.99.32
|
||||
|
||||
proxmox:
|
||||
hosts:
|
||||
dewepbs01.desq-gaming.lan:
|
||||
ansible_host: 192.168.99.32
|
||||
dewepve01.desq-gaming.lan:
|
||||
ansible_host: 192.168.99.31
|
||||
|
||||
hosting:
|
||||
hosts:
|
||||
dewepve01.desq-gaming.lan:
|
||||
ansible_host: 192.168.99.31
|
||||
|
||||
management:
|
||||
hosts:
|
||||
dewesrv-ansible01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.46
|
||||
dewesrv-patch01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.45
|
||||
|
||||
applications:
|
||||
hosts:
|
||||
dewesrv-budget02.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.44
|
||||
dewesrv-cache02.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.24
|
||||
dewesrv-cloud01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.14
|
||||
dewesrv-crafty02.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.19
|
||||
dewesrv-db01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.21
|
||||
dewesrv-db02.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.23
|
||||
dewesrv-docker02.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.30
|
||||
dewesrv-git01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.38
|
||||
dewesrv-grafana01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.22
|
||||
dewesrv-ha01.desq-gaming.lan:
|
||||
ansible_host: 192.168.30.10
|
||||
dewesrv-homarr01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.35
|
||||
dewesrv-mail01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.40
|
||||
dewesrv-nodejs01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.20
|
||||
dewesrv-omv01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.15
|
||||
dewesrv-overseerr01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.18
|
||||
dewesrv-plex02.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.39
|
||||
dewesrv-puppet01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.25
|
||||
dewesrv-recipe01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.37
|
||||
dewesrv-rust02.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.27
|
||||
dewesrv-speed01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.16
|
||||
dewesrv-steam01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.12
|
||||
dewesrv-support01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.28
|
||||
dewesrv-tautulli01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.17
|
||||
dewesrv-tv01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.43
|
||||
dewesrv-uptime01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.11
|
||||
dewesrv-vault01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.34
|
||||
dewesrv-wallos01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.29
|
||||
dewesrv-wazuh01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.13
|
||||
dewesrv-wiki01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.36
|
||||
@@ -0,0 +1,105 @@
|
||||
all:
|
||||
children:
|
||||
desq_gaming:
|
||||
children:
|
||||
linux:
|
||||
children:
|
||||
networking:
|
||||
hosts:
|
||||
dewenpm01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.3
|
||||
dewedns02.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.2
|
||||
dewesrv-unifi02.desq-gaming.lan:
|
||||
ansible_host: 192.168.99.5
|
||||
|
||||
backup:
|
||||
hosts:
|
||||
dewepbs01.desq-gaming.lan:
|
||||
ansible_host: 192.168.99.32
|
||||
|
||||
proxmox:
|
||||
hosts:
|
||||
dewepbs01.desq-gaming.lan:
|
||||
ansible_host: 192.168.99.32
|
||||
dewepve01.desq-gaming.lan:
|
||||
ansible_host: 192.168.99.31
|
||||
|
||||
hosting:
|
||||
hosts:
|
||||
dewepve01.desq-gaming.lan:
|
||||
ansible_host: 192.168.99.31
|
||||
|
||||
management:
|
||||
hosts:
|
||||
dewesrv-ansible01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.46
|
||||
dewesrv-patch01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.45
|
||||
|
||||
applications:
|
||||
hosts:
|
||||
dewesrv-budget02.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.44
|
||||
dewesrv-cache02.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.24
|
||||
dewesrv-cloud01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.14
|
||||
dewesrv-crafty02.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.19
|
||||
dewesrv-db01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.21
|
||||
dewesrv-db02.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.23
|
||||
dewesrv-docker02.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.30
|
||||
dewesrv-git01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.38
|
||||
dewesrv-grafana01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.22
|
||||
dewesrv-ha01.desq-gaming.lan:
|
||||
ansible_host: 192.168.30.10
|
||||
dewesrv-homarr01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.35
|
||||
dewesrv-mail01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.40
|
||||
dewesrv-nodejs01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.20
|
||||
dewesrv-omv01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.15
|
||||
dewesrv-overseerr01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.18
|
||||
dewesrv-plex02.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.39
|
||||
dewesrv-puppet01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.25
|
||||
dewesrv-recipe01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.37
|
||||
dewesrv-rust02.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.27
|
||||
dewesrv-speed01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.16
|
||||
dewesrv-steam01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.12
|
||||
dewesrv-support01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.28
|
||||
dewesrv-tautulli01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.17
|
||||
dewesrv-tv01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.43
|
||||
dewesrv-uptime01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.11
|
||||
dewesrv-vault01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.34
|
||||
dewesrv-wallos01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.29
|
||||
dewesrv-wazuh01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.13
|
||||
dewesrv-wiki01.desq-gaming.lan:
|
||||
ansible_host: 192.168.20.36
|
||||
windows:
|
||||
children:
|
||||
client:
|
||||
hosts:
|
||||
DESKTOP-ROBERT.desq-gaming.lan:
|
||||
ansible_host: 192.168.10.11
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,75 @@
|
||||
---
|
||||
# PURPOSE: Preview / clean up Checkmk scripts
|
||||
# DESCRIPTION: List obsolete managed script paths; remove them only with explicit deletion approval.
|
||||
# TARGETS: linux, windows
|
||||
# INPUTS (omitted values inherit inventory / role defaults):
|
||||
# aim_debug [bool]: false
|
||||
# checkmk_cleanup_enabled [bool]: false
|
||||
# checkmk_unifi_mode [choice]: auto
|
||||
# want_linux_check_certificate [bool]: false
|
||||
# want_windows_citrix [bool]: false
|
||||
# want_windows_surebackup [bool]: false
|
||||
# want_windows_backup [bool]: false
|
||||
# want_windows_nsp_mailqueue [bool]: false
|
||||
# want_windows_certificate [bool]: false
|
||||
# want_windows_veeam_cloud_connect [bool]: false
|
||||
# want_windows_veeam_backup [bool]: false
|
||||
# AUTH: existing inventory / Vault credentials; no embedded passwords.
|
||||
# CHANGES: Cleanup only touches the documented managed filenames. Preview is the default; enabling deletion requires another confirmation.
|
||||
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
|
||||
# playbooks/checkmk_cleanup_scripts.yml --limit <host> --vault-id <customer>@prompt
|
||||
- name: Checkmk | Preview or clean up linux
|
||||
hosts: linux
|
||||
gather_facts: true
|
||||
pre_tasks:
|
||||
- name: AIM | Validate diagnostics option
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
|
||||
'false']
|
||||
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||
quiet: true
|
||||
- name: AIM | Reject mixed platform membership
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
|
||||
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
|
||||
quiet: true
|
||||
- name: AIM | Execution context
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
host: '{{ inventory_hostname }}'
|
||||
diagnostics: Enabled; secret values are never included by this task.
|
||||
when: aim_debug | default(false) | bool
|
||||
become: true
|
||||
roles:
|
||||
- role: system_detect_roles
|
||||
- role: checkmk_script_plan
|
||||
- role: checkmk_cleanup_scripts
|
||||
- name: Checkmk | Preview or clean up windows
|
||||
hosts: windows
|
||||
gather_facts: true
|
||||
pre_tasks:
|
||||
- name: AIM | Validate diagnostics option
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
|
||||
'false']
|
||||
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||
quiet: true
|
||||
- name: AIM | Reject mixed platform membership
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
|
||||
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
|
||||
quiet: true
|
||||
- name: AIM | Execution context
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
host: '{{ inventory_hostname }}'
|
||||
diagnostics: Enabled; secret values are never included by this task.
|
||||
when: aim_debug | default(false) | bool
|
||||
roles:
|
||||
- role: system_detect_roles
|
||||
- role: checkmk_script_plan
|
||||
- role: checkmk_cleanup_scripts
|
||||
@@ -0,0 +1,136 @@
|
||||
# PURPOSE: Install Checkmk agent
|
||||
# DESCRIPTION: Install staged agent packages, deploy selected checks, render Windows settings and ensure the agent is running.
|
||||
# TARGETS: linux, windows
|
||||
# INPUTS (omitted values inherit inventory / role defaults):
|
||||
# aim_debug [bool]: false
|
||||
# checkmk_unifi_mode [choice]: auto
|
||||
# checkmk_unifi_username [text]: bf-monitoring
|
||||
# checkmk_unifi_password [secret_ref]: vault_checkmk_unifi_password
|
||||
# checkmk_unifi_baseurl [url]: network: https://127.0.0.1:8443; os: https://127.0.0.1:11443
|
||||
# checkmk_unifi_curl_options [text]: --insecure --tlsv1.2
|
||||
# want_linux_check_certificate [bool]: false
|
||||
# want_windows_citrix [bool]: false
|
||||
# want_windows_surebackup [bool]: false
|
||||
# want_windows_backup [bool]: false
|
||||
# want_windows_nsp_mailqueue [bool]: false
|
||||
# want_windows_certificate [bool]: false
|
||||
# want_windows_veeam_cloud_connect [bool]: false
|
||||
# want_windows_veeam_backup [bool]: false
|
||||
# checkmk_unifi_status_provisioning [int]: 1
|
||||
# checkmk_unifi_status_upgrading [int]: 1
|
||||
# checkmk_unifi_status_upgradable [int]: 0
|
||||
# checkmk_unifi_status_heartbeat_missed [int]: 1
|
||||
# checkmk_unifi_status_noautobackup [int]: 0
|
||||
# checkmk_windows_updates_timeout [int]: 3600
|
||||
# checkmk_windows_updates_cache [int]: 43200
|
||||
# checkmk_mk_inventory_timeout [int]: 120
|
||||
# checkmk_plugins_default_timeout [int]: 120
|
||||
# checkmk_plugins_default_cache [int]: 600
|
||||
# checkmk_extra_plugin_patterns [sequence]: []
|
||||
# AUTH: existing inventory / Vault credentials; no embedded passwords.
|
||||
# CHANGES: Installs packages and AIM-managed script files. On Windows only the marked plugins section in check_mk.user.yml is replaced; all other user-config sections are preserved. UniFi deployment also removes the alternative UniFi local check; no other cleanup is performed.
|
||||
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
|
||||
# playbooks/checkmk_install_agent.yml --limit <host> --vault-id <customer>@prompt
|
||||
- name: Checkmk | Install linux
|
||||
hosts: linux
|
||||
gather_facts: true
|
||||
pre_tasks:
|
||||
- name: AIM | Validate diagnostics option
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in
|
||||
['true', 'false']
|
||||
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||
quiet: true
|
||||
- name: AIM | Reject mixed platform membership
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
|
||||
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
|
||||
quiet: true
|
||||
- name: AIM | Execution context
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
host: '{{ inventory_hostname }}'
|
||||
diagnostics: Enabled; secret values are never included by this task.
|
||||
when: aim_debug | default(false) | bool
|
||||
- name: Load system detect roles
|
||||
ansible.builtin.include_role:
|
||||
name: system_detect_roles
|
||||
- name: Load checkmk script plan
|
||||
ansible.builtin.include_role:
|
||||
name: checkmk_script_plan
|
||||
- name: Checkmk | Preflight scripts and credentials
|
||||
ansible.builtin.include_role:
|
||||
name: checkmk_deploy_scripts
|
||||
tasks_from: preflight
|
||||
become: true
|
||||
roles:
|
||||
- role: checkmk_agent
|
||||
- role: checkmk_deploy_scripts
|
||||
- role: checkmk_configure_agent
|
||||
- role: checkmk_manage_service
|
||||
post_tasks:
|
||||
- name: Checkmk | Observe installed agent
|
||||
ansible.builtin.include_role:
|
||||
name: checkmk_report
|
||||
- name: AIM | Publish operation result
|
||||
ansible.builtin.set_stats:
|
||||
per_host: true
|
||||
aggregate: false
|
||||
data:
|
||||
aim_output:
|
||||
protocol: aim_output_v1
|
||||
schema: checkmk_agent_state_v1
|
||||
data: '{{ _aim_checkmk_state }}'
|
||||
- name: Checkmk | Install windows
|
||||
hosts: windows
|
||||
gather_facts: true
|
||||
pre_tasks:
|
||||
- name: AIM | Validate diagnostics option
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in
|
||||
['true', 'false']
|
||||
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||
quiet: true
|
||||
- name: AIM | Reject mixed platform membership
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
|
||||
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
|
||||
quiet: true
|
||||
- name: AIM | Execution context
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
host: '{{ inventory_hostname }}'
|
||||
diagnostics: Enabled; secret values are never included by this task.
|
||||
when: aim_debug | default(false) | bool
|
||||
- name: Load system detect roles
|
||||
ansible.builtin.include_role:
|
||||
name: system_detect_roles
|
||||
- name: Load checkmk script plan
|
||||
ansible.builtin.include_role:
|
||||
name: checkmk_script_plan
|
||||
- name: Checkmk | Preflight scripts and credentials
|
||||
ansible.builtin.include_role:
|
||||
name: checkmk_deploy_scripts
|
||||
tasks_from: preflight
|
||||
roles:
|
||||
- role: checkmk_agent
|
||||
- role: checkmk_deploy_scripts
|
||||
- role: checkmk_configure_agent
|
||||
- role: checkmk_manage_service
|
||||
post_tasks:
|
||||
- name: Checkmk | Observe installed agent
|
||||
ansible.builtin.include_role:
|
||||
name: checkmk_report
|
||||
- name: AIM | Publish operation result
|
||||
ansible.builtin.set_stats:
|
||||
per_host: true
|
||||
aggregate: false
|
||||
data:
|
||||
aim_output:
|
||||
protocol: aim_output_v1
|
||||
schema: checkmk_agent_state_v1
|
||||
data: '{{ _aim_checkmk_state }}'
|
||||
@@ -0,0 +1,115 @@
|
||||
# PURPOSE: Read current Windows Checkmk user configuration
|
||||
# DESCRIPTION: Display the current check_mk.user.yml from Windows without modifying the host.
|
||||
# TARGETS: windows
|
||||
# INPUTS (omitted values inherit inventory / playbook defaults):
|
||||
# aim_debug [bool]: false
|
||||
# checkmk_windows_user_cfg [text]: C:\ProgramData\checkmk\agent\check_mk.user.yml
|
||||
# AUTH: existing inventory / Vault credentials; no embedded passwords.
|
||||
# CHANGES: none; this playbook is read-only.
|
||||
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
|
||||
# playbooks/checkmk_read_windows_config.yml --limit <host> --vault-id <customer>@prompt
|
||||
|
||||
- name: Checkmk | Read Windows user configuration
|
||||
hosts: windows
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: AIM | Validate diagnostics option
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in
|
||||
['true', 'false']
|
||||
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||
quiet: true
|
||||
|
||||
- name: AIM | Reject mixed platform membership
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
|
||||
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
|
||||
quiet: true
|
||||
|
||||
- name: AIM | Validate Checkmk configuration path
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (checkmk_windows_user_cfg | default('C:\\ProgramData\\checkmk\\agent\\check_mk.user.yml')) is
|
||||
string
|
||||
- (checkmk_windows_user_cfg | default('C:\\ProgramData\\checkmk\\agent\\check_mk.user.yml') | trim
|
||||
| length) > 0
|
||||
fail_msg: checkmk_windows_user_cfg must be a non-empty Windows path.
|
||||
quiet: true
|
||||
|
||||
- name: AIM | Execution context
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
host: '{{ inventory_hostname }}'
|
||||
configuration: "{{ checkmk_windows_user_cfg | default('C:\\\\ProgramData\\\\checkmk\\\\agent\\\\check_mk.user.yml')
|
||||
}}"
|
||||
mode: Read-only; no Checkmk configuration is modified.
|
||||
when: aim_debug | default(false) | bool
|
||||
|
||||
- name: Windows | Inspect current Checkmk user configuration
|
||||
ansible.windows.win_stat:
|
||||
path: "{{ checkmk_windows_user_cfg | default('C:\\ProgramData\\checkmk\\agent\\check_mk.user.yml') }}"
|
||||
get_checksum: false
|
||||
register: _checkmk_windows_user_config_stat
|
||||
changed_when: false
|
||||
|
||||
- name: Windows | Require the approved Checkmk user filename
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- not (_checkmk_windows_user_config_stat.stat.exists | default(false)) or (_checkmk_windows_user_config_stat.stat.filename | lower) == 'check_mk.user.yml'
|
||||
- not (_checkmk_windows_user_config_stat.stat.exists | default(false)) or (_checkmk_windows_user_config_stat.stat.isreg | default(false))
|
||||
- (_checkmk_windows_user_config_stat.stat.size | default(0) | int) <= 524288
|
||||
fail_msg: Only a regular check_mk.user.yml file up to 512 KiB may be read.
|
||||
quiet: true
|
||||
|
||||
- name: Windows | Read current Checkmk user configuration
|
||||
ansible.windows.slurp:
|
||||
src: "{{ checkmk_windows_user_cfg | default('C:\\ProgramData\\checkmk\\agent\\check_mk.user.yml') }}"
|
||||
register: _checkmk_windows_user_config_slurp
|
||||
when: _checkmk_windows_user_config_stat.stat.exists | default(false)
|
||||
no_log: true
|
||||
|
||||
- name: Windows | Build Checkmk user configuration result
|
||||
ansible.builtin.set_fact:
|
||||
_checkmk_windows_user_config:
|
||||
exists: '{{ _checkmk_windows_user_config_stat.stat.exists | default(false) | bool }}'
|
||||
path: "{{ _checkmk_windows_user_config_stat.stat.path | default(checkmk_windows_user_cfg | default('C:\\ProgramData\\checkmk\\agent\\check_mk.user.yml')) }}"
|
||||
size_bytes: '{{ _checkmk_windows_user_config_stat.stat.size | default(0) | int }}'
|
||||
last_write_time_utc: >-
|
||||
{{ (_checkmk_windows_user_config_stat.stat.lastwritetime | aim_epoch_iso_utc)
|
||||
if (_checkmk_windows_user_config_stat.stat.exists | default(false)) else none }}
|
||||
content: >-
|
||||
{{ (_checkmk_windows_user_config_slurp.content | b64decode)
|
||||
if (_checkmk_windows_user_config_stat.stat.exists | default(false)) else '' }}
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Windows | Report missing Checkmk user configuration
|
||||
ansible.builtin.debug:
|
||||
msg: |-
|
||||
{{ inventory_hostname }}
|
||||
Checkmk user configuration was not found.
|
||||
Path: {{ _checkmk_windows_user_config.path }}
|
||||
when: not (_checkmk_windows_user_config.exists | bool)
|
||||
|
||||
- name: Windows | Print current Checkmk user configuration
|
||||
ansible.builtin.debug:
|
||||
msg: |-
|
||||
{{ inventory_hostname }}
|
||||
Path: {{ _checkmk_windows_user_config.path }}
|
||||
Size: {{ _checkmk_windows_user_config.size_bytes }} bytes
|
||||
Last write (UTC): {{ _checkmk_windows_user_config.last_write_time_utc }}
|
||||
----- BEGIN check_mk.user.yml -----
|
||||
{{ _checkmk_windows_user_config.content }}
|
||||
----- END check_mk.user.yml -----
|
||||
when: _checkmk_windows_user_config.exists | bool
|
||||
- name: AIM | Publish operation result
|
||||
ansible.builtin.set_stats:
|
||||
per_host: true
|
||||
aggregate: false
|
||||
data:
|
||||
aim_output:
|
||||
protocol: aim_output_v1
|
||||
schema: checkmk_user_config_v1
|
||||
data: '{{ _checkmk_windows_user_config | aim_report_checkmk_config }}'
|
||||
@@ -0,0 +1,135 @@
|
||||
# PURPOSE: Update Checkmk scripts and configuration
|
||||
# DESCRIPTION: Deploy selected checks and Windows configuration without installing agent packages.
|
||||
# TARGETS: linux, windows
|
||||
# INPUTS (omitted values inherit inventory / role defaults):
|
||||
# aim_debug [bool]: false
|
||||
# checkmk_unifi_mode [choice]: auto
|
||||
# checkmk_unifi_username [text]: bf-monitoring
|
||||
# checkmk_unifi_password [secret_ref]: vault_checkmk_unifi_password
|
||||
# checkmk_unifi_baseurl [url]: network: https://127.0.0.1:8443; os: https://127.0.0.1:11443
|
||||
# checkmk_unifi_curl_options [text]: --insecure --tlsv1.2
|
||||
# want_linux_check_certificate [bool]: false
|
||||
# want_windows_citrix [bool]: false
|
||||
# want_windows_surebackup [bool]: false
|
||||
# want_windows_backup [bool]: false
|
||||
# want_windows_nsp_mailqueue [bool]: false
|
||||
# want_windows_certificate [bool]: false
|
||||
# want_windows_veeam_cloud_connect [bool]: false
|
||||
# want_windows_veeam_backup [bool]: false
|
||||
# checkmk_unifi_status_provisioning [int]: 1
|
||||
# checkmk_unifi_status_upgrading [int]: 1
|
||||
# checkmk_unifi_status_upgradable [int]: 0
|
||||
# checkmk_unifi_status_heartbeat_missed [int]: 1
|
||||
# checkmk_unifi_status_noautobackup [int]: 0
|
||||
# checkmk_windows_updates_timeout [int]: 3600
|
||||
# checkmk_windows_updates_cache [int]: 43200
|
||||
# checkmk_mk_inventory_timeout [int]: 120
|
||||
# checkmk_plugins_default_timeout [int]: 120
|
||||
# checkmk_plugins_default_cache [int]: 600
|
||||
# checkmk_extra_plugin_patterns [sequence]: []
|
||||
# AUTH: existing inventory / Vault credentials; no embedded passwords.
|
||||
# CHANGES: AIM-managed script files are updated. On Windows only the marked plugins section in check_mk.user.yml is replaced; all other user-config sections are preserved. Only the opposite UniFi check is removed during a UniFi mode transition.
|
||||
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
|
||||
# playbooks/checkmk_update_scripts_config.yml --limit <host> --vault-id <customer>@prompt
|
||||
- name: Checkmk | Update linux
|
||||
hosts: linux
|
||||
gather_facts: true
|
||||
pre_tasks:
|
||||
- name: AIM | Validate diagnostics option
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in
|
||||
['true', 'false']
|
||||
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||
quiet: true
|
||||
- name: AIM | Reject mixed platform membership
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
|
||||
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
|
||||
quiet: true
|
||||
- name: AIM | Execution context
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
host: '{{ inventory_hostname }}'
|
||||
diagnostics: Enabled; secret values are never included by this task.
|
||||
when: aim_debug | default(false) | bool
|
||||
- name: Load system detect roles
|
||||
ansible.builtin.include_role:
|
||||
name: system_detect_roles
|
||||
- name: Load checkmk script plan
|
||||
ansible.builtin.include_role:
|
||||
name: checkmk_script_plan
|
||||
- name: Checkmk | Preflight scripts and credentials
|
||||
ansible.builtin.include_role:
|
||||
name: checkmk_deploy_scripts
|
||||
tasks_from: preflight
|
||||
become: true
|
||||
roles:
|
||||
- role: checkmk_deploy_scripts
|
||||
- role: checkmk_configure_agent
|
||||
- role: checkmk_manage_service
|
||||
post_tasks:
|
||||
- &id001
|
||||
name: Checkmk | Collect managed change summary
|
||||
ansible.builtin.set_fact:
|
||||
_aim_checkmk_changes: '{{ _aim_check_copies | default({}) | aim_report_checkmk_changes(_checkmk_selected_scripts,
|
||||
_checkmk_plugins_update | default({}), _aim_unifi_write | default({}), _aim_opposite_remove | default({}),
|
||||
_checkmk_unifi_effective, ansible_check_mode) }}'
|
||||
- name: AIM | Publish operation result
|
||||
ansible.builtin.set_stats:
|
||||
per_host: true
|
||||
aggregate: false
|
||||
data:
|
||||
aim_output:
|
||||
protocol: aim_output_v1
|
||||
schema: checkmk_agent_config_v1
|
||||
data: '{{ _aim_checkmk_changes }}'
|
||||
- name: Checkmk | Update windows
|
||||
hosts: windows
|
||||
gather_facts: true
|
||||
pre_tasks:
|
||||
- name: AIM | Validate diagnostics option
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in
|
||||
['true', 'false']
|
||||
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||
quiet: true
|
||||
- name: AIM | Reject mixed platform membership
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
|
||||
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
|
||||
quiet: true
|
||||
- name: AIM | Execution context
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
host: '{{ inventory_hostname }}'
|
||||
diagnostics: Enabled; secret values are never included by this task.
|
||||
when: aim_debug | default(false) | bool
|
||||
- name: Load system detect roles
|
||||
ansible.builtin.include_role:
|
||||
name: system_detect_roles
|
||||
- name: Load checkmk script plan
|
||||
ansible.builtin.include_role:
|
||||
name: checkmk_script_plan
|
||||
- name: Checkmk | Preflight scripts and credentials
|
||||
ansible.builtin.include_role:
|
||||
name: checkmk_deploy_scripts
|
||||
tasks_from: preflight
|
||||
roles:
|
||||
- role: checkmk_deploy_scripts
|
||||
- role: checkmk_configure_agent
|
||||
- role: checkmk_manage_service
|
||||
post_tasks:
|
||||
- *id001
|
||||
- name: AIM | Publish operation result
|
||||
ansible.builtin.set_stats:
|
||||
per_host: true
|
||||
aggregate: false
|
||||
data:
|
||||
aim_output:
|
||||
protocol: aim_output_v1
|
||||
schema: checkmk_agent_config_v1
|
||||
data: '{{ _aim_checkmk_changes }}'
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
# PURPOSE: Apply customer Sophos configuration
|
||||
# DESCRIPTION: Apply this customer profile using hostname, network_objects and vlan_interfaces from inventory.
|
||||
# TARGETS: sophosxgs
|
||||
# REQUIRED NETWORK KEYS: derz_lan, derz_sslvpn, facility, guest, lan_old, management, office, server, voip
|
||||
# INPUTS (omitted values inherit inventory / role defaults):
|
||||
# aim_debug [bool]: false
|
||||
# AUTH: existing inventory / Vault credentials; no embedded passwords.
|
||||
# CHANGES: Changes customer firewall configuration. VLAN parent remains Port1 as in the supplied playbooks. Only this customer profile is selected.
|
||||
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
|
||||
# playbooks/customers/bluuunit/sophos_apply_customer.yml --limit <host> --vault-id <customer>@prompt --ask-pass
|
||||
- name: Sophos | Apply customer configuration | bluuunit
|
||||
hosts: sophosxgs
|
||||
gather_facts: false
|
||||
any_errors_fatal: false
|
||||
tasks:
|
||||
- name: Apply customer firewall policy
|
||||
ansible.builtin.import_role:
|
||||
name: sophos_customer_bluuunit
|
||||
pre_tasks:
|
||||
- name: AIM | Validate diagnostics option
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
|
||||
'false']
|
||||
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||
quiet: true
|
||||
- name: AIM | Reject mixed platform membership
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
|
||||
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
|
||||
quiet: true
|
||||
- name: AIM | Execution context
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
host: '{{ inventory_hostname }}'
|
||||
diagnostics: Enabled; secret values are never included by this task.
|
||||
when: aim_debug | default(false) | bool
|
||||
- name: Sophos | Require customer host configuration
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- hostname is defined
|
||||
- hostname is string
|
||||
- hostname | length > 0
|
||||
- network_objects is defined
|
||||
- network_objects is mapping
|
||||
- vlan_interfaces is defined
|
||||
- vlan_interfaces is mapping
|
||||
fail_msg: Set hostname, network_objects and vlan_interfaces in this host inventory. AIM can prepare
|
||||
these fields.
|
||||
quiet: true
|
||||
- name: Sophos | Require profile network keys
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- item in network_objects
|
||||
fail_msg: A customer-required network_objects key is missing. See the loop item.
|
||||
quiet: true
|
||||
loop:
|
||||
- derz_lan
|
||||
- derz_sslvpn
|
||||
- facility
|
||||
- guest
|
||||
- lan_old
|
||||
- management
|
||||
- office
|
||||
- server
|
||||
- voip
|
||||
- name: Sophos | Validate network object shape
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- item.value is mapping
|
||||
- item.value.name is defined
|
||||
- item.value.network is defined
|
||||
- item.value.subnetmask is defined
|
||||
fail_msg: Every network object requires name, network and subnetmask.
|
||||
quiet: true
|
||||
loop: '{{ network_objects | dict2items }}'
|
||||
loop_control:
|
||||
label: '{{ item.key }}'
|
||||
- name: Sophos | Validate VLAN shape
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- item.value is mapping
|
||||
- item.value.name is defined
|
||||
- item.value.ip_address is defined
|
||||
- item.value.subnetmask is defined
|
||||
- item.value.vlan_id is defined
|
||||
- item.value.zone_name is defined
|
||||
- item.value.zone_type is defined
|
||||
- item.value.zone_description is defined
|
||||
fail_msg: Each VLAN requires its full interface and zone mapping.
|
||||
quiet: true
|
||||
loop: '{{ vlan_interfaces | dict2items }}'
|
||||
loop_control:
|
||||
label: '{{ item.key }}'
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
# PURPOSE: Apply customer Sophos configuration
|
||||
# DESCRIPTION: Apply this customer profile using hostname, network_objects and vlan_interfaces from inventory.
|
||||
# TARGETS: sophosxgs
|
||||
# REQUIRED NETWORK KEYS: azuregwc_lan, lan_old, management, office
|
||||
# INPUTS (omitted values inherit inventory / role defaults):
|
||||
# aim_debug [bool]: false
|
||||
# AUTH: existing inventory / Vault credentials; no embedded passwords.
|
||||
# CHANGES: Changes customer firewall configuration. VLAN parent remains Port1 as in the supplied playbooks. Only this customer profile is selected.
|
||||
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
|
||||
# playbooks/customers/formicon/sophos_apply_customer.yml --limit <host> --vault-id <customer>@prompt --ask-pass
|
||||
- name: Sophos | Apply customer configuration | formicon
|
||||
hosts: sophosxgs
|
||||
gather_facts: false
|
||||
any_errors_fatal: false
|
||||
tasks:
|
||||
- name: Apply customer firewall policy
|
||||
ansible.builtin.import_role:
|
||||
name: sophos_customer_formicon
|
||||
pre_tasks:
|
||||
- name: AIM | Validate diagnostics option
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
|
||||
'false']
|
||||
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||
quiet: true
|
||||
- name: AIM | Reject mixed platform membership
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
|
||||
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
|
||||
quiet: true
|
||||
- name: AIM | Execution context
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
host: '{{ inventory_hostname }}'
|
||||
diagnostics: Enabled; secret values are never included by this task.
|
||||
when: aim_debug | default(false) | bool
|
||||
- name: Sophos | Require customer host configuration
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- hostname is defined
|
||||
- hostname is string
|
||||
- hostname | length > 0
|
||||
- network_objects is defined
|
||||
- network_objects is mapping
|
||||
- vlan_interfaces is defined
|
||||
- vlan_interfaces is mapping
|
||||
fail_msg: Set hostname, network_objects and vlan_interfaces in this host inventory. AIM can prepare
|
||||
these fields.
|
||||
quiet: true
|
||||
- name: Sophos | Require profile network keys
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- item in network_objects
|
||||
fail_msg: A customer-required network_objects key is missing. See the loop item.
|
||||
quiet: true
|
||||
loop:
|
||||
- azuregwc_lan
|
||||
- lan_old
|
||||
- management
|
||||
- office
|
||||
- name: Sophos | Validate network object shape
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- item.value is mapping
|
||||
- item.value.name is defined
|
||||
- item.value.network is defined
|
||||
- item.value.subnetmask is defined
|
||||
fail_msg: Every network object requires name, network and subnetmask.
|
||||
quiet: true
|
||||
loop: '{{ network_objects | dict2items }}'
|
||||
loop_control:
|
||||
label: '{{ item.key }}'
|
||||
- name: Sophos | Validate VLAN shape
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- item.value is mapping
|
||||
- item.value.name is defined
|
||||
- item.value.ip_address is defined
|
||||
- item.value.subnetmask is defined
|
||||
- item.value.vlan_id is defined
|
||||
- item.value.zone_name is defined
|
||||
- item.value.zone_type is defined
|
||||
- item.value.zone_description is defined
|
||||
fail_msg: Each VLAN requires its full interface and zone mapping.
|
||||
quiet: true
|
||||
loop: '{{ vlan_interfaces | dict2items }}'
|
||||
loop_control:
|
||||
label: '{{ item.key }}'
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
# PURPOSE: Apply customer Sophos configuration
|
||||
# DESCRIPTION: Apply this customer profile using hostname, network_objects and vlan_interfaces from inventory.
|
||||
# TARGETS: sophosxgs
|
||||
# REQUIRED NETWORK KEYS: drucker, guest, office, wlan
|
||||
# INPUTS (omitted values inherit inventory / role defaults):
|
||||
# aim_debug [bool]: false
|
||||
# AUTH: existing inventory / Vault credentials; no embedded passwords.
|
||||
# CHANGES: Changes customer firewall configuration. VLAN parent remains Port1 as in the supplied playbooks. Only this customer profile is selected.
|
||||
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
|
||||
# playbooks/customers/gebhardt_stahl/sophos_apply_customer.yml --limit <host> --vault-id <customer>@prompt --ask-pass
|
||||
- name: Sophos | Apply customer configuration | gebhardt_stahl
|
||||
hosts: sophosxgs
|
||||
gather_facts: false
|
||||
any_errors_fatal: false
|
||||
tasks:
|
||||
- name: Apply customer firewall policy
|
||||
ansible.builtin.import_role:
|
||||
name: sophos_customer_gebhardt_stahl
|
||||
pre_tasks:
|
||||
- name: AIM | Validate diagnostics option
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
|
||||
'false']
|
||||
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||
quiet: true
|
||||
- name: AIM | Reject mixed platform membership
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
|
||||
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
|
||||
quiet: true
|
||||
- name: AIM | Execution context
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
host: '{{ inventory_hostname }}'
|
||||
diagnostics: Enabled; secret values are never included by this task.
|
||||
when: aim_debug | default(false) | bool
|
||||
- name: Sophos | Require customer host configuration
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- hostname is defined
|
||||
- hostname is string
|
||||
- hostname | length > 0
|
||||
- network_objects is defined
|
||||
- network_objects is mapping
|
||||
- vlan_interfaces is defined
|
||||
- vlan_interfaces is mapping
|
||||
fail_msg: Set hostname, network_objects and vlan_interfaces in this host inventory. AIM can prepare
|
||||
these fields.
|
||||
quiet: true
|
||||
- name: Sophos | Require profile network keys
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- item in network_objects
|
||||
fail_msg: A customer-required network_objects key is missing. See the loop item.
|
||||
quiet: true
|
||||
loop:
|
||||
- drucker
|
||||
- guest
|
||||
- office
|
||||
- wlan
|
||||
- name: Sophos | Validate network object shape
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- item.value is mapping
|
||||
- item.value.name is defined
|
||||
- item.value.network is defined
|
||||
- item.value.subnetmask is defined
|
||||
fail_msg: Every network object requires name, network and subnetmask.
|
||||
quiet: true
|
||||
loop: '{{ network_objects | dict2items }}'
|
||||
loop_control:
|
||||
label: '{{ item.key }}'
|
||||
- name: Sophos | Validate VLAN shape
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- item.value is mapping
|
||||
- item.value.name is defined
|
||||
- item.value.ip_address is defined
|
||||
- item.value.subnetmask is defined
|
||||
- item.value.vlan_id is defined
|
||||
- item.value.zone_name is defined
|
||||
- item.value.zone_type is defined
|
||||
- item.value.zone_description is defined
|
||||
fail_msg: Each VLAN requires its full interface and zone mapping.
|
||||
quiet: true
|
||||
loop: '{{ vlan_interfaces | dict2items }}'
|
||||
loop_control:
|
||||
label: '{{ item.key }}'
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
# PURPOSE: Apply customer Sophos configuration
|
||||
# DESCRIPTION: Apply this customer profile using hostname, network_objects and vlan_interfaces from inventory.
|
||||
# TARGETS: sophosxgs
|
||||
# REQUIRED NETWORK KEYS: facility, guest, management, office, voip
|
||||
# INPUTS (omitted values inherit inventory / role defaults):
|
||||
# aim_debug [bool]: false
|
||||
# AUTH: existing inventory / Vault credentials; no embedded passwords.
|
||||
# CHANGES: Changes customer firewall configuration. VLAN parent remains Port1 as in the supplied playbooks. Only this customer profile is selected.
|
||||
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
|
||||
# playbooks/customers/hungeling_und_toechter/sophos_apply_customer.yml --limit <host> --vault-id <customer>@prompt --ask-pass
|
||||
- name: Sophos | Apply customer configuration | hungeling_und_toechter
|
||||
hosts: sophosxgs
|
||||
gather_facts: false
|
||||
any_errors_fatal: false
|
||||
tasks:
|
||||
- name: Apply customer firewall policy
|
||||
ansible.builtin.import_role:
|
||||
name: sophos_customer_hungeling_und_toechter
|
||||
pre_tasks:
|
||||
- name: AIM | Validate diagnostics option
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
|
||||
'false']
|
||||
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||
quiet: true
|
||||
- name: AIM | Reject mixed platform membership
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
|
||||
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
|
||||
quiet: true
|
||||
- name: AIM | Execution context
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
host: '{{ inventory_hostname }}'
|
||||
diagnostics: Enabled; secret values are never included by this task.
|
||||
when: aim_debug | default(false) | bool
|
||||
- name: Sophos | Require customer host configuration
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- hostname is defined
|
||||
- hostname is string
|
||||
- hostname | length > 0
|
||||
- network_objects is defined
|
||||
- network_objects is mapping
|
||||
- vlan_interfaces is defined
|
||||
- vlan_interfaces is mapping
|
||||
fail_msg: Set hostname, network_objects and vlan_interfaces in this host inventory. AIM can prepare
|
||||
these fields.
|
||||
quiet: true
|
||||
- name: Sophos | Require profile network keys
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- item in network_objects
|
||||
fail_msg: A customer-required network_objects key is missing. See the loop item.
|
||||
quiet: true
|
||||
loop:
|
||||
- facility
|
||||
- guest
|
||||
- management
|
||||
- office
|
||||
- voip
|
||||
- name: Sophos | Validate network object shape
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- item.value is mapping
|
||||
- item.value.name is defined
|
||||
- item.value.network is defined
|
||||
- item.value.subnetmask is defined
|
||||
fail_msg: Every network object requires name, network and subnetmask.
|
||||
quiet: true
|
||||
loop: '{{ network_objects | dict2items }}'
|
||||
loop_control:
|
||||
label: '{{ item.key }}'
|
||||
- name: Sophos | Validate VLAN shape
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- item.value is mapping
|
||||
- item.value.name is defined
|
||||
- item.value.ip_address is defined
|
||||
- item.value.subnetmask is defined
|
||||
- item.value.vlan_id is defined
|
||||
- item.value.zone_name is defined
|
||||
- item.value.zone_type is defined
|
||||
- item.value.zone_description is defined
|
||||
fail_msg: Each VLAN requires its full interface and zone mapping.
|
||||
quiet: true
|
||||
loop: '{{ vlan_interfaces | dict2items }}'
|
||||
loop_control:
|
||||
label: '{{ item.key }}'
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
# PURPOSE: Apply customer Sophos configuration
|
||||
# DESCRIPTION: Apply this customer profile using hostname, network_objects and vlan_interfaces from inventory.
|
||||
# TARGETS: sophosxgs
|
||||
# REQUIRED NETWORK KEYS: facility, guest, management, office, server, voip
|
||||
# INPUTS (omitted values inherit inventory / role defaults):
|
||||
# aim_debug [bool]: false
|
||||
# AUTH: existing inventory / Vault credentials; no embedded passwords.
|
||||
# CHANGES: Changes customer firewall configuration. VLAN parent remains Port1 as in the supplied playbooks. Only this customer profile is selected.
|
||||
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
|
||||
# playbooks/customers/koenig_holding_gmbh/sophos_apply_customer.yml --limit <host> --vault-id <customer>@prompt --ask-pass
|
||||
- name: Sophos | Apply customer configuration | koenig_holding_gmbh
|
||||
hosts: sophosxgs
|
||||
gather_facts: false
|
||||
any_errors_fatal: false
|
||||
tasks:
|
||||
- name: Apply customer firewall policy
|
||||
ansible.builtin.import_role:
|
||||
name: sophos_customer_koenig_holding_gmbh
|
||||
pre_tasks:
|
||||
- name: AIM | Validate diagnostics option
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
|
||||
'false']
|
||||
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||
quiet: true
|
||||
- name: AIM | Reject mixed platform membership
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
|
||||
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
|
||||
quiet: true
|
||||
- name: AIM | Execution context
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
host: '{{ inventory_hostname }}'
|
||||
diagnostics: Enabled; secret values are never included by this task.
|
||||
when: aim_debug | default(false) | bool
|
||||
- name: Sophos | Require customer host configuration
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- hostname is defined
|
||||
- hostname is string
|
||||
- hostname | length > 0
|
||||
- network_objects is defined
|
||||
- network_objects is mapping
|
||||
- vlan_interfaces is defined
|
||||
- vlan_interfaces is mapping
|
||||
fail_msg: Set hostname, network_objects and vlan_interfaces in this host inventory. AIM can prepare
|
||||
these fields.
|
||||
quiet: true
|
||||
- name: Sophos | Require profile network keys
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- item in network_objects
|
||||
fail_msg: A customer-required network_objects key is missing. See the loop item.
|
||||
quiet: true
|
||||
loop:
|
||||
- facility
|
||||
- guest
|
||||
- management
|
||||
- office
|
||||
- server
|
||||
- voip
|
||||
- name: Sophos | Validate network object shape
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- item.value is mapping
|
||||
- item.value.name is defined
|
||||
- item.value.network is defined
|
||||
- item.value.subnetmask is defined
|
||||
fail_msg: Every network object requires name, network and subnetmask.
|
||||
quiet: true
|
||||
loop: '{{ network_objects | dict2items }}'
|
||||
loop_control:
|
||||
label: '{{ item.key }}'
|
||||
- name: Sophos | Validate VLAN shape
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- item.value is mapping
|
||||
- item.value.name is defined
|
||||
- item.value.ip_address is defined
|
||||
- item.value.subnetmask is defined
|
||||
- item.value.vlan_id is defined
|
||||
- item.value.zone_name is defined
|
||||
- item.value.zone_type is defined
|
||||
- item.value.zone_description is defined
|
||||
fail_msg: Each VLAN requires its full interface and zone mapping.
|
||||
quiet: true
|
||||
loop: '{{ vlan_interfaces | dict2items }}'
|
||||
loop_control:
|
||||
label: '{{ item.key }}'
|
||||
@@ -0,0 +1,62 @@
|
||||
# PURPOSE: Detect host roles
|
||||
# DESCRIPTION: Report detected AD, DHCP, Hyper-V, Veeam and UniFi capabilities without changing inventory memberships.
|
||||
# TARGETS: linux, windows
|
||||
# INPUTS (omitted values inherit inventory / role defaults):
|
||||
# aim_debug [bool]: false
|
||||
# AUTH: existing inventory / Vault credentials; no embedded passwords.
|
||||
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
|
||||
# playbooks/debug_detect_host_roles.yml --limit <host> --vault-id <customer>@prompt
|
||||
- name: Debug | Detected host roles
|
||||
hosts: linux:windows
|
||||
gather_facts: true
|
||||
pre_tasks:
|
||||
- name: AIM | Validate diagnostics option
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in
|
||||
['true', 'false']
|
||||
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||
quiet: true
|
||||
- name: AIM | Reject mixed platform membership
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
|
||||
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
|
||||
quiet: true
|
||||
- name: AIM | Execution context
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
host: '{{ inventory_hostname }}'
|
||||
diagnostics: Enabled; secret values are never included by this task.
|
||||
when: aim_debug | default(false) | bool
|
||||
roles:
|
||||
- role: system_detect_roles
|
||||
tasks:
|
||||
- name: Detection summary
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
is_dc: '{{ is_dc | default(false) }}'
|
||||
is_dhcp_server: '{{ is_dhcp_server | default(false) }}'
|
||||
is_hyperv_host: '{{ is_hyperv_host | default(false) }}'
|
||||
has_veeam_vbr: '{{ has_veeam_vbr | default(false) }}'
|
||||
has_veeam_vbo: '{{ has_veeam_vbo | default(false) }}'
|
||||
has_veeam_em: '{{ has_veeam_em | default(false) }}'
|
||||
is_unifi_controller: '{{ is_unifi_controller | default(false) }}'
|
||||
is_unifi_os_server: '{{ is_unifi_os_server | default(false) }}'
|
||||
- name: AIM | Publish operation result
|
||||
ansible.builtin.set_stats:
|
||||
per_host: true
|
||||
aggregate: false
|
||||
data:
|
||||
aim_output:
|
||||
protocol: aim_output_v1
|
||||
schema: host_capabilities_v1
|
||||
data:
|
||||
is_dc: '{{ is_dc | default(false) | bool }}'
|
||||
is_dhcp_server: '{{ is_dhcp_server | default(false) | bool }}'
|
||||
is_hyperv_host: '{{ is_hyperv_host | default(false) | bool }}'
|
||||
has_veeam_vbr: '{{ has_veeam_vbr | default(false) | bool }}'
|
||||
has_veeam_vbo: '{{ has_veeam_vbo | default(false) | bool }}'
|
||||
has_veeam_em: '{{ has_veeam_em | default(false) | bool }}'
|
||||
is_unifi_controller: '{{ is_unifi_controller | default(false) | bool }}'
|
||||
is_unifi_os_server: '{{ is_unifi_os_server | default(false) | bool }}'
|
||||
@@ -0,0 +1,185 @@
|
||||
# PURPOSE: Show disk usage
|
||||
# DESCRIPTION: Report attached Windows volumes and common operational Linux mounts.
|
||||
# TARGETS: windows, linux
|
||||
# INPUTS (omitted values inherit inventory / role defaults):
|
||||
# aim_debug [bool]: false
|
||||
# AUTH: existing inventory / Vault credentials; no embedded passwords.
|
||||
# NOTES:
|
||||
# Windows reports attached storage volumes from community.windows.win_disk_facts; mapped/network drives are excluded.
|
||||
# Linux excludes pseudo/system mounts and reports common operational paths plus network/storage filesystems.
|
||||
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
|
||||
# playbooks/debug_show_disk_usage.yml --limit <host> --vault-id <customer>@prompt
|
||||
|
||||
- name: Debug | Windows disk usage
|
||||
hosts: windows
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: AIM | Validate diagnostics option
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in
|
||||
['true', 'false']
|
||||
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||
quiet: true
|
||||
|
||||
- name: AIM | Reject mixed platform membership
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
|
||||
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
|
||||
quiet: true
|
||||
|
||||
- name: AIM | Execution context
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
host: '{{ inventory_hostname }}'
|
||||
diagnostics: Enabled; secret values are never included by this task.
|
||||
when: aim_debug | default(false) | bool
|
||||
|
||||
- name: Windows | Gather attached disk and volume facts
|
||||
community.windows.win_disk_facts:
|
||||
filter:
|
||||
- partitions
|
||||
- volumes
|
||||
changed_when: false
|
||||
|
||||
- name: Windows | Normalize attached volume usage
|
||||
ansible.builtin.set_fact:
|
||||
_windows_disk_report: "{{ ansible_facts.disks | default([]) | aim_report_disks('windows') }}"
|
||||
|
||||
- name: Windows | Print disk usage
|
||||
ansible.builtin.debug:
|
||||
msg: |-
|
||||
{{ inventory_hostname }}
|
||||
{% for d in _windows_disk_report.filesystems | default([]) %}
|
||||
{% if d.status == 'available' %}
|
||||
{{ '%-18s' | format(d.name) }} {{ (d.used_bytes / 1073741824) | round(2) }} GB / {{ (d.total_bytes / 1073741824) | round(2) }} GB | {{ d.used_percent | round(1) }}% used | {{ (d.available_bytes / 1073741824) | round(2) }} GB free | {{ d.filesystem_type | default('unknown', true) }} | {{ d.mount }}
|
||||
{% else %}
|
||||
{{ '%-18s' | format(d.name) }} unavailable | {{ d.mount }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
- name: AIM | Publish operation result
|
||||
ansible.builtin.set_stats:
|
||||
per_host: true
|
||||
aggregate: false
|
||||
data:
|
||||
aim_output:
|
||||
protocol: aim_output_v1
|
||||
schema: filesystem_usage_v1
|
||||
data: "{{ _windows_disk_report }}"
|
||||
- name: Debug | Linux disk usage
|
||||
hosts: linux
|
||||
gather_facts: false
|
||||
become: false
|
||||
vars:
|
||||
_disk_common_mounts:
|
||||
- /
|
||||
- /boot
|
||||
- /boot/efi
|
||||
- /home
|
||||
- /var
|
||||
- /var/log
|
||||
- /tmp
|
||||
- /opt
|
||||
- /srv
|
||||
_disk_network_storage_fstypes:
|
||||
- nfs
|
||||
- nfs4
|
||||
- cifs
|
||||
- smb3
|
||||
- ceph
|
||||
- glusterfs
|
||||
- fuse.sshfs
|
||||
- fuse.glusterfs
|
||||
_disk_excluded_fstypes:
|
||||
- proc
|
||||
- sysfs
|
||||
- devtmpfs
|
||||
- tmpfs
|
||||
- cgroup
|
||||
- cgroup2
|
||||
- overlay
|
||||
- squashfs
|
||||
- nsfs
|
||||
- tracefs
|
||||
- debugfs
|
||||
- securityfs
|
||||
- pstore
|
||||
- configfs
|
||||
- hugetlbfs
|
||||
- mqueue
|
||||
- rpc_pipefs
|
||||
- autofs
|
||||
- fusectl
|
||||
- binfmt_misc
|
||||
tasks:
|
||||
- name: AIM | Validate diagnostics option
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in
|
||||
['true', 'false']
|
||||
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||
quiet: true
|
||||
|
||||
- name: AIM | Reject mixed platform membership
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
|
||||
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
|
||||
quiet: true
|
||||
|
||||
- name: Linux | Collect mount facts
|
||||
ansible.builtin.setup:
|
||||
filter:
|
||||
- ansible_mounts
|
||||
gather_subset:
|
||||
- '!all'
|
||||
- '!min'
|
||||
changed_when: false
|
||||
|
||||
- name: Linux | Reset selected mount report
|
||||
ansible.builtin.set_fact:
|
||||
_linux_disk_mounts: []
|
||||
- name: Linux | Select common operational mounts
|
||||
ansible.builtin.set_fact:
|
||||
_linux_disk_mounts: '{{ (_linux_disk_mounts | default([])) + [item] }}'
|
||||
loop: '{{ ansible_facts.mounts | default(ansible_mounts | default([])) }}'
|
||||
loop_control:
|
||||
label: "{{ item.mount | default('?') }}"
|
||||
when:
|
||||
- item.fstype | default('') not in _disk_excluded_fstypes
|
||||
- >-
|
||||
(item.mount | default('')) in _disk_common_mounts
|
||||
or (item.mount | default('')).startswith('/mnt/')
|
||||
or (item.mount | default('')).startswith('/media/')
|
||||
or (item.fstype | default('')) in _disk_network_storage_fstypes
|
||||
|
||||
- name: Linux | Execution context
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
host: '{{ inventory_hostname }}'
|
||||
selected_mounts: "{{ (_linux_disk_mounts | default([])) | map(attribute='mount') | list }}"
|
||||
diagnostics: Enabled; pseudo/system mounts are excluded.
|
||||
when: aim_debug | default(false) | bool
|
||||
|
||||
- name: Linux | Print disk usage
|
||||
ansible.builtin.debug:
|
||||
msg: |-
|
||||
{{ inventory_hostname }}
|
||||
{% for m in (_linux_disk_mounts | default([]) | sort(attribute='mount')) %}
|
||||
{% set total = m.size_total | default(0) | float %}
|
||||
{% set free = m.size_available | default(0) | float %}
|
||||
{% set used = total - free %}
|
||||
{% set pct = ((used / total) * 100) if total > 0 else 0 %}
|
||||
{{ '%-18s' | format(m.mount) }} {{ (used / 1073741824) | round(2) }} GB / {{ (total / 1073741824) | round(2) }} GB | {{ pct | round(1) }}% used | {{ (free / 1073741824) | round(2) }} GB free | {{ m.fstype }}
|
||||
{% endfor %}
|
||||
- name: AIM | Publish operation result
|
||||
ansible.builtin.set_stats:
|
||||
per_host: true
|
||||
aggregate: false
|
||||
data:
|
||||
aim_output:
|
||||
protocol: aim_output_v1
|
||||
schema: filesystem_usage_v1
|
||||
data: "{{ _linux_disk_mounts | aim_report_disks('linux') }}"
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
# PURPOSE: Test Ansible connection
|
||||
# DESCRIPTION: Check Ansible manageability using ping or win_ping; this is not an ICMP ping.
|
||||
# TARGETS: linux, windows
|
||||
# INPUTS (omitted values inherit inventory / role defaults):
|
||||
# aim_debug [bool]: false
|
||||
# AUTH: existing inventory / Vault credentials; no embedded passwords.
|
||||
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
|
||||
# playbooks/debug_test_connection.yml --limit <host> --vault-id <customer>@prompt
|
||||
- name: Debug | Windows manageability
|
||||
hosts: windows
|
||||
gather_facts: false
|
||||
pre_tasks:
|
||||
- name: AIM | Validate diagnostics option
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
|
||||
'false']
|
||||
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||
quiet: true
|
||||
- name: AIM | Reject mixed platform membership
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
|
||||
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
|
||||
quiet: true
|
||||
- name: AIM | Execution context
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
host: '{{ inventory_hostname }}'
|
||||
diagnostics: Enabled; secret values are never included by this task.
|
||||
when: aim_debug | default(false) | bool
|
||||
tasks:
|
||||
- name: Windows | Test WinRM
|
||||
ansible.windows.win_ping: {}
|
||||
- name: Debug | Linux manageability
|
||||
hosts: linux
|
||||
gather_facts: false
|
||||
pre_tasks:
|
||||
- name: AIM | Validate diagnostics option
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
|
||||
'false']
|
||||
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||
quiet: true
|
||||
- name: AIM | Reject mixed platform membership
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
|
||||
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
|
||||
quiet: true
|
||||
- name: AIM | Execution context
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
host: '{{ inventory_hostname }}'
|
||||
diagnostics: Enabled; secret values are never included by this task.
|
||||
when: aim_debug | default(false) | bool
|
||||
tasks:
|
||||
- name: Linux | Test SSH and Python
|
||||
ansible.builtin.ping: {}
|
||||
Binary file not shown.
@@ -0,0 +1,398 @@
|
||||
"""Report normalization owned by the shipped runbooks, not by the Core API.
|
||||
|
||||
Only deliberately selected public fields leave these functions. Raw registered
|
||||
results, exception text, credentials and command lines are never returned.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from collections.abc import Mapping
|
||||
|
||||
|
||||
def _bool(value):
|
||||
if type(value) is bool: return value
|
||||
if str(value).lower() in ('true','yes','1'): return True
|
||||
if str(value).lower() in ('false','no','0','none',''): return False
|
||||
raise ValueError('Report boolean is not a supported literal')
|
||||
|
||||
|
||||
def epoch_iso_utc(value):
|
||||
return datetime.fromtimestamp(float(value), tz=timezone.utc).isoformat().replace('+00:00','Z')
|
||||
|
||||
def capabilities(value):
|
||||
names = ('is_dc','is_dhcp_server','is_hyperv_host','has_veeam_vbr','has_veeam_vbo',
|
||||
'has_veeam_em','is_unifi_controller','is_unifi_os_server')
|
||||
return {k:_bool(value.get(k, False)) for k in names}
|
||||
|
||||
|
||||
def disks(value, platform):
|
||||
result=[]
|
||||
if platform == 'windows':
|
||||
# community.windows.win_disk_facts returns disks -> partitions -> volumes.
|
||||
# Report attached volumes rather than PowerShell-session/mapped drives.
|
||||
for disk in value or []:
|
||||
for partition in disk.get('partitions', []) or []:
|
||||
drive_letter=partition.get('drive_letter')
|
||||
for volume in partition.get('volumes', []) or []:
|
||||
total=volume.get('size')
|
||||
free=volume.get('size_remaining')
|
||||
good=isinstance(total, (int, float)) and isinstance(free, (int, float)) and total >= 0 and free >= 0
|
||||
if good:
|
||||
total=int(total); free=int(free); used=max(0,total-free)
|
||||
pct=round(used/total*100,2) if total else 0.0
|
||||
else:
|
||||
used=total=free=pct=None
|
||||
native_path=volume.get('path') or volume.get('object_id') or ''
|
||||
if drive_letter:
|
||||
name=f'{drive_letter}:'
|
||||
mount=f'{drive_letter}:\\'
|
||||
else:
|
||||
name=volume.get('label') or native_path or f"volume-disk{disk.get('number','?')}-part{partition.get('number','?')}"
|
||||
mount=native_path or name
|
||||
result.append(dict(name=str(name),mount=str(mount),filesystem_type=volume.get('type'),
|
||||
used_bytes=used,total_bytes=total,available_bytes=free,used_percent=pct,
|
||||
status='available' if good else 'unavailable'))
|
||||
else:
|
||||
for row in value:
|
||||
total,free=int(row.get('size_total',0)),int(row.get('size_available',0))
|
||||
used=max(0,total-free); good=total>0
|
||||
name,mount,fs=row.get('device',row['mount']),row['mount'],row.get('fstype')
|
||||
if not good: used=total=free=pct=None
|
||||
else:
|
||||
used,total,free=(int(x) if x is not None else None for x in (used,total,free))
|
||||
pct=round(used/total*100,2) if total and used is not None else None
|
||||
result.append(dict(name=name,mount=mount,filesystem_type=fs,used_bytes=used,total_bytes=total,
|
||||
available_bytes=free,used_percent=pct,status='available' if good else 'unavailable'))
|
||||
return {'platform':platform,'filesystems':result}
|
||||
|
||||
|
||||
SERVICE_ERRORS={
|
||||
5:('permission_denied','Access was denied when starting the service.'),
|
||||
1053:('start_timeout','The service did not respond to the start request in time.'),
|
||||
1058:('service_disabled','The service is disabled.'),
|
||||
1060:('service_not_found','The service no longer exists.'),
|
||||
1068:('dependency_failed','A required dependency service could not be started.'),
|
||||
1069:('logon_failed','The service account could not log on.'),
|
||||
}
|
||||
|
||||
def service_error(raw):
|
||||
code=raw.get('native_code',raw.get('error_code'))
|
||||
if type(code) is not int: code=None
|
||||
reason,message=SERVICE_ERRORS.get(code,('start_failed','The service start request failed.'))
|
||||
# A bounded fallback for the existing win_service module; no raw text export.
|
||||
text=str(raw.get('msg',''))[:4096].lower()
|
||||
if code is None:
|
||||
signatures=[('access is denied',5),('access denied',5),('dependency',1068),
|
||||
('disabled',1058),('timed out',1053),('does not exist',1060),('logon failure',1069)]
|
||||
for term,num in signatures:
|
||||
if term in text:
|
||||
reason,message=SERVICE_ERRORS[num]; break
|
||||
return reason,message,code
|
||||
|
||||
|
||||
def services(before, attempts, after, include=(), exclude=(), check=False):
|
||||
stopped=sorted(s['name'] for s in before if s.get('state')=='stopped')
|
||||
eligible=sorted(s['name'] for s in before if s.get('state')=='stopped'
|
||||
and s.get('start_mode') in ('auto','delayed') and (not include or s['name'] in include)
|
||||
and s['name'] not in exclude)
|
||||
actual={}
|
||||
for row in attempts:
|
||||
if row.get('skipped'): continue
|
||||
name=row.get('item',{}).get('name')
|
||||
if name in eligible and not check: actual[name]=row
|
||||
states={s['name']:s.get('state','unknown') for s in after}
|
||||
newly=sorted(n for n in stopped if states.get(n)=='started')
|
||||
still=sorted(n for n in stopped if states.get(n)=='stopped')
|
||||
absent=sorted(n for n in stopped if n not in states)
|
||||
failures=[]
|
||||
for name, row in actual.items():
|
||||
if states.get(name)=='started': continue
|
||||
if row.get('failed'):
|
||||
reason,message,code=service_error(row)
|
||||
elif name not in states:
|
||||
reason,message,code='state_unavailable','Post-start service state could not be observed.',None
|
||||
else:
|
||||
reason,message,code='not_running_after_start','The service is not running at the post-start observation.',None
|
||||
failures.append({'name':name,'reason':reason,'message':message,'native_code':code})
|
||||
return dict(mode='check' if check else 'apply',initially_stopped=stopped,eligible=eligible,
|
||||
attempted=sorted(actual),excluded=sorted(set(stopped)-set(eligible)),newly_running=newly,
|
||||
still_stopped=still,unobserved=absent,failed_to_start=failures,
|
||||
started_count=len(set(newly)&set(actual)),failed_count=len(failures),before_count=len(stopped),
|
||||
after_observed=bool(after) or not before)
|
||||
|
||||
|
||||
def package_snapshot(raw, platform):
|
||||
"""Normalize package_facts (preferred) or legacy textual snapshots."""
|
||||
result={}
|
||||
if isinstance(raw, Mapping):
|
||||
for name, rows in raw.items():
|
||||
if not isinstance(rows, list): continue
|
||||
for row in rows:
|
||||
if not isinstance(row, Mapping): continue
|
||||
arch=str(row.get('arch') or 'unknown')
|
||||
version=str(row.get('version') or '')
|
||||
release=row.get('release')
|
||||
epoch=row.get('epoch')
|
||||
if release not in (None,''):
|
||||
version=f'{version}-{release}'
|
||||
if epoch not in (None,'','0',0):
|
||||
version=f'{epoch}:{version}'
|
||||
result.setdefault((str(name),arch),set()).add(version)
|
||||
return result
|
||||
for line in str(raw).splitlines():
|
||||
columns=line.split('\t')
|
||||
if len(columns)!=4: raise ValueError('Invalid package database record')
|
||||
name,arch,version,status=columns
|
||||
if platform=='debian' and status!='installed': continue
|
||||
result.setdefault((name,arch),set()).add(version)
|
||||
return result
|
||||
|
||||
|
||||
def _patch_reboot_fields(reboot_required, rebooted, preexisting, reboot_enabled, delay_minutes, blocked_reason=None):
|
||||
observed = None if reboot_required is None else bool(reboot_required)
|
||||
performed = bool(rebooted)
|
||||
final_required = False if performed else observed
|
||||
deferred = bool(final_required) and not bool(reboot_enabled)
|
||||
return dict(
|
||||
reboot_required=final_required,
|
||||
reboot_required_before=bool(preexisting),
|
||||
reboot_required_after=final_required,
|
||||
reboot_performed=performed,
|
||||
reboot_deferred=deferred,
|
||||
reboot_delay_minutes=int(delay_minutes),
|
||||
blocked_reason=blocked_reason,
|
||||
)
|
||||
|
||||
|
||||
def _reboot_reasons(value):
|
||||
rows=value if isinstance(value,list) else []
|
||||
out=[]
|
||||
for row in rows:
|
||||
if not isinstance(row,Mapping): continue
|
||||
source=str(row.get('source','unknown'))[:128]
|
||||
desc=str(row.get('description',''))[:512]
|
||||
out.append(dict(source=source,description=desc))
|
||||
return out[:32]
|
||||
|
||||
|
||||
def patch_blocked(platform, check=False, delay_minutes=0, rescan_after_reboot=False, reboot_reasons=()):
|
||||
result=dict(platform=str(platform), mode='check' if check else 'apply',
|
||||
evidence='preflight_reboot_state', complete=False, updates=[], updated_count=0,
|
||||
installed_count=0, removed_count=0, pending=[], failed_updates=[])
|
||||
result.update(_patch_reboot_fields(True, False, True, False, delay_minutes,
|
||||
'preexisting_reboot_required'))
|
||||
if str(platform) == 'windows':
|
||||
result.update(rescan_after_reboot=bool(rescan_after_reboot), patch_cycles=0,
|
||||
continuation_required=True, remaining_updates_known=False,
|
||||
reboot_reasons_before=_reboot_reasons(reboot_reasons))
|
||||
return result
|
||||
|
||||
|
||||
def patch_linux(before, after, platform, check=False, complete=True, reboot_required=None, rebooted=False,
|
||||
preexisting=False, reboot_enabled=True, delay_minutes=0):
|
||||
old,new=package_snapshot(before,platform),package_snapshot(after,platform)
|
||||
updates=[]
|
||||
if not check:
|
||||
for name,arch in sorted(set(old)|set(new)):
|
||||
a,b=old.get((name,arch),set()),new.get((name,arch),set())
|
||||
if a==b: continue
|
||||
updates.append(dict(name=name,identifier=None,architecture=arch,old_versions=sorted(a),new_versions=sorted(b),
|
||||
action='updated' if a and b else 'installed' if b else 'removed',kb=[]))
|
||||
result=dict(platform=platform,mode='check' if check else 'apply',evidence='package_snapshots',complete=bool(complete),
|
||||
updates=updates,updated_count=sum(x['action']=='updated' for x in updates),
|
||||
installed_count=sum(x['action']=='installed' for x in updates),removed_count=sum(x['action']=='removed' for x in updates),
|
||||
pending=[],failed_updates=[])
|
||||
result.update(_patch_reboot_fields(reboot_required, rebooted, preexisting, reboot_enabled, delay_minutes))
|
||||
return result
|
||||
|
||||
|
||||
|
||||
def _hresult_u32(code):
|
||||
if type(code) is not int: return None
|
||||
return code & 0xffffffff
|
||||
|
||||
|
||||
WINDOWS_UPDATE_FAILURES={
|
||||
0x80240009:('operation_in_progress','Another conflicting Windows Update operation is in progress.'),
|
||||
0x80240016:('install_not_allowed','Windows Update could not install this update because another installation was active or a mandatory reboot was pending.'),
|
||||
0x80240017:('not_applicable','The update is no longer applicable to this host.'),
|
||||
0x80240019:('exclusive_install_conflict','Windows Update reported an exclusive-install conflict.'),
|
||||
0x8024001B:('self_update_in_progress','The Windows Update Agent is updating itself.'),
|
||||
0x8024001F:('no_connection','Windows Update could not complete because its network connection was unavailable.'),
|
||||
0x80240021:('timeout','Windows Update did not complete before its operation timeout.'),
|
||||
}
|
||||
|
||||
|
||||
def _windows_failure(code):
|
||||
normalized=_hresult_u32(code)
|
||||
reason,message=WINDOWS_UPDATE_FAILURES.get(normalized,('update_failed','Windows Update failed to install this update.'))
|
||||
return normalized,reason,message
|
||||
|
||||
|
||||
def windows_update_result_failed(value):
|
||||
value=value if isinstance(value,Mapping) else {}
|
||||
if value.get('failed') is True: return True
|
||||
if int(value.get('failed_update_count',0) or 0)>0: return True
|
||||
return any(isinstance(row,Mapping) and 'failure_hresult_code' in row for row in value.get('updates',{}).values())
|
||||
|
||||
|
||||
def windows_update_block_reason(value):
|
||||
value=value if isinstance(value,Mapping) else {}
|
||||
for row in value.get('updates',{}).values():
|
||||
if isinstance(row,Mapping) and 'failure_hresult_code' in row:
|
||||
return _windows_failure(row.get('failure_hresult_code'))[1]
|
||||
# ansible_failed_result can carry only a generic module failure. Keep this bounded.
|
||||
return 'update_failed'
|
||||
|
||||
|
||||
def _windows_pending_from_search(value):
|
||||
value=value if isinstance(value,Mapping) else {}
|
||||
pending=[]
|
||||
for ident,row in value.get('updates',{}).items():
|
||||
if not isinstance(row,Mapping): continue
|
||||
pending.append(dict(name=str(row.get('title',ident)),identifier=str(ident),
|
||||
kb=[str(x) for x in row.get('kb',[])]))
|
||||
return pending
|
||||
|
||||
|
||||
def patch_windows_runs(runs, searches=(), check=False, preexisting=False, rebooted=False,
|
||||
reboot_enabled=True, delay_minutes=0, rescan_after_reboot=False,
|
||||
patch_cycles=0, continuation_required=False, remaining_updates_known=False,
|
||||
reboot_deferred=False, reboot_required_after=False, blocked_reason=None,
|
||||
complete_override=True, reboot_reasons_before=()):
|
||||
runs=runs if isinstance(runs,list) else []
|
||||
searches=searches if isinstance(searches,list) else []
|
||||
installed={}; failed={}
|
||||
for entry in runs:
|
||||
if not isinstance(entry,Mapping): continue
|
||||
requested=entry.get('requested',{}) if isinstance(entry.get('requested'),Mapping) else {}
|
||||
value=entry.get('result',{}) if isinstance(entry.get('result'),Mapping) else {}
|
||||
rows=value.get('updates',{}) if isinstance(value.get('updates'),Mapping) else {}
|
||||
if not rows and entry.get('task_failed'):
|
||||
wave=int(entry.get('wave',0) or 0)
|
||||
ident=str(requested.get('identifier') or ('wave-%s' % wave if wave else 'windows-update-wave'))
|
||||
failed[ident]=dict(name=str(requested.get('title') or 'Windows Update patch wave'),identifier=ident,
|
||||
native_code=None,native_code_hex=None,reason='update_failed',
|
||||
message='Windows Update failed before per-update failure details were available.')
|
||||
for ident,row in rows.items():
|
||||
if not isinstance(row,Mapping): continue
|
||||
ident=str(ident); name=str(row.get('title',requested.get('title',ident)))
|
||||
kb=[str(x) for x in row.get('kb',requested.get('kb',[]))]
|
||||
if row.get('installed') is True and not check:
|
||||
installed[ident]=dict(name=name,identifier=ident,architecture=None,old_versions=[],new_versions=[],
|
||||
action='updated',kb=kb)
|
||||
failed.pop(ident,None)
|
||||
if 'failure_hresult_code' in row:
|
||||
code,reason,message=_windows_failure(row.get('failure_hresult_code'))
|
||||
failed[ident]=dict(name=name,identifier=ident,native_code=code,
|
||||
native_code_hex=(f'0x{code:08X}' if code is not None else None),
|
||||
reason=reason,message=message)
|
||||
pending=[]
|
||||
if remaining_updates_known and searches:
|
||||
pending=_windows_pending_from_search(searches[-1])
|
||||
# A final search is authoritative for remaining applicability; do not duplicate
|
||||
# updates that it says are no longer pending.
|
||||
final_required=bool(reboot_required_after)
|
||||
performed=bool(rebooted)
|
||||
deferred=bool(reboot_deferred) or (final_required and not bool(reboot_enabled))
|
||||
complete=bool(complete_override) and not bool(failed)
|
||||
result=dict(platform='windows',mode='check' if check else 'apply',evidence='windows_update_result',
|
||||
complete=complete,updates=list(installed.values()),updated_count=len(installed),installed_count=0,
|
||||
removed_count=0,pending=pending,failed_updates=list(failed.values()),
|
||||
reboot_required=final_required,reboot_required_before=bool(preexisting),
|
||||
reboot_required_after=final_required,reboot_performed=performed,reboot_deferred=deferred,
|
||||
reboot_delay_minutes=int(delay_minutes),blocked_reason=blocked_reason,
|
||||
rescan_after_reboot=bool(rescan_after_reboot),patch_cycles=int(patch_cycles),
|
||||
continuation_required=bool(continuation_required),remaining_updates_known=bool(remaining_updates_known),
|
||||
reboot_reasons_before=_reboot_reasons(reboot_reasons_before))
|
||||
return result
|
||||
|
||||
def cleanup(paths, directory, removed, unifi, unifi_result, enabled=False, check=False):
|
||||
deleted=[r.get('item') for r in removed.get('results',[]) if r.get('changed') and not check]
|
||||
mode='check' if check else 'apply' if enabled else 'preview'
|
||||
state='retained'
|
||||
if unifi=='disabled':
|
||||
state='candidate' if not enabled or check else 'removed' if unifi_result.get('changed') else 'unchanged'
|
||||
return dict(mode=mode,directory=directory,candidates=list(paths),removed=deleted,
|
||||
unifi_configuration=state,complete=True)
|
||||
|
||||
|
||||
_SECRET=re.compile(r'password|passwd|passphrase|secret|token|credential|private.?key|authorization|community',re.I)
|
||||
_COMMAND={'cmd_line','command','command_line','arguments','args','environment','env','passphrase'}
|
||||
|
||||
def checkmk_config(value):
|
||||
"""Read only the named user config; redact secret/command fields before publication."""
|
||||
import yaml
|
||||
path=value['path']
|
||||
if re.split(r'[\\/]',path)[-1].lower()!='check_mk.user.yml':
|
||||
raise ValueError('Only check_mk.user.yml can be published')
|
||||
content=value.get('content','')
|
||||
if len(content.encode('utf-8'))>512*1024:
|
||||
raise ValueError('Checkmk user configuration exceeds report limit')
|
||||
data=yaml.safe_load(content) if value.get('exists') else {}
|
||||
if data is None: data={}
|
||||
if not isinstance(data,dict): raise ValueError('Checkmk configuration must be a mapping')
|
||||
redactions=[]; seen=set(); budget=[0]
|
||||
def walk(item,path,depth=0):
|
||||
budget[0]+=1
|
||||
if depth>18 or budget[0]>50000: raise ValueError('Configuration structure exceeds limit')
|
||||
if isinstance(item,(dict,list)):
|
||||
if id(item) in seen: raise ValueError('Recursive configuration aliases are unsupported')
|
||||
seen.add(id(item))
|
||||
try:
|
||||
if isinstance(item,list): return [walk(x,path+[str(i)],depth+1) for i,x in enumerate(item)]
|
||||
out={}
|
||||
for key,val in item.items():
|
||||
if not isinstance(key,str): key=str(key)
|
||||
here=path+[key]
|
||||
if _SECRET.search(key) or key.lower() in _COMMAND or (path and path[0].lower()=='mrpe' and key.lower() in ('config','entries')):
|
||||
out[key]='[REDACTED]'; redactions.append('.'.join(here)); continue
|
||||
out[key]=walk(val,here,depth+1)
|
||||
return out
|
||||
finally:seen.remove(id(item))
|
||||
if isinstance(item,str):
|
||||
# Do not expose URL userinfo, inline password assignment, PEM key bodies.
|
||||
if re.search(r'://[^/\s]+:[^/\s]+@|(?:password|passwd|token|secret)\s*[=:]|-----BEGIN .*PRIVATE KEY',item,re.I):
|
||||
redactions.append('.'.join(path)); return '[REDACTED]'
|
||||
# Multiline operational strings are represented by spaces, not terminal controls.
|
||||
return ' '.join(item.splitlines())
|
||||
if item is None or type(item) in (bool,int,float): return item
|
||||
return str(item)
|
||||
return dict(path=path,exists=bool(value.get('exists')),size_bytes=int(value.get('size_bytes',0)),
|
||||
last_write_time_utc=value.get('last_write_time_utc'),sections=walk(data,[]),
|
||||
redacted_paths=redactions,comment_preservation='not_in_structured_output')
|
||||
|
||||
|
||||
def checkmk_changes(copies, selected, plugin_update, unifi_result, opposite, mode, check=False):
|
||||
changed_checks=[r['item']['filename'] for r in copies.get('results',[]) if r.get('changed') and isinstance(r.get('item'),Mapping)]
|
||||
removed=[]
|
||||
if opposite.get('changed') and mode in ('os','network'):
|
||||
removed=['check_unifi-controller.sh' if mode=='os' else 'check_unifi-os.sh']
|
||||
changes=[dict(component='check',name=n,action='updated') for n in changed_checks]
|
||||
changes += [dict(component='check',name=n,action='removed') for n in removed]
|
||||
if plugin_update.get('changed'):changes.append(dict(component='section',name='plugins',action='updated'))
|
||||
if unifi_result.get('changed'):changes.append(dict(component='configuration',name='unifi.cfg',action='updated'))
|
||||
return dict(mode='check' if check else 'apply',changed=bool(changes),managed_sections=['plugins'] if plugin_update else [],
|
||||
changes=changes,deployed_checks=[s['filename'] for s in selected],changed_checks=changed_checks,
|
||||
removed_checks=removed if not check else [],unknown_files_policy='untouched_not_enumerated')
|
||||
|
||||
|
||||
def checkmk_state(observed, service_rows, change_report, package_changed=False, check=False):
|
||||
return dict(mode='check' if check else 'apply',installed=observed.get('installed'),version=observed.get('version'),
|
||||
version_source=observed.get('version_source','unavailable'),
|
||||
services=[dict(name=s['name'],state=s.get('state','unknown')) for s in service_rows],
|
||||
package_changed=bool(package_changed),configuration_updated=any(c['component'] in ('section','configuration') for c in change_report['changes']),
|
||||
checks_deployed=change_report['deployed_checks'],changed_checks=change_report['changed_checks'],removed_checks=change_report['removed_checks'])
|
||||
|
||||
|
||||
class FilterModule:
|
||||
def filters(self):
|
||||
return {'aim_epoch_iso_utc':epoch_iso_utc,'aim_report_capabilities':capabilities,'aim_report_disks':disks,'aim_report_services':services,
|
||||
'aim_report_patch_linux':patch_linux,'aim_report_patch_windows_runs':patch_windows_runs,
|
||||
'aim_report_patch_blocked':patch_blocked,
|
||||
'aim_windows_update_result_failed':windows_update_result_failed,
|
||||
'aim_windows_update_block_reason':windows_update_block_reason,
|
||||
'aim_report_cleanup':cleanup,'aim_report_checkmk_config':checkmk_config,
|
||||
'aim_report_checkmk_changes':checkmk_changes,'aim_report_checkmk_state':checkmk_state}
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
# PURPOSE: Export Windows event logs
|
||||
# DESCRIPTION: Export selected event channels to EVTX files on the target; existing event logs are not cleared.
|
||||
# TARGETS: windows
|
||||
# INPUTS (omitted values inherit inventory / role defaults):
|
||||
# aim_debug [bool]: false
|
||||
# event_age_days [int]: 45
|
||||
# export_folder [text]: C:\Logs
|
||||
# event_log_channels [list]: Application, Security, System, Setup
|
||||
# AUTH: existing inventory / Vault credentials; no embedded passwords.
|
||||
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
|
||||
# playbooks/maintenance_export_event_logs.yml --limit <host> --vault-id <customer>@prompt
|
||||
- name: Maintenance | Export Windows event logs
|
||||
hosts: windows
|
||||
gather_facts: false
|
||||
pre_tasks:
|
||||
- name: AIM | Validate diagnostics option
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
|
||||
'false']
|
||||
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||
quiet: true
|
||||
- name: AIM | Reject mixed platform membership
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
|
||||
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
|
||||
quiet: true
|
||||
- name: AIM | Execution context
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
host: '{{ inventory_hostname }}'
|
||||
diagnostics: Enabled; secret values are never included by this task.
|
||||
when: aim_debug | default(false) | bool
|
||||
roles:
|
||||
- role: maintenance_export_event_logs
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
# PURPOSE: Patch operating systems
|
||||
# DESCRIPTION: Apply updates on Windows, Debian and RedHat-family systems; optionally reboot when required.
|
||||
# TARGETS: linux, windows
|
||||
# INPUTS (omitted values inherit inventory / role defaults):
|
||||
# aim_debug [bool]: false
|
||||
# os_patching_reboot [bool]: true
|
||||
# os_patching_windows_categories [list]: SecurityUpdates, CriticalUpdates, UpdateRollups, DefinitionUpdates, Updates
|
||||
# os_patching_serial [serial]: 100%
|
||||
# os_patching_reboot_timeout [int]: 600
|
||||
# os_patching_reboot_delay_minutes [int]: 0
|
||||
# os_patching_reboot_message [text]: AIM maintenance: operating system patching requires a reboot.
|
||||
# os_patching_rescan_after_reboot [bool]: false (Windows only; continue with a newly discovered patch wave after reboot)
|
||||
# AUTH: existing inventory / Vault credentials; no embedded passwords.
|
||||
# CHANGES: Updates production operating systems. Optional AIM-initiated reboots notify logged-in users and honor the configured delay.
|
||||
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
|
||||
# playbooks/maintenance_patch_os.yml --limit <host> --vault-id <customer>@prompt
|
||||
- name: Maintenance | Patch Linux
|
||||
hosts: linux
|
||||
gather_facts: true
|
||||
pre_tasks:
|
||||
- name: AIM | Validate diagnostics option
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
|
||||
'false']
|
||||
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||
quiet: true
|
||||
- name: AIM | Reject mixed platform membership
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
|
||||
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
|
||||
quiet: true
|
||||
- name: AIM | Execution context
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
host: '{{ inventory_hostname }}'
|
||||
diagnostics: Enabled; secret values are never included by this task.
|
||||
when: aim_debug | default(false) | bool
|
||||
become: true
|
||||
serial: '{{ os_patching_serial | default(''100%'') }}'
|
||||
roles:
|
||||
- role: maintenance_patch_os
|
||||
- name: Maintenance | Patch Windows
|
||||
hosts: windows
|
||||
gather_facts: true
|
||||
pre_tasks:
|
||||
- name: AIM | Validate diagnostics option
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
|
||||
'false']
|
||||
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||
quiet: true
|
||||
- name: AIM | Reject mixed platform membership
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
|
||||
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
|
||||
quiet: true
|
||||
- name: AIM | Execution context
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
host: '{{ inventory_hostname }}'
|
||||
diagnostics: Enabled; secret values are never included by this task.
|
||||
when: aim_debug | default(false) | bool
|
||||
serial: '{{ os_patching_serial | default(''100%'') }}'
|
||||
roles:
|
||||
- role: maintenance_patch_os
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
# PURPOSE: Reboot hosts
|
||||
# DESCRIPTION: Reboot selected hosts in batches and wait for management connectivity.
|
||||
# TARGETS: linux, windows
|
||||
# INPUTS (omitted values inherit inventory / role defaults):
|
||||
# aim_debug [bool]: false
|
||||
# maintenance_reboot_serial [serial]: 10
|
||||
# maintenance_reboot_timeout [int]: 1800
|
||||
# maintenance_reboot_message [text]: Reboot initiated by debsansible01.bfmiglabor.lan (Ansible)
|
||||
# maintenance_reboot_pre_delay [int]: 0
|
||||
# maintenance_reboot_post_delay [int]: 15
|
||||
# AUTH: existing inventory / Vault credentials; no embedded passwords.
|
||||
# CHANGES: Every selected host will be rebooted. No reboot occurs before final confirmation.
|
||||
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
|
||||
# playbooks/maintenance_reboot_hosts.yml --limit <host> --vault-id <customer>@prompt
|
||||
- name: Maintenance | Reboot Linux
|
||||
hosts: linux
|
||||
gather_facts: true
|
||||
pre_tasks:
|
||||
- name: AIM | Validate diagnostics option
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
|
||||
'false']
|
||||
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||
quiet: true
|
||||
- name: AIM | Reject mixed platform membership
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
|
||||
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
|
||||
quiet: true
|
||||
- name: AIM | Execution context
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
host: '{{ inventory_hostname }}'
|
||||
diagnostics: Enabled; secret values are never included by this task.
|
||||
when: aim_debug | default(false) | bool
|
||||
become: true
|
||||
serial: '{{ maintenance_reboot_serial | default(10) }}'
|
||||
roles:
|
||||
- role: maintenance_reboot_hosts
|
||||
- name: Maintenance | Reboot Windows
|
||||
hosts: windows
|
||||
gather_facts: false
|
||||
pre_tasks:
|
||||
- name: AIM | Validate diagnostics option
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
|
||||
'false']
|
||||
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||
quiet: true
|
||||
- name: AIM | Reject mixed platform membership
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
|
||||
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
|
||||
quiet: true
|
||||
- name: AIM | Execution context
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
host: '{{ inventory_hostname }}'
|
||||
diagnostics: Enabled; secret values are never included by this task.
|
||||
when: aim_debug | default(false) | bool
|
||||
serial: '{{ maintenance_reboot_serial | default(10) }}'
|
||||
roles:
|
||||
- role: maintenance_reboot_hosts
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
# PURPOSE: Start stopped automatic services
|
||||
# DESCRIPTION: Start eligible stopped services, apply optional include/exclude lists and report partial failures.
|
||||
# TARGETS: windows
|
||||
# INPUTS (omitted values inherit inventory / role defaults):
|
||||
# aim_debug [bool]: false
|
||||
# maintenance_service_include [list]: []
|
||||
# maintenance_service_exclude [list]: []
|
||||
# maintenance_service_fail_on_error [bool]: true
|
||||
# AUTH: existing inventory / Vault credentials; no embedded passwords.
|
||||
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
|
||||
# playbooks/maintenance_start_stopped_services.yml --limit <host> --vault-id <customer>@prompt
|
||||
- name: Maintenance | Start automatic services
|
||||
hosts: windows
|
||||
gather_facts: false
|
||||
pre_tasks:
|
||||
- name: AIM | Validate diagnostics option
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
|
||||
'false']
|
||||
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||
quiet: true
|
||||
- name: AIM | Reject mixed platform membership
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
|
||||
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
|
||||
quiet: true
|
||||
- name: AIM | Execution context
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
host: '{{ inventory_hostname }}'
|
||||
diagnostics: Enabled; secret values are never included by this task.
|
||||
when: aim_debug | default(false) | bool
|
||||
roles:
|
||||
- role: maintenance_start_stopped_services
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
# PURPOSE: Apply bitformer pfSense baseline
|
||||
# DESCRIPTION: Apply the supplied pfSense baseline without changing its firewall/VPN policy.
|
||||
# TARGETS: pfsense
|
||||
# INPUTS (omitted values inherit inventory / role defaults):
|
||||
# aim_debug [bool]: false
|
||||
# AUTH: existing inventory / Vault credentials; no embedded passwords.
|
||||
# CHANGES: Contains the original any-source WAN management rule for ports 22/80/443. The original CA, VPN endpoint and client certificate reference are unchanged; verify them before execution. Requires separately approved pfsensible.core installation.
|
||||
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
|
||||
# playbooks/pfsense_apply_baseline.yml --limit <host> --vault-id <customer>@prompt
|
||||
- name: Install pfSense sudo package
|
||||
hosts: pfsense
|
||||
tasks:
|
||||
- name: Apply supplied firewall policy
|
||||
ansible.builtin.import_role:
|
||||
name: pfsense_install_prerequisites
|
||||
pre_tasks:
|
||||
- name: AIM | Validate diagnostics option
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
|
||||
'false']
|
||||
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||
quiet: true
|
||||
- name: AIM | Reject mixed platform membership
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
|
||||
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
|
||||
quiet: true
|
||||
- name: AIM | Execution context
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
host: '{{ inventory_hostname }}'
|
||||
diagnostics: Enabled; secret values are never included by this task.
|
||||
when: aim_debug | default(false) | bool
|
||||
- name: Initial pfSense bitformer config
|
||||
hosts: pfsense
|
||||
become: true
|
||||
tasks:
|
||||
- name: Apply supplied firewall policy
|
||||
ansible.builtin.import_role:
|
||||
name: pfsense_apply_baseline
|
||||
pre_tasks:
|
||||
- name: AIM | Validate diagnostics option
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
|
||||
'false']
|
||||
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||
quiet: true
|
||||
- name: AIM | Reject mixed platform membership
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
|
||||
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
|
||||
quiet: true
|
||||
- name: AIM | Execution context
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
host: '{{ inventory_hostname }}'
|
||||
diagnostics: Enabled; secret values are never included by this task.
|
||||
when: aim_debug | default(false) | bool
|
||||
@@ -0,0 +1,77 @@
|
||||
# AIM public operation-data schema. Closed fields except explicitly redacted configuration sections.
|
||||
type: object
|
||||
properties:
|
||||
mode:
|
||||
type: string
|
||||
maxLength: 128
|
||||
enum:
|
||||
- apply
|
||||
- check
|
||||
changed:
|
||||
type: boolean
|
||||
managed_sections:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
maxItems: 20000
|
||||
changes:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
component:
|
||||
type: string
|
||||
maxLength: 128
|
||||
enum:
|
||||
- section
|
||||
- check
|
||||
- configuration
|
||||
name:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
action:
|
||||
type: string
|
||||
maxLength: 128
|
||||
enum:
|
||||
- updated
|
||||
- removed
|
||||
required:
|
||||
- component
|
||||
- name
|
||||
- action
|
||||
additionalProperties: false
|
||||
maxItems: 20000
|
||||
deployed_checks:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
maxItems: 20000
|
||||
changed_checks:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
maxItems: 20000
|
||||
removed_checks:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
maxItems: 20000
|
||||
unknown_files_policy:
|
||||
type: string
|
||||
maxLength: 128
|
||||
enum:
|
||||
- untouched_not_enumerated
|
||||
required:
|
||||
- mode
|
||||
- changed
|
||||
- managed_sections
|
||||
- changes
|
||||
- deployed_checks
|
||||
- changed_checks
|
||||
- removed_checks
|
||||
- unknown_files_policy
|
||||
additionalProperties: false
|
||||
@@ -0,0 +1,74 @@
|
||||
# AIM public operation-data schema. Closed fields except explicitly redacted configuration sections.
|
||||
type: object
|
||||
properties:
|
||||
mode:
|
||||
type: string
|
||||
maxLength: 128
|
||||
enum:
|
||||
- apply
|
||||
- check
|
||||
installed:
|
||||
type:
|
||||
- boolean
|
||||
- 'null'
|
||||
version:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
maxLength: 1024
|
||||
version_source:
|
||||
type: string
|
||||
maxLength: 128
|
||||
enum:
|
||||
- registry
|
||||
- package_database
|
||||
- unavailable
|
||||
services:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
state:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
required:
|
||||
- name
|
||||
- state
|
||||
additionalProperties: false
|
||||
maxItems: 20000
|
||||
package_changed: &id001
|
||||
type: boolean
|
||||
configuration_updated: *id001
|
||||
checks_deployed:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
maxItems: 20000
|
||||
changed_checks:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
maxItems: 20000
|
||||
removed_checks:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
maxItems: 20000
|
||||
required:
|
||||
- mode
|
||||
- installed
|
||||
- version
|
||||
- version_source
|
||||
- services
|
||||
- package_changed
|
||||
- configuration_updated
|
||||
- checks_deployed
|
||||
- changed_checks
|
||||
- removed_checks
|
||||
additionalProperties: false
|
||||
@@ -0,0 +1,40 @@
|
||||
# AIM public operation-data schema. Closed fields except explicitly redacted configuration sections.
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
exists:
|
||||
type: boolean
|
||||
size_bytes:
|
||||
type: integer
|
||||
minimum: 0
|
||||
last_write_time_utc:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
maxLength: 1024
|
||||
sections:
|
||||
type: object
|
||||
properties: {}
|
||||
additionalProperties: true
|
||||
redacted_paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
maxItems: 20000
|
||||
comment_preservation:
|
||||
type: string
|
||||
maxLength: 128
|
||||
enum:
|
||||
- not_in_structured_output
|
||||
required:
|
||||
- path
|
||||
- exists
|
||||
- size_bytes
|
||||
- last_write_time_utc
|
||||
- sections
|
||||
- redacted_paths
|
||||
- comment_preservation
|
||||
additionalProperties: false
|
||||
@@ -0,0 +1,30 @@
|
||||
# AIM public operation-data schema. Closed fields except explicitly redacted configuration sections.
|
||||
type: object
|
||||
properties:
|
||||
mode:
|
||||
type: string
|
||||
maxLength: 128
|
||||
enum:
|
||||
- apply
|
||||
- check
|
||||
days:
|
||||
type: integer
|
||||
minimum: 0
|
||||
files:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
maxItems: 20000
|
||||
channels:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
maxItems: 20000
|
||||
required:
|
||||
- mode
|
||||
- days
|
||||
- files
|
||||
- channels
|
||||
additionalProperties: false
|
||||
@@ -0,0 +1,66 @@
|
||||
# AIM public operation-data schema. Closed fields except explicitly redacted configuration sections.
|
||||
type: object
|
||||
properties:
|
||||
platform:
|
||||
type: string
|
||||
maxLength: 128
|
||||
enum:
|
||||
- windows
|
||||
- linux
|
||||
filesystems:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
mount:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
filesystem_type:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
maxLength: 1024
|
||||
used_bytes:
|
||||
type:
|
||||
- integer
|
||||
- 'null'
|
||||
minimum: 0
|
||||
total_bytes:
|
||||
type:
|
||||
- integer
|
||||
- 'null'
|
||||
minimum: 0
|
||||
available_bytes:
|
||||
type:
|
||||
- integer
|
||||
- 'null'
|
||||
minimum: 0
|
||||
used_percent:
|
||||
type:
|
||||
- number
|
||||
- 'null'
|
||||
minimum: 0
|
||||
status:
|
||||
type: string
|
||||
maxLength: 128
|
||||
enum:
|
||||
- available
|
||||
- unavailable
|
||||
required:
|
||||
- name
|
||||
- mount
|
||||
- filesystem_type
|
||||
- used_bytes
|
||||
- total_bytes
|
||||
- available_bytes
|
||||
- used_percent
|
||||
- status
|
||||
additionalProperties: false
|
||||
maxItems: 20000
|
||||
required:
|
||||
- platform
|
||||
- filesystems
|
||||
additionalProperties: false
|
||||
@@ -0,0 +1,22 @@
|
||||
# AIM public operation-data schema. Closed fields except explicitly redacted configuration sections.
|
||||
type: object
|
||||
properties:
|
||||
is_dc: &id001
|
||||
type: boolean
|
||||
is_dhcp_server: *id001
|
||||
is_hyperv_host: *id001
|
||||
has_veeam_vbr: *id001
|
||||
has_veeam_vbo: *id001
|
||||
has_veeam_em: *id001
|
||||
is_unifi_controller: *id001
|
||||
is_unifi_os_server: *id001
|
||||
required:
|
||||
- is_dc
|
||||
- is_dhcp_server
|
||||
- is_hyperv_host
|
||||
- has_veeam_vbr
|
||||
- has_veeam_vbo
|
||||
- has_veeam_em
|
||||
- is_unifi_controller
|
||||
- is_unifi_os_server
|
||||
additionalProperties: false
|
||||
@@ -0,0 +1,43 @@
|
||||
# AIM public operation-data schema. Closed fields except explicitly redacted configuration sections.
|
||||
type: object
|
||||
properties:
|
||||
mode:
|
||||
type: string
|
||||
maxLength: 128
|
||||
enum:
|
||||
- preview
|
||||
- apply
|
||||
- check
|
||||
directory:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
candidates:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
maxItems: 20000
|
||||
removed:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
maxItems: 20000
|
||||
unifi_configuration:
|
||||
type: string
|
||||
maxLength: 128
|
||||
enum:
|
||||
- retained
|
||||
- candidate
|
||||
- removed
|
||||
- unchanged
|
||||
complete:
|
||||
type: boolean
|
||||
required:
|
||||
- mode
|
||||
- directory
|
||||
- candidates
|
||||
- removed
|
||||
- unifi_configuration
|
||||
- complete
|
||||
additionalProperties: false
|
||||
@@ -0,0 +1,190 @@
|
||||
# AIM public operation-data schema. Closed fields; no raw package-manager/module output.
|
||||
type: object
|
||||
properties:
|
||||
platform:
|
||||
type: string
|
||||
maxLength: 128
|
||||
enum:
|
||||
- windows
|
||||
- debian
|
||||
- redhat
|
||||
mode:
|
||||
type: string
|
||||
maxLength: 128
|
||||
enum:
|
||||
- apply
|
||||
- check
|
||||
evidence:
|
||||
type: string
|
||||
maxLength: 128
|
||||
enum:
|
||||
- package_snapshots
|
||||
- windows_update_result
|
||||
- preflight_reboot_state
|
||||
complete:
|
||||
type: boolean
|
||||
updates:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
identifier:
|
||||
type: [string, 'null']
|
||||
maxLength: 1024
|
||||
architecture:
|
||||
type: [string, 'null']
|
||||
maxLength: 1024
|
||||
old_versions:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
maxItems: 100
|
||||
new_versions:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
maxItems: 100
|
||||
action:
|
||||
type: string
|
||||
maxLength: 128
|
||||
enum: [updated, installed, removed]
|
||||
kb:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
maxItems: 100
|
||||
required: [name, identifier, architecture, old_versions, new_versions, action, kb]
|
||||
additionalProperties: false
|
||||
maxItems: 20000
|
||||
updated_count:
|
||||
type: integer
|
||||
minimum: 0
|
||||
installed_count:
|
||||
type: integer
|
||||
minimum: 0
|
||||
removed_count:
|
||||
type: integer
|
||||
minimum: 0
|
||||
pending:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
identifier:
|
||||
type: [string, 'null']
|
||||
maxLength: 1024
|
||||
kb:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
maxItems: 100
|
||||
required: [name, identifier, kb]
|
||||
additionalProperties: false
|
||||
maxItems: 20000
|
||||
failed_updates:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
identifier:
|
||||
type: [string, 'null']
|
||||
maxLength: 1024
|
||||
native_code:
|
||||
type: [integer, 'null']
|
||||
minimum: 0
|
||||
native_code_hex:
|
||||
type: [string, 'null']
|
||||
maxLength: 32
|
||||
reason:
|
||||
type: string
|
||||
maxLength: 128
|
||||
enum: [update_failed, operation_in_progress, install_not_allowed, not_applicable, exclusive_install_conflict, self_update_in_progress, no_connection, timeout]
|
||||
message:
|
||||
type: string
|
||||
maxLength: 512
|
||||
required: [name, identifier, native_code, native_code_hex, reason, message]
|
||||
additionalProperties: false
|
||||
maxItems: 20000
|
||||
reboot_required:
|
||||
type: [boolean, 'null']
|
||||
description: Final observed/predicted pending reboot state after this run.
|
||||
reboot_required_before:
|
||||
type: boolean
|
||||
description: A pending reboot was detected before patching began.
|
||||
reboot_reasons_before:
|
||||
type: array
|
||||
description: Windows only. Native reboot sources reported by ansible.windows.win_reboot_info before patching.
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
source:
|
||||
type: string
|
||||
maxLength: 128
|
||||
description:
|
||||
type: string
|
||||
maxLength: 512
|
||||
required: [source, description]
|
||||
additionalProperties: false
|
||||
maxItems: 32
|
||||
reboot_required_after:
|
||||
type: [boolean, 'null']
|
||||
description: Final pending reboot state; false after an AIM-performed successful reboot.
|
||||
reboot_performed:
|
||||
type: boolean
|
||||
reboot_deferred:
|
||||
type: boolean
|
||||
description: A reboot remains required because automatic reboot was disabled.
|
||||
reboot_delay_minutes:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 1440
|
||||
blocked_reason:
|
||||
type: [string, 'null']
|
||||
maxLength: 128
|
||||
enum: [preexisting_reboot_required, update_failed, operation_in_progress, install_not_allowed, not_applicable, exclusive_install_conflict, self_update_in_progress, no_connection, timeout, cycle_limit_reached, null]
|
||||
rescan_after_reboot:
|
||||
type: boolean
|
||||
description: Windows only. True permits a newly discovered patch wave after an AIM-performed reboot.
|
||||
patch_cycles:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 12
|
||||
description: Windows discovery/install cycles entered by this run.
|
||||
continuation_required:
|
||||
type: boolean
|
||||
description: Another operator-approved patch run is recommended or required to continue patching.
|
||||
remaining_updates_known:
|
||||
type: boolean
|
||||
description: True only when the report contains an authoritative post-wave discovery in pending.
|
||||
required:
|
||||
- platform
|
||||
- mode
|
||||
- evidence
|
||||
- complete
|
||||
- updates
|
||||
- updated_count
|
||||
- installed_count
|
||||
- removed_count
|
||||
- pending
|
||||
- failed_updates
|
||||
- reboot_required
|
||||
- reboot_required_before
|
||||
- reboot_required_after
|
||||
- reboot_performed
|
||||
- reboot_deferred
|
||||
- reboot_delay_minutes
|
||||
- blocked_reason
|
||||
additionalProperties: false
|
||||
@@ -0,0 +1,113 @@
|
||||
# AIM public operation-data schema. Closed fields except explicitly redacted configuration sections.
|
||||
type: object
|
||||
properties:
|
||||
mode:
|
||||
type: string
|
||||
maxLength: 128
|
||||
enum:
|
||||
- apply
|
||||
- check
|
||||
initially_stopped:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
maxItems: 20000
|
||||
eligible:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
maxItems: 20000
|
||||
attempted:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
maxItems: 20000
|
||||
excluded:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
maxItems: 20000
|
||||
newly_running:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
maxItems: 20000
|
||||
still_stopped:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
maxItems: 20000
|
||||
unobserved:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
maxItems: 20000
|
||||
failed_to_start:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
reason:
|
||||
type: string
|
||||
maxLength: 128
|
||||
enum:
|
||||
- dependency_failed
|
||||
- permission_denied
|
||||
- service_disabled
|
||||
- start_timeout
|
||||
- service_not_found
|
||||
- logon_failed
|
||||
- start_failed
|
||||
- not_running_after_start
|
||||
- state_unavailable
|
||||
message:
|
||||
type: string
|
||||
maxLength: 1024
|
||||
native_code:
|
||||
type:
|
||||
- integer
|
||||
- 'null'
|
||||
minimum: 0
|
||||
required:
|
||||
- name
|
||||
- reason
|
||||
- message
|
||||
- native_code
|
||||
additionalProperties: false
|
||||
maxItems: 20000
|
||||
started_count:
|
||||
type: integer
|
||||
minimum: 0
|
||||
failed_count:
|
||||
type: integer
|
||||
minimum: 0
|
||||
before_count:
|
||||
type: integer
|
||||
minimum: 0
|
||||
after_observed:
|
||||
type: boolean
|
||||
required:
|
||||
- mode
|
||||
- initially_stopped
|
||||
- eligible
|
||||
- attempted
|
||||
- excluded
|
||||
- newly_running
|
||||
- still_stopped
|
||||
- unobserved
|
||||
- failed_to_start
|
||||
- started_count
|
||||
- failed_count
|
||||
- before_count
|
||||
- after_observed
|
||||
additionalProperties: false
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
# PURPOSE: Apply bitformer Sophos baseline
|
||||
# DESCRIPTION: Apply the supplied bitformer firewall baseline. Existing policy values and action order are preserved.
|
||||
# TARGETS: sophosxgs
|
||||
# INPUTS (omitted values inherit inventory / role defaults):
|
||||
# aim_debug [bool]: false
|
||||
# AUTH: existing inventory / Vault credentials; no embedded passwords.
|
||||
# CHANGES: Changes firewall management access, objects and rules, including rule removal and a final drop rule. Policy values have NOT been redesigned.
|
||||
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
|
||||
# playbooks/sophos_apply_baseline.yml --limit <host> --vault-id <customer>@prompt --ask-pass
|
||||
- name: Grundkonfiguration der Sophos-Firewall nach bitformer Standard
|
||||
hosts: sophosxgs
|
||||
gather_facts: false
|
||||
any_errors_fatal: false
|
||||
vars:
|
||||
network_hosts:
|
||||
- name: bf_spn_network
|
||||
network: 10.242.176.0
|
||||
subnetmask: 255.255.255.0
|
||||
- name: rfc_1918_a
|
||||
network: 10.0.0.0
|
||||
subnetmask: 255.0.0.0
|
||||
- name: rfc_1918_b
|
||||
network: 172.16.0.0
|
||||
subnetmask: 255.240.0.0
|
||||
- name: rfc_1918_c
|
||||
network: 192.168.0.0
|
||||
subnetmask: 255.255.0.0
|
||||
- name: rfc_5735
|
||||
network: 169.254.0.0
|
||||
subnetmask: 255.255.0.0
|
||||
firewall_rules_to_remove:
|
||||
- '[example] Traffic to Internal Zones'
|
||||
- '[example] Traffic to WAN'
|
||||
- '[example] Traffic to DMZ'
|
||||
wireless_networks_to_remove:
|
||||
- GuestAP
|
||||
- Sophos
|
||||
tasks:
|
||||
- name: Apply supplied firewall policy
|
||||
ansible.builtin.import_role:
|
||||
name: sophos_apply_baseline
|
||||
tasks_from: main
|
||||
pre_tasks:
|
||||
- name: AIM | Validate diagnostics option
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
|
||||
'false']
|
||||
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||
quiet: true
|
||||
- name: AIM | Reject mixed platform membership
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
|
||||
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
|
||||
quiet: true
|
||||
- name: AIM | Execution context
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
host: '{{ inventory_hostname }}'
|
||||
diagnostics: Enabled; secret values are never included by this task.
|
||||
when: aim_debug | default(false) | bool
|
||||
@@ -0,0 +1,2 @@
|
||||
# AIM canonical Ansible runtime. Separate from the AIM/add-on environments.
|
||||
ansible-core==2.19.11
|
||||
@@ -0,0 +1,10 @@
|
||||
# requirements.yml
|
||||
collections:
|
||||
- name: ansible.netcommon
|
||||
- name: ansible.windows
|
||||
version: ">=3.8.0,<4.0.0" # win_reboot_info baseline; compatible with ansible-core 2.19.11
|
||||
- name: ansible.posix
|
||||
- name: community.windows
|
||||
- name: community.general
|
||||
- name: sophos.sophos_firewall
|
||||
- name: pfsensible.core
|
||||
@@ -0,0 +1,23 @@
|
||||
# checkmk_agent
|
||||
|
||||
Operator defaults are intentionally low-precedence; inventory and explicit run options may override them.
|
||||
|
||||
```yaml
|
||||
---
|
||||
|
||||
checkmk_linux_tmp_path: /tmp
|
||||
checkmk_windows_tmp_path: C:\Windows\Temp
|
||||
checkmk_deb_filename: check-mk-agent.deb
|
||||
checkmk_rpm_filename: check-mk-agent.rpm
|
||||
checkmk_msi_filename: check_mk_agent.msi
|
||||
checkmk_agent_files_dir: '{{ lookup(''ansible.builtin.env'', ''AIM_CHECKMK_AGENT_FILES_DIR'') | default(role_path
|
||||
~ ''/files'', true) }}'
|
||||
```
|
||||
|
||||
## Structured result integration
|
||||
|
||||
Current reporting behavior and field semantics are specified in
|
||||
[scripts/docs/OPERATION_RESULTS.md](../../scripts/docs/OPERATION_RESULTS.md).
|
||||
The calling catalog playbook owns publication; helper roles do not implicitly export
|
||||
arbitrary facts, module results or debug data. Existing defaults above retain their
|
||||
precedence. See the current validation/sanity documents before using the new candidate.
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
|
||||
checkmk_linux_tmp_path: /tmp
|
||||
checkmk_windows_tmp_path: C:\Windows\Temp
|
||||
checkmk_deb_filename: check-mk-agent.deb
|
||||
checkmk_rpm_filename: check-mk-agent.rpm
|
||||
checkmk_msi_filename: check_mk_agent.msi
|
||||
checkmk_agent_files_dir: '{{ lookup(''ansible.builtin.env'', ''AIM_CHECKMK_AGENT_FILES_DIR'') | default(role_path
|
||||
~ ''/files'', true) }}'
|
||||
@@ -0,0 +1,3 @@
|
||||
# Staged agent packages
|
||||
|
||||
AIM Maintenance populates `check_mk_agent.msi`, `check-mk-agent.deb` and `check-mk-agent.rpm` here by default. Binaries are not included or replaced by this source bundle. Existing package files must be retained.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,25 @@
|
||||
---
|
||||
|
||||
- name: Checkmk | Verify staged package on controller
|
||||
ansible.builtin.stat:
|
||||
path: '{{ checkmk_agent_files_dir }}/{{ checkmk_deb_filename }}'
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
register: _checkmk_package
|
||||
- name: Checkmk | Require staged package
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- _checkmk_package.stat.isreg | default(false)
|
||||
- _checkmk_package.stat.size | default(0) | int > 0
|
||||
fail_msg: Required agent package is absent/empty. Use AIM Maintenance to download packages first.
|
||||
quiet: true
|
||||
- name: Debian | Copy Checkmk agent package from role files
|
||||
ansible.builtin.copy:
|
||||
src: '{{ checkmk_agent_files_dir }}/{{ checkmk_deb_filename }}'
|
||||
dest: '{{ checkmk_linux_tmp_path }}/{{ checkmk_deb_filename }}'
|
||||
mode: '0644'
|
||||
- name: Debian | Install Checkmk agent from local .deb
|
||||
ansible.builtin.apt:
|
||||
deb: '{{ checkmk_linux_tmp_path }}/{{ checkmk_deb_filename }}'
|
||||
state: present
|
||||
register: _checkmk_package_install
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
|
||||
- name: Checkmk | Verify staged package on controller
|
||||
ansible.builtin.stat:
|
||||
path: '{{ checkmk_agent_files_dir }}/{{ checkmk_rpm_filename }}'
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
register: _checkmk_package
|
||||
- name: Checkmk | Require staged package
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- _checkmk_package.stat.isreg | default(false)
|
||||
- _checkmk_package.stat.size | default(0) | int > 0
|
||||
fail_msg: Required agent package is absent/empty. Use AIM Maintenance to download packages first.
|
||||
quiet: true
|
||||
- name: RedHat | Copy Checkmk agent package from role files
|
||||
ansible.builtin.copy:
|
||||
src: '{{ checkmk_agent_files_dir }}/{{ checkmk_rpm_filename }}'
|
||||
dest: '{{ checkmk_linux_tmp_path }}/{{ checkmk_rpm_filename }}'
|
||||
mode: '0644'
|
||||
- name: RedHat | Install Checkmk agent from local .rpm
|
||||
ansible.builtin.dnf:
|
||||
name: '{{ checkmk_linux_tmp_path }}/{{ checkmk_rpm_filename }}'
|
||||
state: present
|
||||
register: _checkmk_package_install
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
|
||||
- name: Checkmk | Supported installer
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ansible_facts.os_family in ['Debian','RedHat','Windows']
|
||||
fail_msg: Unsupported installer platform. No Python/package-manager bootstrap is performed.
|
||||
quiet: true
|
||||
- name: Include Debian tasks
|
||||
ansible.builtin.include_tasks: linux_debian.yml
|
||||
when: ansible_facts['os_family'] == "Debian"
|
||||
- name: Include RedHat tasks
|
||||
ansible.builtin.include_tasks: linux_redhat.yml
|
||||
when: ansible_facts['os_family'] == "RedHat"
|
||||
- name: Include Windows tasks
|
||||
ansible.builtin.include_tasks: windows.yml
|
||||
when: ansible_facts['os_family'] == "Windows"
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
|
||||
- name: Checkmk | Verify staged package on controller
|
||||
ansible.builtin.stat:
|
||||
path: '{{ checkmk_agent_files_dir }}/{{ checkmk_msi_filename }}'
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
register: _checkmk_package
|
||||
- name: Checkmk | Require staged package
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- _checkmk_package.stat.isreg | default(false)
|
||||
- _checkmk_package.stat.size | default(0) | int > 0
|
||||
fail_msg: Required agent package is absent/empty. Use AIM Maintenance to download packages first.
|
||||
quiet: true
|
||||
- name: Windows | Ensure temp dir exists
|
||||
ansible.windows.win_file:
|
||||
path: '{{ checkmk_windows_tmp_path }}'
|
||||
state: directory
|
||||
- name: Windows | Copy Checkmk agent MSI from role files
|
||||
ansible.windows.win_copy:
|
||||
src: '{{ checkmk_agent_files_dir }}/{{ checkmk_msi_filename }}'
|
||||
dest: '{{ checkmk_windows_tmp_path }}\{{ checkmk_msi_filename }}'
|
||||
- name: Windows | Install Checkmk agent from local MSI
|
||||
ansible.windows.win_package:
|
||||
path: '{{ checkmk_windows_tmp_path }}\{{ checkmk_msi_filename }}'
|
||||
state: present
|
||||
register: _checkmk_package_install
|
||||
@@ -0,0 +1,36 @@
|
||||
# checkmk_cleanup_scripts
|
||||
|
||||
Operator defaults are intentionally low-precedence; inventory and explicit run options may override them.
|
||||
|
||||
```yaml
|
||||
---
|
||||
|
||||
checkmk_linux_plugin_dir: /usr/lib/check_mk_agent/plugins
|
||||
checkmk_linux_local_dir: /usr/lib/check_mk_agent/local
|
||||
checkmk_linux_config_dir: /etc/check_mk
|
||||
checkmk_windows_plugin_dir: C:\ProgramData\checkmk\agent\plugins
|
||||
checkmk_windows_builtin_plugin_dir: C:\Program Files (x86)\checkmk\service\plugins
|
||||
checkmk_windows_local_dir: C:\ProgramData\checkmk\agent\local
|
||||
checkmk_monitoring_scripts_dir: '{{ lookup(''ansible.builtin.env'', ''AIM_CHECKMK_MONITORING_SCRIPTS_DIR'')
|
||||
| default(''/etc/checkmk_monitoring_scripts'', true) }}'
|
||||
checkmk_windows_scripts_dir: '{{ checkmk_monitoring_scripts_dir }}/Scripts Windows'
|
||||
checkmk_linux_scripts_dir: '{{ checkmk_monitoring_scripts_dir }}/Scripts Linux'
|
||||
checkmk_unifi_mode: auto
|
||||
want_linux_check_certificate: false
|
||||
want_windows_citrix: false
|
||||
want_windows_surebackup: false
|
||||
want_windows_backup: false
|
||||
want_windows_nsp_mailqueue: false
|
||||
want_windows_certificate: false
|
||||
want_windows_veeam_cloud_connect: false
|
||||
want_windows_veeam_backup: false
|
||||
checkmk_cleanup_enabled: false
|
||||
```
|
||||
|
||||
## Structured result integration
|
||||
|
||||
Current reporting behavior and field semantics are specified in
|
||||
[scripts/docs/OPERATION_RESULTS.md](../../scripts/docs/OPERATION_RESULTS.md).
|
||||
The calling catalog playbook owns publication; helper roles do not implicitly export
|
||||
arbitrary facts, module results or debug data. Existing defaults above retain their
|
||||
precedence. See the current validation/sanity documents before using the new candidate.
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
|
||||
checkmk_linux_plugin_dir: /usr/lib/check_mk_agent/plugins
|
||||
checkmk_linux_local_dir: /usr/lib/check_mk_agent/local
|
||||
checkmk_linux_config_dir: /etc/check_mk
|
||||
checkmk_windows_plugin_dir: C:\ProgramData\checkmk\agent\plugins
|
||||
checkmk_windows_builtin_plugin_dir: C:\Program Files (x86)\checkmk\service\plugins
|
||||
checkmk_windows_local_dir: C:\ProgramData\checkmk\agent\local
|
||||
checkmk_monitoring_scripts_dir: '{{ lookup(''ansible.builtin.env'', ''AIM_CHECKMK_MONITORING_SCRIPTS_DIR'')
|
||||
| default(''/etc/checkmk_monitoring_scripts'', true) }}'
|
||||
checkmk_windows_scripts_dir: '{{ checkmk_monitoring_scripts_dir }}/Scripts Windows'
|
||||
checkmk_linux_scripts_dir: '{{ checkmk_monitoring_scripts_dir }}/Scripts Linux'
|
||||
checkmk_unifi_mode: auto
|
||||
want_linux_check_certificate: false
|
||||
want_windows_citrix: false
|
||||
want_windows_surebackup: false
|
||||
want_windows_backup: false
|
||||
want_windows_nsp_mailqueue: false
|
||||
want_windows_certificate: false
|
||||
want_windows_veeam_cloud_connect: false
|
||||
want_windows_veeam_backup: false
|
||||
checkmk_cleanup_enabled: false
|
||||
@@ -0,0 +1,15 @@
|
||||
|
||||
- name: Cleanup | Remove obsolete managed local check
|
||||
ansible.builtin.file:
|
||||
path: '{{ checkmk_linux_local_dir }}/{{ item }}'
|
||||
state: absent
|
||||
loop: '{{ _checkmk_remove_paths }}'
|
||||
register: _aim_cleanup_files
|
||||
- name: Cleanup | Remove UniFi configuration only when UniFi is disabled
|
||||
ansible.builtin.file:
|
||||
path: '{{ checkmk_linux_config_dir }}/unifi.cfg'
|
||||
state: absent
|
||||
when: _checkmk_unifi_effective == 'disabled'
|
||||
diff: false
|
||||
no_log: true
|
||||
register: _aim_cleanup_unifi
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
- name: Cleanup | Validate opt-in
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (checkmk_cleanup_enabled) is boolean or (checkmk_cleanup_enabled | string | lower) in ['true',
|
||||
'false']
|
||||
fail_msg: checkmk_cleanup_enabled must be boolean.
|
||||
quiet: true
|
||||
- name: Cleanup | Build obsolete paths
|
||||
ansible.builtin.set_fact:
|
||||
_checkmk_remove_paths: "{{ _checkmk_obsolete_scripts | map(attribute='filename') | list }}"
|
||||
- name: Cleanup | Candidate files
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
directory: "{{ checkmk_windows_local_dir if ansible_facts.os_family == 'Windows' else checkmk_linux_local_dir
|
||||
}}"
|
||||
files: '{{ _checkmk_remove_paths }}'
|
||||
unifi_config: "{{ 'remove only when disabled' if ansible_facts.os_family != 'Windows' and _checkmk_unifi_effective
|
||||
== 'disabled' else 'retained' }}"
|
||||
mode: "{{ 'DELETE APPROVED' if checkmk_cleanup_enabled | bool else 'PREVIEW ONLY' }}"
|
||||
- name: Cleanup | linux
|
||||
ansible.builtin.include_tasks: linux.yml
|
||||
when:
|
||||
- checkmk_cleanup_enabled | bool
|
||||
- ansible_facts.os_family != 'Windows'
|
||||
- name: Cleanup | windows
|
||||
ansible.builtin.include_tasks: windows.yml
|
||||
when:
|
||||
- checkmk_cleanup_enabled | bool
|
||||
- ansible_facts.os_family == 'Windows'
|
||||
- name: AIM | Publish operation result
|
||||
ansible.builtin.set_stats:
|
||||
per_host: true
|
||||
aggregate: false
|
||||
data:
|
||||
aim_output:
|
||||
protocol: aim_output_v1
|
||||
schema: managed_cleanup_preview_v1
|
||||
data: "{{ _checkmk_remove_paths | aim_report_cleanup(checkmk_windows_local_dir if ansible_facts.os_family
|
||||
== 'Windows' else checkmk_linux_local_dir, _aim_cleanup_files | default({}), _checkmk_unifi_effective
|
||||
if ansible_facts.os_family != 'Windows' else 'not_applicable', _aim_cleanup_unifi | default({}), checkmk_cleanup_enabled
|
||||
| bool, ansible_check_mode) }}"
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
- name: Cleanup | Remove obsolete managed local check or legacy local plugin copy
|
||||
ansible.windows.win_file:
|
||||
path: '{{ checkmk_windows_local_dir }}\{{ item }}'
|
||||
state: absent
|
||||
loop: '{{ _checkmk_remove_paths }}'
|
||||
register: _aim_cleanup_local_files
|
||||
|
||||
- name: Cleanup | Remove obsolete managed custom plugin
|
||||
ansible.windows.win_file:
|
||||
path: '{{ checkmk_windows_plugin_dir }}\{{ item }}'
|
||||
state: absent
|
||||
loop: >-
|
||||
{{ _checkmk_obsolete_scripts
|
||||
| selectattr('destination', 'defined')
|
||||
| selectattr('destination', 'equalto', 'custom_plugin')
|
||||
| map(attribute='filename') | list }}
|
||||
register: _aim_cleanup_custom_plugin_files
|
||||
|
||||
- name: Cleanup | Remove obsolete known legacy built-in plugin copy
|
||||
ansible.windows.win_file:
|
||||
path: '{{ checkmk_windows_builtin_plugin_dir }}\{{ item }}'
|
||||
state: absent
|
||||
loop: >-
|
||||
{{ _checkmk_remove_paths
|
||||
| select('in', ['veeam_o365_status.ps1', 'veeam_backup_status.ps1'])
|
||||
| list }}
|
||||
register: _aim_cleanup_builtin_files
|
||||
|
||||
- name: Cleanup | Combine Windows cleanup results
|
||||
ansible.builtin.set_fact:
|
||||
_aim_cleanup_files:
|
||||
results: >-
|
||||
{{ (_aim_cleanup_local_files.results | default([]))
|
||||
+ (_aim_cleanup_custom_plugin_files.results | default([]))
|
||||
+ (_aim_cleanup_builtin_files.results | default([])) }}
|
||||
@@ -0,0 +1,33 @@
|
||||
# checkmk_configure_agent
|
||||
|
||||
On Windows, AIM owns only the top-level `plugins:` section of `check_mk.user.yml`.
|
||||
The role preserves all other top-level sections and comments, including `global`,
|
||||
`winperf`, `fileinfo`, `logwatch`, `local`, and `mrpe`.
|
||||
|
||||
The first line is an AIM ownership notice. The managed `plugins:` section also gets
|
||||
its own ownership comment so operators can see the exact management boundary.
|
||||
|
||||
Operator defaults are intentionally low-precedence; inventory and explicit run options may override them.
|
||||
|
||||
```yaml
|
||||
---
|
||||
checkmk_windows_user_cfg: C:\ProgramData\checkmk\agent\check_mk.user.yml
|
||||
checkmk_windows_updates_timeout: 3600
|
||||
checkmk_windows_updates_cache: 43200
|
||||
checkmk_mk_inventory_timeout: 120
|
||||
checkmk_plugins_default_timeout: 120
|
||||
checkmk_plugins_default_cache: 600
|
||||
checkmk_extra_plugin_patterns: []
|
||||
```
|
||||
|
||||
`checkmk_extra_plugin_patterns` entries are inserted before AIM's standard plugin
|
||||
rules. AIM does not manage `local:` or `mrpe:` from this role; existing host-specific
|
||||
configuration in those sections is left intact.
|
||||
|
||||
## Structured result integration
|
||||
|
||||
Current reporting behavior and field semantics are specified in
|
||||
[scripts/docs/OPERATION_RESULTS.md](../../scripts/docs/OPERATION_RESULTS.md).
|
||||
The calling catalog playbook owns publication; helper roles do not implicitly export
|
||||
arbitrary facts, module results or debug data. Existing defaults above retain their
|
||||
precedence. See the current validation/sanity documents before using the new candidate.
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
# AIM owns only the top-level plugins section in the Windows user configuration.
|
||||
# All other sections and comments must remain untouched.
|
||||
checkmk_windows_user_cfg: C:\ProgramData\checkmk\agent\check_mk.user.yml
|
||||
checkmk_windows_updates_timeout: 3600
|
||||
checkmk_windows_updates_cache: 43200
|
||||
checkmk_mk_inventory_timeout: 120
|
||||
checkmk_plugins_default_timeout: 120
|
||||
checkmk_plugins_default_cache: 600
|
||||
checkmk_extra_plugin_patterns: []
|
||||
@@ -0,0 +1,196 @@
|
||||
---
|
||||
|
||||
- name: Checkmk | Validate Windows plugin execution settings
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- checkmk_extra_plugin_patterns is sequence
|
||||
- checkmk_extra_plugin_patterns is not string
|
||||
- checkmk_windows_updates_timeout | int >= 0
|
||||
- checkmk_windows_updates_cache | int >= 0
|
||||
- checkmk_mk_inventory_timeout | int >= 0
|
||||
- checkmk_plugins_default_timeout | int >= 0
|
||||
- checkmk_plugins_default_cache | int >= 0
|
||||
fail_msg: Invalid Checkmk plugin execution setting type or negative timeout.
|
||||
quiet: true
|
||||
when: ansible_facts.os_family == 'Windows'
|
||||
|
||||
- name: Checkmk | Validate custom plugin rule fields
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- item is mapping
|
||||
- item.pattern is defined
|
||||
- item.pattern is string
|
||||
- item.run is not defined or item.run is boolean
|
||||
- item['async'] is not defined or item['async'] is boolean
|
||||
- item.timeout is not defined or item.timeout | int >= 0
|
||||
- item.cache_age is not defined or item.cache_age | int >= 0
|
||||
- item.keys() | difference(['pattern','run','async','timeout','cache_age','retry_count']) | length == 0
|
||||
fail_msg: Custom plugin rules require pattern and supported execution fields.
|
||||
quiet: true
|
||||
loop: '{{ checkmk_extra_plugin_patterns }}'
|
||||
loop_control:
|
||||
label: custom plugin execution rule
|
||||
no_log: true
|
||||
when: ansible_facts.os_family == 'Windows'
|
||||
|
||||
- name: Windows | Render AIM-managed Checkmk plugins section
|
||||
ansible.windows.win_template:
|
||||
src: windows_plugins_section.yml.j2
|
||||
dest: '{{ checkmk_windows_user_cfg }}.aim-plugins.tmp'
|
||||
when: ansible_facts.os_family == 'Windows'
|
||||
diff: false
|
||||
changed_when: false
|
||||
|
||||
- name: Windows | Replace only Checkmk plugins section
|
||||
ansible.windows.win_shell: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$configPath = '{{ checkmk_windows_user_cfg }}'
|
||||
$fragmentPath = '{{ checkmk_windows_user_cfg }}.aim-plugins.tmp'
|
||||
$header = '# Managed by Ansible (checkmk_configure_agent). Only AIM-marked sections are modified; all other content is preserved.'
|
||||
$legacyHeader = '# Managed by Ansible (checkmk_configure_agent).'
|
||||
$managedMarkerPrefix = '# AIM managed section: plugins (checkmk_configure_agent).'
|
||||
|
||||
if (-not (Test-Path -LiteralPath $fragmentPath)) {
|
||||
throw "AIM Checkmk plugins fragment is missing: $fragmentPath"
|
||||
}
|
||||
|
||||
$fragment = [System.IO.File]::ReadAllText($fragmentPath)
|
||||
$fragment = $fragment.TrimEnd([char[]]"`r`n")
|
||||
|
||||
if (Test-Path -LiteralPath $configPath) {
|
||||
$original = [System.IO.File]::ReadAllText($configPath)
|
||||
}
|
||||
else {
|
||||
$original = ''
|
||||
}
|
||||
|
||||
if ($original.Contains("`r`n")) {
|
||||
$newline = "`r`n"
|
||||
}
|
||||
else {
|
||||
$newline = "`n"
|
||||
}
|
||||
|
||||
$hadFinalNewline = $original.EndsWith("`r`n") -or $original.EndsWith("`n") -or $original.EndsWith("`r")
|
||||
$lines = @()
|
||||
if ($original.Length -gt 0) {
|
||||
$lines = @([regex]::Split($original, "`r`n|`n|`r"))
|
||||
if ($hadFinalNewline -and $lines.Count -gt 0 -and $lines[$lines.Count - 1] -eq '') {
|
||||
if ($lines.Count -eq 1) {
|
||||
$lines = @()
|
||||
}
|
||||
else {
|
||||
$lines = @($lines[0..($lines.Count - 2)])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Keep the AIM ownership notice as line 1 without discarding the previous first line.
|
||||
if ($lines.Count -eq 0) {
|
||||
$lines = @($header)
|
||||
}
|
||||
elseif ($lines[0] -eq $legacyHeader -or $lines[0].StartsWith('# Managed by Ansible (checkmk_configure_agent).')) {
|
||||
$lines[0] = $header
|
||||
}
|
||||
else {
|
||||
$lines = @($header) + $lines
|
||||
}
|
||||
|
||||
$pluginIndex = -1
|
||||
for ($i = 0; $i -lt $lines.Count; $i++) {
|
||||
if ($lines[$i] -match '^plugins\s*:\s*(?:#.*)?$') {
|
||||
$pluginIndex = $i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
$fragmentLines = @([regex]::Split($fragment, "`r`n|`n|`r"))
|
||||
|
||||
if ($pluginIndex -ge 0) {
|
||||
$replaceStart = $pluginIndex
|
||||
if ($pluginIndex -gt 0 -and $lines[$pluginIndex - 1].StartsWith($managedMarkerPrefix)) {
|
||||
$replaceStart = $pluginIndex - 1
|
||||
}
|
||||
|
||||
$nextTopLevelKey = $lines.Count
|
||||
for ($i = $pluginIndex + 1; $i -lt $lines.Count; $i++) {
|
||||
if ($lines[$i] -match '^[A-Za-z_][A-Za-z0-9_.-]*\s*:') {
|
||||
$nextTopLevelKey = $i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# Preserve blank lines and top-level comments immediately before the next untouched section.
|
||||
$replaceEnd = $nextTopLevelKey
|
||||
while ($replaceEnd -gt ($pluginIndex + 1)) {
|
||||
$candidate = $lines[$replaceEnd - 1]
|
||||
if ([string]::IsNullOrWhiteSpace($candidate) -or $candidate.StartsWith('#')) {
|
||||
$replaceEnd--
|
||||
}
|
||||
else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
$before = @()
|
||||
if ($replaceStart -gt 0) {
|
||||
$before = @($lines[0..($replaceStart - 1)])
|
||||
}
|
||||
$after = @()
|
||||
if ($replaceEnd -lt $lines.Count) {
|
||||
$after = @($lines[$replaceEnd..($lines.Count - 1)])
|
||||
}
|
||||
$lines = @($before + $fragmentLines + $after)
|
||||
}
|
||||
else {
|
||||
if ($lines.Count -gt 0 -and -not [string]::IsNullOrWhiteSpace($lines[$lines.Count - 1])) {
|
||||
$lines += ''
|
||||
}
|
||||
$lines += $fragmentLines
|
||||
}
|
||||
|
||||
$updated = [string]::Join($newline, $lines)
|
||||
if ($hadFinalNewline -or $original.Length -eq 0) {
|
||||
$updated += $newline
|
||||
}
|
||||
|
||||
if ($updated -ne $original) {
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||||
[System.IO.File]::WriteAllText($configPath, $updated, $utf8NoBom)
|
||||
Write-Output 'AIM_CHANGED=true'
|
||||
}
|
||||
else {
|
||||
Write-Output 'AIM_CHANGED=false'
|
||||
}
|
||||
register: _checkmk_plugins_update
|
||||
changed_when: "'AIM_CHANGED=true' in _checkmk_plugins_update.stdout"
|
||||
when: ansible_facts.os_family == 'Windows'
|
||||
notify: checkmk | windows | configuration-changed
|
||||
diff: false
|
||||
|
||||
- name: Windows | Normalize ACL on Checkmk user configuration
|
||||
ansible.builtin.include_role:
|
||||
name: checkmk_windows_acl
|
||||
vars:
|
||||
checkmk_windows_acl_paths:
|
||||
- '{{ checkmk_windows_user_cfg }}'
|
||||
when: ansible_facts.os_family == 'Windows'
|
||||
|
||||
- name: Windows | Remove temporary Checkmk plugins fragment
|
||||
ansible.windows.win_file:
|
||||
path: '{{ checkmk_windows_user_cfg }}.aim-plugins.tmp'
|
||||
state: absent
|
||||
when: ansible_facts.os_family == 'Windows'
|
||||
changed_when: false
|
||||
|
||||
- name: Checkmk | Configuration summary
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
destination: '{{ checkmk_windows_user_cfg }}'
|
||||
managed_section: plugins
|
||||
extra_plugin_rules: '{{ checkmk_extra_plugin_patterns | length }}'
|
||||
other_sections: preserved
|
||||
when:
|
||||
- ansible_facts.os_family == 'Windows'
|
||||
- aim_debug | default(false) | bool
|
||||
@@ -0,0 +1,72 @@
|
||||
# AIM managed section: plugins (checkmk_configure_agent). Content under plugins: may be replaced by AIM.
|
||||
plugins:
|
||||
execution:
|
||||
{% if checkmk_extra_plugin_patterns | length %}
|
||||
{{ checkmk_extra_plugin_patterns | to_nice_yaml(indent=2, sort_keys=false) | indent(4, true) }}
|
||||
{% endif %}
|
||||
- pattern: '$BUILTIN_PLUGINS_PATH$\windows_updates.vbs'
|
||||
run: true
|
||||
async: true
|
||||
timeout: {{ checkmk_windows_updates_timeout | int }}
|
||||
cache_age: {{ checkmk_windows_updates_cache | int }}
|
||||
retry_count: 0
|
||||
- pattern: '$BUILTIN_PLUGINS_PATH$\mk_inventory.vbs'
|
||||
run: true
|
||||
async: true
|
||||
timeout: {{ checkmk_mk_inventory_timeout | int }}
|
||||
cache_age: 3600
|
||||
- pattern: '$BUILTIN_PLUGINS_PATH$\mssql.vbs'
|
||||
run: true
|
||||
timeout: {{ checkmk_plugins_default_timeout | int }}
|
||||
{% if has_veeam_vbo | default(false) %}
|
||||
- pattern: '$CUSTOM_PLUGINS_PATH$\veeam_o365_status.ps1'
|
||||
run: true
|
||||
async: true
|
||||
timeout: {{ checkmk_plugins_default_timeout | int }}
|
||||
cache_age: {{ checkmk_plugins_default_cache | int }}
|
||||
{% endif %}
|
||||
{% if want_windows_citrix | default(false) %}
|
||||
- pattern: '$CUSTOM_PLUGINS_PATH$\citrix_sessions_customized.ps1'
|
||||
run: true
|
||||
async: true
|
||||
timeout: {{ checkmk_plugins_default_timeout | int }}
|
||||
cache_age: {{ checkmk_plugins_default_cache | int }}
|
||||
{% endif %}
|
||||
{% if want_windows_veeam_backup | default(false) %}
|
||||
- pattern: '$CUSTOM_PLUGINS_PATH$\veeam_backup_status.ps1'
|
||||
run: true
|
||||
async: true
|
||||
timeout: {{ checkmk_plugins_default_timeout | int }}
|
||||
cache_age: {{ checkmk_plugins_default_cache | int }}
|
||||
{% endif %}
|
||||
{% if is_dc | default(false) %}
|
||||
- pattern: '$BUILTIN_PLUGINS_PATH$\ad_replication.bat'
|
||||
run: true
|
||||
timeout: {{ checkmk_plugins_default_timeout | int }}
|
||||
{% endif %}
|
||||
{% if is_dhcp_server | default(false) %}
|
||||
- pattern: '$BUILTIN_PLUGINS_PATH$\win_dhcp_pools.bat'
|
||||
run: true
|
||||
timeout: {{ checkmk_plugins_default_timeout | int }}
|
||||
{% endif %}
|
||||
{% if is_hyperv_host | default(false) %}
|
||||
- pattern: '$BUILTIN_PLUGINS_PATH$\hyperv_vms.ps1'
|
||||
run: true
|
||||
timeout: {{ checkmk_plugins_default_timeout | int }}
|
||||
- pattern: '$BUILTIN_PLUGINS_PATH$\hyperv_vms_guestinfos.ps1'
|
||||
run: true
|
||||
timeout: {{ checkmk_plugins_default_timeout | int }}
|
||||
{% endif %}
|
||||
- pattern: '$CUSTOM_PLUGINS_PATH$\*.ps1'
|
||||
run: true
|
||||
async: true
|
||||
timeout: {{ checkmk_plugins_default_timeout | int }}
|
||||
cache_age: {{ checkmk_plugins_default_cache | int }}
|
||||
- pattern: '$CUSTOM_PLUGINS_PATH$\*.*'
|
||||
run: true
|
||||
timeout: {{ checkmk_plugins_default_timeout | int }}
|
||||
- pattern: '$BUILTIN_PLUGINS_PATH$\*.*'
|
||||
run: false
|
||||
timeout: {{ checkmk_plugins_default_timeout | int }}
|
||||
- pattern: '*'
|
||||
run: false
|
||||
@@ -0,0 +1,48 @@
|
||||
# checkmk_deploy_scripts
|
||||
|
||||
Operator defaults are intentionally low-precedence; inventory and explicit run options may override them.
|
||||
|
||||
```yaml
|
||||
---
|
||||
# unifi.cfg is role-managed and replaced, mode 0600. Password comes from Vault.
|
||||
# An absent/CHANGEME password fails before deployment; no credentials are logged.
|
||||
checkmk_linux_plugin_dir: /usr/lib/check_mk_agent/plugins
|
||||
checkmk_linux_local_dir: /usr/lib/check_mk_agent/local
|
||||
checkmk_linux_config_dir: /etc/check_mk
|
||||
checkmk_windows_plugin_dir: C:\ProgramData\checkmk\agent\plugins
|
||||
checkmk_windows_builtin_plugin_dir: C:\Program Files (x86)\checkmk\service\plugins
|
||||
checkmk_windows_local_dir: C:\ProgramData\checkmk\agent\local
|
||||
checkmk_monitoring_scripts_dir: '{{ lookup(''ansible.builtin.env'', ''AIM_CHECKMK_MONITORING_SCRIPTS_DIR'')
|
||||
| default(''/etc/checkmk_monitoring_scripts'', true) }}'
|
||||
checkmk_windows_scripts_dir: '{{ checkmk_monitoring_scripts_dir }}/Scripts Windows'
|
||||
checkmk_linux_scripts_dir: '{{ checkmk_monitoring_scripts_dir }}/Scripts Linux'
|
||||
checkmk_unifi_mode: auto
|
||||
want_linux_check_certificate: false
|
||||
want_windows_citrix: false
|
||||
want_windows_surebackup: false
|
||||
want_windows_backup: false
|
||||
want_windows_nsp_mailqueue: false
|
||||
want_windows_certificate: false
|
||||
want_windows_veeam_cloud_connect: false
|
||||
want_windows_veeam_backup: false
|
||||
checkmk_unifi_username: bf-monitoring
|
||||
checkmk_unifi_password: '{{ vault_checkmk_unifi_password | default('''') }}'
|
||||
checkmk_unifi_baseurl: '{{ ''https://127.0.0.1:11443'' if _checkmk_unifi_effective == ''os'' else ''https://127.0.0.1:8443''
|
||||
}}'
|
||||
checkmk_unifi_curl_options: ' --insecure --tlsv1.2'
|
||||
checkmk_unifi_status_provisioning: 1
|
||||
checkmk_unifi_status_upgrading: 1
|
||||
checkmk_unifi_status_upgradable: 0
|
||||
checkmk_unifi_status_heartbeat_missed: 1
|
||||
checkmk_unifi_status_noautobackup: 0
|
||||
```
|
||||
|
||||
`unifi.cfg` is generated from the supplied schema, POSIX-shell quoted, mode `0600`, and protected with `no_log` and `diff: false`. Use a Vault reference for the password. `CHANGEME` and empty passwords fail before deployment. The active UniFi mode replaces the alternative local check. Other obsolete scripts are removed only by explicit cleanup.
|
||||
|
||||
## Structured result integration
|
||||
|
||||
Current reporting behavior and field semantics are specified in
|
||||
[scripts/docs/OPERATION_RESULTS.md](../../scripts/docs/OPERATION_RESULTS.md).
|
||||
The calling catalog playbook owns publication; helper roles do not implicitly export
|
||||
arbitrary facts, module results or debug data. Existing defaults above retain their
|
||||
precedence. See the current validation/sanity documents before using the new candidate.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
# unifi.cfg is role-managed and replaced, mode 0600. Password comes from Vault.
|
||||
# An absent/CHANGEME password fails before deployment; no credentials are logged.
|
||||
checkmk_linux_plugin_dir: /usr/lib/check_mk_agent/plugins
|
||||
checkmk_linux_local_dir: /usr/lib/check_mk_agent/local
|
||||
checkmk_linux_config_dir: /etc/check_mk
|
||||
checkmk_windows_plugin_dir: C:\ProgramData\checkmk\agent\plugins
|
||||
checkmk_windows_builtin_plugin_dir: C:\Program Files (x86)\checkmk\service\plugins
|
||||
checkmk_windows_local_dir: C:\ProgramData\checkmk\agent\local
|
||||
checkmk_monitoring_scripts_dir: '{{ lookup(''ansible.builtin.env'', ''AIM_CHECKMK_MONITORING_SCRIPTS_DIR'')
|
||||
| default(''/etc/checkmk_monitoring_scripts'', true) }}'
|
||||
checkmk_windows_scripts_dir: '{{ checkmk_monitoring_scripts_dir }}/Scripts Windows'
|
||||
checkmk_linux_scripts_dir: '{{ checkmk_monitoring_scripts_dir }}/Scripts Linux'
|
||||
checkmk_unifi_mode: auto
|
||||
want_linux_check_certificate: false
|
||||
want_windows_citrix: false
|
||||
want_windows_surebackup: false
|
||||
want_windows_backup: false
|
||||
want_windows_nsp_mailqueue: false
|
||||
want_windows_certificate: false
|
||||
want_windows_veeam_cloud_connect: false
|
||||
want_windows_veeam_backup: false
|
||||
checkmk_unifi_username: bf-monitoring
|
||||
checkmk_unifi_password: '{{ vault_checkmk_unifi_password | default('''') }}'
|
||||
checkmk_unifi_baseurl: '{{ ''https://127.0.0.1:11443'' if _checkmk_unifi_effective == ''os'' else ''https://127.0.0.1:8443''
|
||||
}}'
|
||||
checkmk_unifi_curl_options: ' --insecure --tlsv1.2'
|
||||
checkmk_unifi_status_provisioning: 1
|
||||
checkmk_unifi_status_upgrading: 1
|
||||
checkmk_unifi_status_upgradable: 0
|
||||
checkmk_unifi_status_heartbeat_missed: 1
|
||||
checkmk_unifi_status_noautobackup: 0
|
||||
@@ -0,0 +1,40 @@
|
||||
# UniFi mode replacement is the explicitly approved exception to separate cleanup.
|
||||
# Unknown files and the active UniFi check are never removed here.
|
||||
- name: Linux | Ensure local check directory
|
||||
ansible.builtin.file:
|
||||
path: '{{ checkmk_linux_local_dir }}'
|
||||
state: directory
|
||||
mode: '0755'
|
||||
- name: Linux | Ensure configuration directory
|
||||
ansible.builtin.file:
|
||||
path: '{{ checkmk_linux_config_dir }}'
|
||||
state: directory
|
||||
mode: '0755'
|
||||
- name: Linux | Write the single UniFi configuration
|
||||
ansible.builtin.template:
|
||||
src: unifi.cfg.j2
|
||||
dest: '{{ checkmk_linux_config_dir }}/unifi.cfg'
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0600'
|
||||
validate: /bin/sh -n %s
|
||||
when: _checkmk_unifi_effective in ['network','os']
|
||||
no_log: true
|
||||
diff: false
|
||||
register: _aim_unifi_write
|
||||
- name: Linux | Deploy selected monitoring checks
|
||||
ansible.builtin.copy:
|
||||
src: '{{ item.source }}'
|
||||
dest: '{{ checkmk_linux_local_dir }}/{{ item.filename }}'
|
||||
mode: '0755'
|
||||
loop: '{{ _checkmk_selected_scripts }}'
|
||||
loop_control:
|
||||
label: '{{ item.filename }}'
|
||||
register: _aim_check_copies
|
||||
- name: Linux | Remove only the opposite UniFi local check
|
||||
ansible.builtin.file:
|
||||
path: "{{ checkmk_linux_local_dir }}/{{ 'check_unifi-controller.sh' if _checkmk_unifi_effective == 'os' else
|
||||
'check_unifi-os.sh' }}"
|
||||
state: absent
|
||||
when: _checkmk_unifi_effective in ['network','os']
|
||||
register: _aim_opposite_remove
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
|
||||
- name: Checkmk | Validate controller sources and required parameters
|
||||
ansible.builtin.import_tasks: preflight.yml
|
||||
- name: Checkmk | Deploy linux checks
|
||||
ansible.builtin.include_tasks: linux.yml
|
||||
when: ansible_facts.os_family != 'Windows'
|
||||
- name: Checkmk | Deploy windows checks
|
||||
ansible.builtin.include_tasks: windows.yml
|
||||
when: ansible_facts.os_family == 'Windows'
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
|
||||
- name: Checkmk | Inspect selected controller script sources
|
||||
ansible.builtin.stat:
|
||||
path: '{{ item.source }}'
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
loop: '{{ _checkmk_selected_scripts }}'
|
||||
loop_control:
|
||||
label: '{{ item.filename }}'
|
||||
register: _checkmk_sources
|
||||
- name: Checkmk | Require selected controller files
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- item.stat.exists | default(false)
|
||||
- item.stat.isreg | default(false)
|
||||
fail_msg: A selected monitoring script is missing on the controller. Sync the monitoring repository first.
|
||||
quiet: true
|
||||
loop: '{{ _checkmk_sources.results }}'
|
||||
loop_control:
|
||||
label: '{{ item.item.filename }}'
|
||||
- name: Checkmk | Validate UniFi public settings
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- checkmk_unifi_username is string
|
||||
- checkmk_unifi_username | length > 0
|
||||
- checkmk_unifi_baseurl is match('^https?://[^\s]+$')
|
||||
- checkmk_unifi_curl_options is string
|
||||
- checkmk_unifi_status_provisioning | int in [0,1,2,3]
|
||||
- checkmk_unifi_status_upgrading | int in [0,1,2,3]
|
||||
- checkmk_unifi_status_upgradable | int in [0,1,2,3]
|
||||
- checkmk_unifi_status_heartbeat_missed | int in [0,1,2,3]
|
||||
- checkmk_unifi_status_noautobackup | int in [0,1,2,3]
|
||||
fail_msg: Invalid UniFi username, base URL, curl options or status mapping.
|
||||
quiet: true
|
||||
when:
|
||||
- ansible_facts.os_family != 'Windows'
|
||||
- _checkmk_unifi_effective in ['network','os']
|
||||
- name: Checkmk | Require a real UniFi monitoring password
|
||||
when:
|
||||
- ansible_facts.os_family != 'Windows'
|
||||
- _checkmk_unifi_effective in ['network','os']
|
||||
block:
|
||||
- name: Checkmk | Validate secret
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- checkmk_unifi_password is string
|
||||
- checkmk_unifi_password | length > 0
|
||||
- checkmk_unifi_password != 'CHANGEME'
|
||||
fail_msg: A real UniFi password is required.
|
||||
quiet: true
|
||||
no_log: true
|
||||
rescue:
|
||||
- name: Checkmk | Explain missing UniFi secret
|
||||
ansible.builtin.fail:
|
||||
msg: Set vault_checkmk_unifi_password in the customer Vault, or override checkmk_unifi_password with
|
||||
a host-specific Vault reference. Empty values and CHANGEME are refused. No secret was logged.
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
- name: Windows | Ensure Checkmk local directory
|
||||
ansible.windows.win_file:
|
||||
path: '{{ checkmk_windows_local_dir }}'
|
||||
state: directory
|
||||
|
||||
- name: Windows | Ensure Checkmk custom plugin directory
|
||||
ansible.windows.win_file:
|
||||
path: '{{ checkmk_windows_plugin_dir }}'
|
||||
state: directory
|
||||
when: >-
|
||||
{{ _checkmk_selected_scripts
|
||||
| selectattr('destination', 'defined')
|
||||
| selectattr('destination', 'equalto', 'custom_plugin')
|
||||
| list | length > 0 }}
|
||||
|
||||
- name: Windows | Deploy selected monitoring checks
|
||||
ansible.windows.win_copy:
|
||||
src: '{{ item.source }}'
|
||||
dest: >-
|
||||
{{ (checkmk_windows_plugin_dir
|
||||
if item.destination | default('local') == 'custom_plugin'
|
||||
else checkmk_windows_local_dir) }}\{{ item.filename }}
|
||||
loop: '{{ _checkmk_selected_scripts }}'
|
||||
loop_control:
|
||||
label: '{{ item.filename }}'
|
||||
register: _aim_check_copies
|
||||
|
||||
- name: Windows | Normalize ACL on AIM-managed monitoring checks
|
||||
ansible.builtin.include_role:
|
||||
name: checkmk_windows_acl
|
||||
vars:
|
||||
checkmk_windows_acl_paths:
|
||||
- >-
|
||||
{{ (checkmk_windows_plugin_dir
|
||||
if checkmk_acl_script.destination | default('local') == 'custom_plugin'
|
||||
else checkmk_windows_local_dir) }}\{{ checkmk_acl_script.filename }}
|
||||
loop: '{{ _checkmk_selected_scripts }}'
|
||||
loop_control:
|
||||
loop_var: checkmk_acl_script
|
||||
label: '{{ checkmk_acl_script.filename }}'
|
||||
|
||||
- name: Windows | Remove legacy local copies after custom plugin relocation
|
||||
ansible.windows.win_file:
|
||||
path: '{{ checkmk_windows_local_dir }}\{{ item.filename }}'
|
||||
state: absent
|
||||
loop: >-
|
||||
{{ _checkmk_selected_scripts
|
||||
| selectattr('destination', 'defined')
|
||||
| selectattr('destination', 'equalto', 'custom_plugin')
|
||||
| list }}
|
||||
loop_control:
|
||||
label: '{{ item.filename }}'
|
||||
register: _aim_custom_plugin_legacy_local_remove
|
||||
|
||||
- name: Windows | Remove known legacy built-in copies after custom plugin relocation
|
||||
ansible.windows.win_file:
|
||||
path: '{{ checkmk_windows_builtin_plugin_dir }}\{{ item.filename }}'
|
||||
state: absent
|
||||
loop: >-
|
||||
{{ _checkmk_selected_scripts
|
||||
| selectattr('destination', 'defined')
|
||||
| selectattr('destination', 'equalto', 'custom_plugin')
|
||||
| selectattr('filename', 'in', ['veeam_o365_status.ps1', 'veeam_backup_status.ps1'])
|
||||
| list }}
|
||||
loop_control:
|
||||
label: '{{ item.filename }}'
|
||||
register: _aim_custom_plugin_legacy_builtin_remove
|
||||
@@ -0,0 +1,13 @@
|
||||
# Managed by Ansible - checkmk_deploy_scripts. One UniFi variant per host.
|
||||
# Values are POSIX-shell quoted because the monitoring scripts source this file.
|
||||
USERNAME={{ checkmk_unifi_username | quote }}
|
||||
PASSWORD={{ checkmk_unifi_password | quote }}
|
||||
BASEURL={{ checkmk_unifi_baseurl | quote }}
|
||||
CURLOPTS={{ checkmk_unifi_curl_options | quote }}
|
||||
|
||||
# 0 OK, 1 WARN, 2 CRIT, 3 UNKNOWN
|
||||
STATUS_PROVISIONING={{ checkmk_unifi_status_provisioning | int }}
|
||||
STATUS_UPGRADING={{ checkmk_unifi_status_upgrading | int }}
|
||||
STATUS_UPGRADABLE={{ checkmk_unifi_status_upgradable | int }}
|
||||
STATUS_HEARTBEAT_MISSED={{ checkmk_unifi_status_heartbeat_missed | int }}
|
||||
STATUS_NOAUTOBACKUP={{ checkmk_unifi_status_noautobackup | int }}
|
||||
@@ -0,0 +1,23 @@
|
||||
# checkmk_manage_service
|
||||
|
||||
Operator defaults are intentionally low-precedence; inventory and explicit run options may override them.
|
||||
|
||||
```yaml
|
||||
---
|
||||
|
||||
checkmk_linux_service_name: cmk-agent-ctl-daemon.service
|
||||
checkmk_linux_socket_name: check-mk-agent.socket
|
||||
checkmk_windows_service_name: ''
|
||||
checkmk_windows_service_candidates:
|
||||
- Check_MK_Agent
|
||||
- CheckmkService
|
||||
- Checkmk Service
|
||||
```
|
||||
|
||||
## Structured result integration
|
||||
|
||||
Current reporting behavior and field semantics are specified in
|
||||
[scripts/docs/OPERATION_RESULTS.md](../../scripts/docs/OPERATION_RESULTS.md).
|
||||
The calling catalog playbook owns publication; helper roles do not implicitly export
|
||||
arbitrary facts, module results or debug data. Existing defaults above retain their
|
||||
precedence. See the current validation/sanity documents before using the new candidate.
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
|
||||
checkmk_linux_service_name: cmk-agent-ctl-daemon.service
|
||||
checkmk_linux_socket_name: check-mk-agent.socket
|
||||
checkmk_windows_service_name: ''
|
||||
checkmk_windows_service_candidates:
|
||||
- Check_MK_Agent
|
||||
- CheckmkService
|
||||
- Checkmk Service
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
|
||||
- name: Checkmk | Restart changed Windows agent configuration
|
||||
ansible.windows.win_service:
|
||||
name: '{{ item }}'
|
||||
state: restarted
|
||||
listen: checkmk | windows | configuration-changed
|
||||
loop: '{{ _checkmk_windows_services | default([]) }}'
|
||||
when: ansible_facts.os_family == 'Windows'
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
|
||||
- name: Linux | Require systemd service management
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ansible_facts.service_mgr == 'systemd'
|
||||
fail_msg: This Checkmk service policy requires systemd. No alternate service system is configured.
|
||||
quiet: true
|
||||
- name: Linux | Refresh systemd after package installation
|
||||
ansible.builtin.systemd_service:
|
||||
daemon_reload: true
|
||||
when: _checkmk_package_install.changed | default(false) | bool
|
||||
- name: Linux | Detect configured Checkmk units independently of running state
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- systemctl
|
||||
- show
|
||||
- --property=LoadState
|
||||
- --value
|
||||
- '{{ item }}'
|
||||
loop:
|
||||
- '{{ checkmk_linux_socket_name }}'
|
||||
- '{{ checkmk_linux_service_name }}'
|
||||
register: _checkmk_units
|
||||
changed_when: false
|
||||
failed_when: 'false'
|
||||
check_mode: false
|
||||
- name: Linux | Select the installed unit, preferring the socket
|
||||
ansible.builtin.set_fact:
|
||||
_checkmk_linux_units: '{{ _checkmk_units.results | selectattr(''stdout'', ''defined'') | selectattr(''stdout'',
|
||||
''equalto'', ''loaded'') | map(attribute=''item'') | list }}'
|
||||
- name: Linux | Require an installed Checkmk unit
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- _checkmk_linux_units | length > 0
|
||||
fail_msg: No configured Checkmk socket/service unit was found. Verify installation and the unit names.
|
||||
quiet: true
|
||||
- name: Linux | Ensure Checkmk is enabled and running
|
||||
ansible.builtin.systemd_service:
|
||||
name: '{{ _checkmk_linux_units | first }}'
|
||||
enabled: true
|
||||
state: started
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
|
||||
- name: Checkmk | Ensure agent service on linux
|
||||
ansible.builtin.include_tasks: linux.yml
|
||||
when: ansible_facts.os_family != 'Windows'
|
||||
- name: Checkmk | Ensure agent service on windows
|
||||
ansible.builtin.include_tasks: windows.yml
|
||||
when: ansible_facts.os_family == 'Windows'
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
|
||||
- name: Windows | Find installed Checkmk service
|
||||
ansible.windows.win_service_info:
|
||||
name: '{{ item }}'
|
||||
loop: '{{ ([checkmk_windows_service_name] if checkmk_windows_service_name | length else checkmk_windows_service_candidates)
|
||||
}}'
|
||||
register: _checkmk_win_service_query
|
||||
- name: Windows | Record service names
|
||||
ansible.builtin.set_fact:
|
||||
_checkmk_windows_services: '{{ _checkmk_win_service_query.results | selectattr(''services'', ''defined'')
|
||||
| map(attribute=''services'') | flatten | map(attribute=''name'') | unique | list }}'
|
||||
- name: Windows | Require installed Checkmk service
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- _checkmk_windows_services | length > 0
|
||||
fail_msg: No Checkmk service was found. Check the installed package or set checkmk_windows_service_name.
|
||||
quiet: true
|
||||
- name: Windows | Ensure Checkmk service is running
|
||||
ansible.windows.win_service:
|
||||
name: '{{ item }}'
|
||||
start_mode: auto
|
||||
state: started
|
||||
loop: '{{ _checkmk_windows_services }}'
|
||||
@@ -0,0 +1,9 @@
|
||||
# checkmk_report
|
||||
|
||||
Reporting-only helper for the Checkmk installation playbooks. Queries installed version
|
||||
from Windows uninstall registry or Debian/RPM package database, and re-reads current
|
||||
service/unit state. Does not install packages or restart/configure services. Unknown or
|
||||
ambiguous versions are null, never inferred from the staged package filename.
|
||||
|
||||
The calling playbook publishes checkmk_agent_state_v1 via set_stats. See
|
||||
[OPERATION_RESULTS.md](../../scripts/docs/OPERATION_RESULTS.md).
|
||||
@@ -0,0 +1,79 @@
|
||||
- name: Checkmk | Collect managed change summary
|
||||
ansible.builtin.set_fact:
|
||||
_aim_checkmk_changes: '{{ _aim_check_copies | default({}) | aim_report_checkmk_changes(_checkmk_selected_scripts,
|
||||
_checkmk_plugins_update | default({}), _aim_unifi_write | default({}), _aim_opposite_remove | default({}),
|
||||
_checkmk_unifi_effective, ansible_check_mode) }}'
|
||||
|
||||
# No supported Windows package-inventory module exposes arbitrary installed MSI/registry products.
|
||||
# Keep this bounded read-only registry query until an official module covers that data.
|
||||
- name: Checkmk | Query Windows installed version
|
||||
ansible.windows.win_shell: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$roots = @('HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*')
|
||||
$products = @(Get-ItemProperty -Path $roots -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -match '^(Check_MK|Checkmk|Check MK).*Agent' })
|
||||
$versions = @($products | Select-Object -ExpandProperty DisplayVersion -Unique)
|
||||
$version = $null
|
||||
if ($versions.Count -eq 1) { $version = [string]$versions[0] }
|
||||
@{ installed=($products.Count -gt 0); version=$version; version_source='registry' } | ConvertTo-Json -Compress
|
||||
register: _aim_checkmk_version_raw
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
when: ansible_facts.os_family == 'Windows'
|
||||
|
||||
- name: Checkmk | Collect Linux package facts
|
||||
ansible.builtin.package_facts:
|
||||
manager: auto
|
||||
when: ansible_facts.os_family != 'Windows'
|
||||
|
||||
- name: Checkmk | Record Linux installed package rows
|
||||
ansible.builtin.set_fact:
|
||||
_aim_checkmk_linux_package_rows: "{{ ansible_facts.packages.get('check-mk-agent', []) }}"
|
||||
when: ansible_facts.os_family != 'Windows'
|
||||
|
||||
- name: Checkmk | Observe Windows service state
|
||||
ansible.windows.win_service_info:
|
||||
name: '{{ item }}'
|
||||
loop: '{{ _checkmk_windows_services | default([]) }}'
|
||||
register: _aim_checkmk_final_services
|
||||
when: ansible_facts.os_family == 'Windows'
|
||||
|
||||
- name: Checkmk | Collect Linux service facts
|
||||
ansible.builtin.service_facts:
|
||||
when: ansible_facts.os_family != 'Windows'
|
||||
|
||||
- name: Checkmk | Observe Linux unit state
|
||||
ansible.builtin.set_fact:
|
||||
_aim_checkmk_unit_state: >-
|
||||
{{ ansible_facts.services.get(_checkmk_linux_units | first,
|
||||
{'name': _checkmk_linux_units | first, 'state': 'unknown'}) }}
|
||||
when: ansible_facts.os_family != 'Windows'
|
||||
|
||||
- name: Checkmk | Build installed state report
|
||||
ansible.builtin.set_fact:
|
||||
_aim_checkmk_state: >-
|
||||
{{
|
||||
(
|
||||
(_aim_checkmk_version_raw.stdout | from_json)
|
||||
if ansible_facts.os_family == 'Windows'
|
||||
else {
|
||||
'installed': (_aim_checkmk_linux_package_rows | default([]) | length) > 0,
|
||||
'version': (
|
||||
(_aim_checkmk_linux_package_rows | first).version
|
||||
if (_aim_checkmk_linux_package_rows | default([]) | length) == 1
|
||||
else none
|
||||
),
|
||||
'version_source': 'package_facts'
|
||||
}
|
||||
)
|
||||
| aim_report_checkmk_state(
|
||||
(
|
||||
_aim_checkmk_final_services.results
|
||||
| selectattr('services', 'defined')
|
||||
| map(attribute='services')
|
||||
| flatten
|
||||
) if ansible_facts.os_family == 'Windows' else [_aim_checkmk_unit_state],
|
||||
_aim_checkmk_changes,
|
||||
_checkmk_package_install.changed | default(false),
|
||||
ansible_check_mode
|
||||
)
|
||||
}}
|
||||
@@ -0,0 +1,27 @@
|
||||
# checkmk_script_plan
|
||||
|
||||
Operator defaults are intentionally low-precedence; inventory and explicit run options may override them.
|
||||
|
||||
```yaml
|
||||
---
|
||||
# Shared deployment and cleanup paths/optional switches. No secrets.
|
||||
checkmk_linux_plugin_dir: /usr/lib/check_mk_agent/plugins
|
||||
checkmk_linux_local_dir: /usr/lib/check_mk_agent/local
|
||||
checkmk_linux_config_dir: /etc/check_mk
|
||||
checkmk_windows_plugin_dir: C:\ProgramData\checkmk\agent\plugins
|
||||
checkmk_windows_builtin_plugin_dir: C:\Program Files (x86)\checkmk\service\plugins
|
||||
checkmk_windows_local_dir: C:\ProgramData\checkmk\agent\local
|
||||
checkmk_monitoring_scripts_dir: '{{ lookup(''ansible.builtin.env'', ''AIM_CHECKMK_MONITORING_SCRIPTS_DIR'')
|
||||
| default(''/etc/checkmk_monitoring_scripts'', true) }}'
|
||||
checkmk_windows_scripts_dir: '{{ checkmk_monitoring_scripts_dir }}/Scripts Windows'
|
||||
checkmk_linux_scripts_dir: '{{ checkmk_monitoring_scripts_dir }}/Scripts Linux'
|
||||
checkmk_unifi_mode: auto
|
||||
want_linux_check_certificate: false
|
||||
want_windows_citrix: false
|
||||
want_windows_surebackup: false
|
||||
want_windows_backup: false
|
||||
want_windows_nsp_mailqueue: false
|
||||
want_windows_certificate: false
|
||||
want_windows_veeam_cloud_connect: false
|
||||
want_windows_veeam_backup: false
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
# Shared deployment and cleanup paths/optional switches. No secrets.
|
||||
checkmk_linux_plugin_dir: /usr/lib/check_mk_agent/plugins
|
||||
checkmk_linux_local_dir: /usr/lib/check_mk_agent/local
|
||||
checkmk_linux_config_dir: /etc/check_mk
|
||||
checkmk_windows_plugin_dir: C:\ProgramData\checkmk\agent\plugins
|
||||
checkmk_windows_builtin_plugin_dir: C:\Program Files (x86)\checkmk\service\plugins
|
||||
checkmk_windows_local_dir: C:\ProgramData\checkmk\agent\local
|
||||
checkmk_monitoring_scripts_dir: '{{ lookup(''ansible.builtin.env'', ''AIM_CHECKMK_MONITORING_SCRIPTS_DIR'')
|
||||
| default(''/etc/checkmk_monitoring_scripts'', true) }}'
|
||||
checkmk_windows_scripts_dir: '{{ checkmk_monitoring_scripts_dir }}/Scripts Windows'
|
||||
checkmk_linux_scripts_dir: '{{ checkmk_monitoring_scripts_dir }}/Scripts Linux'
|
||||
checkmk_unifi_mode: auto
|
||||
want_linux_check_certificate: false
|
||||
want_windows_citrix: false
|
||||
want_windows_surebackup: false
|
||||
want_windows_backup: false
|
||||
want_windows_nsp_mailqueue: false
|
||||
want_windows_certificate: false
|
||||
want_windows_veeam_cloud_connect: false
|
||||
want_windows_veeam_backup: false
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
|
||||
- name: Checkmk | Validate UniFi mode
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- checkmk_unifi_mode in ['auto','network','os','disabled']
|
||||
fail_msg: UniFi mode must be auto, network, os or disabled.
|
||||
quiet: true
|
||||
- name: Checkmk | Resolve UniFi mode
|
||||
ansible.builtin.set_fact:
|
||||
_checkmk_unifi_effective: '{{ checkmk_unifi_mode if checkmk_unifi_mode != ''auto'' else (''os'' if is_unifi_os_server
|
||||
| default(false) | bool else (''network'' if is_unifi_controller | default(false) | bool else ''disabled''))
|
||||
}}'
|
||||
- name: Checkmk | Build managed script plan
|
||||
ansible.builtin.set_fact:
|
||||
_checkmk_script_catalog: '{{ _checkmk_windows_catalog if ansible_facts.os_family == ''Windows'' else _checkmk_linux_catalog
|
||||
}}'
|
||||
- name: Checkmk | Resolve selected and obsolete checks
|
||||
ansible.builtin.set_fact:
|
||||
_checkmk_selected_scripts: '{{ _checkmk_script_catalog | selectattr(''enabled'') | list }}'
|
||||
_checkmk_obsolete_scripts: '{{ _checkmk_script_catalog | rejectattr(''enabled'') | list }}'
|
||||
- name: Checkmk | Selected check plan
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
files: '{{ _checkmk_selected_scripts | map(attribute=''filename'') | list }}'
|
||||
unifi_mode: '{{ _checkmk_unifi_effective }}'
|
||||
when: aim_debug | default(false) | bool
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
# Internal managed filenames. Repository scripts are not embedded or rewritten by AIM.
|
||||
_checkmk_windows_catalog:
|
||||
- filename: check-ping.ps1
|
||||
source: '{{ checkmk_windows_scripts_dir }}/check-ping.ps1'
|
||||
enabled: '{{ is_dc | default(false) | bool }}'
|
||||
- filename: veeam_config_backup_status.ps1
|
||||
source: '{{ checkmk_windows_scripts_dir }}/veeam_config_backup_status.ps1'
|
||||
enabled: '{{ has_veeam_vbr | default(false) | bool }}'
|
||||
- filename: veeam_backup_license_status.ps1
|
||||
source: '{{ checkmk_windows_scripts_dir }}/veeam_backup_license_status.ps1'
|
||||
enabled: '{{ has_veeam_vbr | default(false) | bool }}'
|
||||
- filename: veeam_o365_status.ps1
|
||||
source: '{{ checkmk_windows_scripts_dir }}/veeam_o365_status.ps1'
|
||||
destination: custom_plugin
|
||||
enabled: '{{ has_veeam_vbo | default(false) | bool }}'
|
||||
- filename: citrix_sessions_customized.ps1
|
||||
source: '{{ checkmk_windows_scripts_dir }}/citrix_sessions_customized.ps1'
|
||||
destination: custom_plugin
|
||||
enabled: '{{ want_windows_citrix | default(false) | bool }}'
|
||||
- filename: veeam_surebackup_status.ps1
|
||||
source: '{{ checkmk_windows_scripts_dir }}/veeam_surebackup_status.ps1'
|
||||
enabled: '{{ want_windows_surebackup | default(false) | bool }}'
|
||||
- filename: windows-backup.ps1
|
||||
source: '{{ checkmk_windows_scripts_dir }}/windows-backup.ps1'
|
||||
enabled: '{{ want_windows_backup | default(false) | bool }}'
|
||||
- filename: check-nsp-mailqueue.ps1
|
||||
source: '{{ checkmk_windows_scripts_dir }}/check-nsp-mailqueue.ps1'
|
||||
enabled: '{{ want_windows_nsp_mailqueue | default(false) | bool }}'
|
||||
- filename: win_check_cert.ps1
|
||||
source: '{{ checkmk_windows_scripts_dir }}/win_check_cert.ps1'
|
||||
enabled: '{{ want_windows_certificate | default(false) | bool }}'
|
||||
- filename: veeam_cloud_connect_status.ps1
|
||||
source: '{{ checkmk_windows_scripts_dir }}/veeam_cloud_connect_status.ps1'
|
||||
enabled: '{{ want_windows_veeam_cloud_connect | default(false) | bool }}'
|
||||
- filename: veeam_backup_status.ps1
|
||||
source: '{{ checkmk_windows_scripts_dir }}/veeam_backup_status.ps1'
|
||||
destination: custom_plugin
|
||||
enabled: '{{ want_windows_veeam_backup | default(false) | bool }}'
|
||||
_checkmk_linux_catalog:
|
||||
- filename: check_unifi-controller.sh
|
||||
source: '{{ checkmk_linux_scripts_dir }}/check_unifi-controller/check_unifi-controller.sh'
|
||||
enabled: '{{ _checkmk_unifi_effective == ''network'' }}'
|
||||
- filename: check_unifi-os.sh
|
||||
source: '{{ checkmk_linux_scripts_dir }}/check_unifi-controller/check_unifi-os.sh'
|
||||
enabled: '{{ _checkmk_unifi_effective == ''os'' }}'
|
||||
- filename: check_certificate_directory.sh
|
||||
source: '{{ checkmk_linux_scripts_dir }}/check_certificate_directory.sh'
|
||||
enabled: '{{ want_linux_check_certificate | default(false) | bool }}'
|
||||
@@ -0,0 +1,16 @@
|
||||
# checkmk_windows_acl
|
||||
|
||||
Normalizes access on AIM-managed persistent Windows Checkmk files without recursively
|
||||
changing Checkmk directories or unknown/operator files.
|
||||
|
||||
The role enables parent ACL inheritance and guarantees locale-independent well-known
|
||||
principals by SID:
|
||||
|
||||
- SYSTEM (`S-1-5-18`): FullControl
|
||||
- local Administrators (`S-1-5-32-544`): FullControl
|
||||
- ALL APPLICATION PACKAGES (`S-1-15-2-1`): ReadAndExecute
|
||||
- ALL RESTRICTED APPLICATION PACKAGES (`S-1-15-2-2`): ReadAndExecute
|
||||
|
||||
AIM does not add customer-specific administrator/user ACEs. Existing intentional parent
|
||||
or explicit ACEs are not blindly purged. Pass persistent AIM-owned file paths through
|
||||
`checkmk_windows_acl_paths`.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user