aim-web2.1.0rc9
This commit is contained in:
+3
-1
@@ -1,2 +1,4 @@
|
|||||||
**/bak.yml
|
**/bak.yml
|
||||||
**/vault.yml
|
**/vault.yml
|
||||||
|
**/__pycache__
|
||||||
|
**/tests
|
||||||
@@ -1,76 +1,72 @@
|
|||||||
# AIM --- Ansible Inventory Manager
|
# AIM: Ansible Inventory Manager
|
||||||
|
|
||||||
AIM is an operator-focused Python application for managing Ansible
|
**Current candidate: 3.3.0rc4. Canonical Ansible Core: 2.19.11. Service/wire/event API: 1.0.**
|
||||||
customer inventories, access configuration, Vaults and curated playbook
|
|
||||||
execution.
|
|
||||||
|
|
||||||
## Source layout
|
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.
|
||||||
|
|
||||||
``` text
|
## This release
|
||||||
/etc/ansible/scripts/
|
|
||||||
├── README.md
|
The new `aim_output_v1` publisher convention and catalog-owned schemas expose purposeful
|
||||||
├── install.md
|
operation data independently of task progress and per-target outcomes. Nine operations
|
||||||
├── CHANGELOG.md
|
now publish reports: role detection, disk usage, event exports, service recovery, OS
|
||||||
├── docs/
|
patching, Checkmk cleanup, user-config reading, agent installation and config updating.
|
||||||
├── pyproject.toml
|
|
||||||
└── src/
|
Results are finite typed data, not raw Ansible stdout/debug/module dictionaries. Existing
|
||||||
└── aim/
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
AIM uses `/etc/ansible/inventories/<customer>/hosts.yml` as the
|
The deployer discovers the existing AIM interpreter from its recognized launcher. Only
|
||||||
inventory source of truth.
|
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.
|
||||||
|
|
||||||
## Supported platform groups
|
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.
|
||||||
|
|
||||||
- `linux`
|
## Canonical documentation
|
||||||
- `windows`
|
|
||||||
- `sophosxgs`
|
|
||||||
- `pfsense`
|
|
||||||
|
|
||||||
A host can belong to multiple groups/subgroups. Platform groups remain
|
Start at [the documentation index](scripts/docs/README.md). There is one current document
|
||||||
the top-level groups because they define Ansible connection semantics.
|
per topic, one current validation record and one current sanity checklist. Historical
|
||||||
|
changes remain only in [CHANGELOG](scripts/CHANGELOG.md), not competing release guides.
|
||||||
|
|
||||||
## Quick start
|
- [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)
|
||||||
|
|
||||||
See [install.md](install.md) for installation, virtual-environment
|
**Acceptance:** implementation/local tests do not certify this candidate on Windows,
|
||||||
setup, authorization-group configuration and recovery of Git-ignored
|
Linux package managers or a deployed service sandbox. Previous controller successes
|
||||||
runtime data.
|
are historical evidence, not new test passes. External execution is still opt-in and
|
||||||
|
requires the authorized execution account, collection access and writable staging.
|
||||||
Start AIM with:
|
|
||||||
|
|
||||||
``` bash
|
|
||||||
aim
|
|
||||||
```
|
|
||||||
|
|
||||||
Useful UI troubleshooting modes:
|
|
||||||
|
|
||||||
``` bash
|
|
||||||
aim --no-clear
|
|
||||||
aim --plain
|
|
||||||
aim --live-output
|
|
||||||
```
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
- [Installation](install.md)
|
|
||||||
- [Operations](docs/OPERATIONS.md)
|
|
||||||
- [Inventory](docs/INVENTORY.md)
|
|
||||||
- [Windows / WinRM / AD](docs/WINDOWS.md)
|
|
||||||
- [Recovery](docs/RECOVERY.md)
|
|
||||||
- [Security and sensitive data](docs/SECURITY.md)
|
|
||||||
- [Development](docs/DEVELOPMENT.md)
|
|
||||||
- [Changelog](CHANGELOG.md)
|
|
||||||
|
|
||||||
## Important operational rules
|
|
||||||
|
|
||||||
AIM does not use `.hosts.tsv` as inventory state. `hosts.yml` is
|
|
||||||
authoritative.
|
|
||||||
|
|
||||||
Routine host changes use local YAML validation and do not unnecessarily
|
|
||||||
prompt for the Vault password. Explicit inventory validation may invoke
|
|
||||||
`ansible-inventory` and request the customer Vault password when a Vault
|
|
||||||
exists.
|
|
||||||
|
|
||||||
Existing customer-specific values and arbitrary valid YAML structures
|
|
||||||
should be preserved unless an operator explicitly requests an operation
|
|
||||||
that changes them.
|
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
root_dir: /etc/ansible
|
|
||||||
service_user: svc_ansible
|
|
||||||
required_group: root
|
|
||||||
platform_groups:
|
|
||||||
- linux
|
|
||||||
- windows
|
|
||||||
- sophosxgs
|
|
||||||
- pfsense
|
|
||||||
ui:
|
|
||||||
clear_screen: true
|
|
||||||
ascii: false
|
|
||||||
output: compact
|
|
||||||
@@ -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())
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
|
||||||
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCzzsHfog
|
|
||||||
Wv9erYAv5gNSMrAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIFEjnIIVsyXXaoqd
|
|
||||||
VGOGwSPXthf8vzZ7L//SZP3OuJjxAAAAoPoC23Ps5HmvCpJrlOsubdcAjq0Ol65xhOb8VQ
|
|
||||||
OolhyCPFPj/eH1z21w/PvXhL1S8tDsBut27qW8+5dSwT2gqjtt/x2SiOQYMHt6EjGGAISu
|
|
||||||
NNy9L+vRcOFIB4Lo1IE/BMeZRUpgW2GXRFcQH9KgZXh/IWMDqcS5q8GbIT69OUxVUeBg3x
|
|
||||||
Wa5f3MyCUMEmIMkBcdbJrebWXLjDvVYDI3myE=
|
|
||||||
-----END OPENSSH PRIVATE KEY-----
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFEjnIIVsyXXaoqdVGOGwSPXthf8vzZ7L//SZP3OuJjx svc_ansible@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
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# Linux / SSH variables
|
# Linux / SSH variables
|
||||||
ansible_connection: ssh
|
ansible_connection: ssh
|
||||||
ansible_user: svc_ansible
|
ansible_user: svc_bf-ansible
|
||||||
ansible_private_key_file:
|
ansible_private_key_file:
|
||||||
/etc/ansible/inventories/desq_gaming/group_vars/linux/.ssh/svc_ansible
|
/etc/ansible/inventories/desq_gaming/group_vars/linux/.ssh/svc_bf-ansible
|
||||||
ansible_ssh_pass: '{{ ansible_vault_linux_ssh_pass }}'
|
ansible_ssh_pass: '{{ ansible_vault_linux_ssh_pass }}'
|
||||||
ansible_become_method: sudo
|
ansible_become_method: sudo
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ ansible_connection: winrm
|
|||||||
ansible_port: 5986
|
ansible_port: 5986
|
||||||
ansible_winrm_transport: ntlm
|
ansible_winrm_transport: ntlm
|
||||||
ansible_winrm_server_cert_validation: ignore
|
ansible_winrm_server_cert_validation: ignore
|
||||||
ansible_user: svc_ansible
|
ansible_user: svc_bf-ansible
|
||||||
ansible_password: '{{ vault_windows_ansible_password }}'
|
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 }}'
|
||||||
@@ -1,4 +1,99 @@
|
|||||||
all:
|
all:
|
||||||
children:
|
children:
|
||||||
desq_gaming:
|
desq_gaming:
|
||||||
children: {}
|
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
|
||||||
@@ -29,7 +29,7 @@ all:
|
|||||||
hosts:
|
hosts:
|
||||||
dewepve01.desq-gaming.lan:
|
dewepve01.desq-gaming.lan:
|
||||||
ansible_host: 192.168.99.31
|
ansible_host: 192.168.99.31
|
||||||
|
|
||||||
management:
|
management:
|
||||||
hosts:
|
hosts:
|
||||||
dewesrv-ansible01.desq-gaming.lan:
|
dewesrv-ansible01.desq-gaming.lan:
|
||||||
@@ -96,4 +96,10 @@ all:
|
|||||||
dewesrv-wazuh01.desq-gaming.lan:
|
dewesrv-wazuh01.desq-gaming.lan:
|
||||||
ansible_host: 192.168.20.13
|
ansible_host: 192.168.20.13
|
||||||
dewesrv-wiki01.desq-gaming.lan:
|
dewesrv-wiki01.desq-gaming.lan:
|
||||||
ansible_host: 192.168.20.36
|
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
@@ -1,64 +0,0 @@
|
|||||||
---
|
|
||||||
- name: "Checkmk | Cleanup agent"
|
|
||||||
hosts: all
|
|
||||||
gather_facts: true
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
- name: "Windows | Remove installed Checkmk agent"
|
|
||||||
when: ansible_facts['os_family'] == 'Windows'
|
|
||||||
ansible.windows.win_powershell:
|
|
||||||
script: |
|
|
||||||
$Ansible.Changed = $false
|
|
||||||
$uninstallRoots = @(
|
|
||||||
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
|
|
||||||
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
|
|
||||||
)
|
|
||||||
|
|
||||||
$products = foreach ($root in $uninstallRoots) {
|
|
||||||
if (Test-Path -LiteralPath $root) {
|
|
||||||
Get-ChildItem -LiteralPath $root -ErrorAction SilentlyContinue |
|
|
||||||
ForEach-Object {
|
|
||||||
$product = Get-ItemProperty -LiteralPath $_.PSPath -ErrorAction SilentlyContinue
|
|
||||||
if ($product.DisplayName -like 'Check MK Agent*' -or
|
|
||||||
$product.DisplayName -like 'Checkmk Agent*') {
|
|
||||||
[PSCustomObject]@{
|
|
||||||
ProductCode = $_.PSChildName
|
|
||||||
DisplayName = $product.DisplayName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($product in $products) {
|
|
||||||
if ($product.ProductCode -match '^\{[0-9A-Fa-f-]+\}$') {
|
|
||||||
$process = Start-Process -FilePath 'msiexec.exe' `
|
|
||||||
-ArgumentList "/x $($product.ProductCode) /qn /norestart" `
|
|
||||||
-Wait -PassThru
|
|
||||||
|
|
||||||
if ($process.ExitCode -notin @(0, 1605, 1614, 3010)) {
|
|
||||||
throw "Failed to uninstall $($product.DisplayName). MSI exit code: $($process.ExitCode)"
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($process.ExitCode -in @(0, 3010)) {
|
|
||||||
$Ansible.Changed = $true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$Ansible.Result = @{
|
|
||||||
removed_products = @($products.DisplayName)
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: "Debian | Remove Checkmk agent"
|
|
||||||
when: ansible_facts['os_family'] == 'Debian'
|
|
||||||
ansible.builtin.apt:
|
|
||||||
name: check-mk-agent
|
|
||||||
state: absent
|
|
||||||
purge: true
|
|
||||||
|
|
||||||
- name: "RedHat | Remove Checkmk agent"
|
|
||||||
when: ansible_facts['os_family'] == 'RedHat'
|
|
||||||
ansible.builtin.dnf:
|
|
||||||
name: check-mk-agent
|
|
||||||
state: absent
|
|
||||||
@@ -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
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
- name: "Checkmk | Deploy agent, scripts and configuration"
|
|
||||||
hosts: all
|
|
||||||
gather_facts: true
|
|
||||||
|
|
||||||
roles:
|
|
||||||
- checkmk_agent
|
|
||||||
- server_role_selection
|
|
||||||
- checkmk_scripts
|
|
||||||
- checkmk_agent_config
|
|
||||||
@@ -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 }}'
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
- name: "Checkmk | Update agent configuration"
|
|
||||||
hosts: all
|
|
||||||
gather_facts: true
|
|
||||||
|
|
||||||
roles:
|
|
||||||
- server_role_selection
|
|
||||||
- checkmk_agent_config
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
- name: "Checkmk | Update monitoring scripts"
|
|
||||||
hosts: all
|
|
||||||
gather_facts: true
|
|
||||||
|
|
||||||
roles:
|
|
||||||
- server_role_selection
|
|
||||||
- checkmk_scripts
|
|
||||||
@@ -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 }}'
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
---
|
|
||||||
- name: "Debug | Disk usage"
|
|
||||||
hosts: all
|
|
||||||
gather_facts: true
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
- name: "Linux | Collect filesystem usage"
|
|
||||||
when: ansible_facts['os_family'] != 'Windows'
|
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: df -hP -x tmpfs -x devtmpfs
|
|
||||||
register: disk_usage_linux
|
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: "Linux | Show filesystem usage"
|
|
||||||
when: ansible_facts['os_family'] != 'Windows'
|
|
||||||
ansible.builtin.debug:
|
|
||||||
var: disk_usage_linux.stdout_lines
|
|
||||||
|
|
||||||
- name: "Windows | Collect filesystem drive usage"
|
|
||||||
when: ansible_facts['os_family'] == 'Windows'
|
|
||||||
ansible.windows.win_powershell:
|
|
||||||
script: |
|
|
||||||
Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" |
|
|
||||||
Select-Object DeviceID,
|
|
||||||
@{Name='SizeGB';Expression={[math]::Round($_.Size / 1GB, 2)}},
|
|
||||||
@{Name='FreeGB';Expression={[math]::Round($_.FreeSpace / 1GB, 2)}},
|
|
||||||
@{Name='UsedPercent';Expression={
|
|
||||||
if ($_.Size -gt 0) {
|
|
||||||
[math]::Round((($_.Size - $_.FreeSpace) / $_.Size) * 100, 1)
|
|
||||||
} else { 0 }
|
|
||||||
}}
|
|
||||||
register: disk_usage_windows
|
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: "Windows | Show filesystem drive usage"
|
|
||||||
when: ansible_facts['os_family'] == 'Windows'
|
|
||||||
ansible.builtin.debug:
|
|
||||||
var: disk_usage_windows.output
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
---
|
|
||||||
- name: "Debug | Ping hosts"
|
|
||||||
hosts: all
|
|
||||||
gather_facts: false
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
- name: "Windows | WinRM ping"
|
|
||||||
when: ansible_connection | default('') == 'winrm'
|
|
||||||
ansible.windows.win_ping:
|
|
||||||
|
|
||||||
- name: "Linux | Ansible ping"
|
|
||||||
when: ansible_connection | default('ssh') != 'winrm'
|
|
||||||
ansible.builtin.ping:
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
---
|
|
||||||
- name: "Debug | Server Role Selection"
|
|
||||||
hosts: all
|
|
||||||
gather_facts: true
|
|
||||||
|
|
||||||
roles:
|
|
||||||
- server_role_selection
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
- name: "Debug | Display detected server roles"
|
|
||||||
ansible.builtin.debug:
|
|
||||||
msg:
|
|
||||||
host: "{{ inventory_hostname }}"
|
|
||||||
os_family: "{{ ansible_facts['os_family'] | default('unknown') }}"
|
|
||||||
|
|
||||||
windows_roles:
|
|
||||||
domain_controller: "{{ is_dc | default(false) | bool }}"
|
|
||||||
dhcp_server: "{{ is_dhcp_server | default(false) | bool }}"
|
|
||||||
hyperv_host: "{{ is_hyperv_host | default(false) | bool }}"
|
|
||||||
|
|
||||||
veeam:
|
|
||||||
vbr: "{{ has_veeam_vbr | default(false) | bool }}"
|
|
||||||
vbo: "{{ has_veeam_vbo | default(false) | bool }}"
|
|
||||||
enterprise_manager: "{{ has_veeam_em | default(false) | bool }}"
|
|
||||||
|
|
||||||
linux_roles:
|
|
||||||
unifi_controller: "{{ is_unifi_controller | default(false) | bool }}"
|
|
||||||
|
|
||||||
optional_features:
|
|
||||||
linux_certificate_check: "{{ want_linux_check_certificate | default(false) | bool }}"
|
|
||||||
windows_citrix: "{{ want_windows_citrix | default(false) | bool }}"
|
|
||||||
windows_surebackup: "{{ want_windows_surebackup | default(false) | bool }}"
|
|
||||||
windows_backup: "{{ want_windows_backup | default(false) | bool }}"
|
|
||||||
|
|
||||||
- name: "Debug | Display Checkmk script deployment decisions"
|
|
||||||
ansible.builtin.debug:
|
|
||||||
msg:
|
|
||||||
linux:
|
|
||||||
unifi_controller:
|
|
||||||
deploy: "{{ is_unifi_controller | default(false) | bool }}"
|
|
||||||
reason: "UniFi Controller detected"
|
|
||||||
scripts:
|
|
||||||
- "check_unifi-controller.sh"
|
|
||||||
- "unifi.cfg"
|
|
||||||
|
|
||||||
certificate_directory:
|
|
||||||
deploy: "{{ want_linux_check_certificate | default(false) | bool }}"
|
|
||||||
reason: "Certificate directory monitoring explicitly enabled"
|
|
||||||
scripts:
|
|
||||||
- "check_certificate_directory.sh"
|
|
||||||
|
|
||||||
windows:
|
|
||||||
check_ping:
|
|
||||||
deploy: "{{ is_dc | default(false) | bool }}"
|
|
||||||
reason: "Domain Controller"
|
|
||||||
scripts:
|
|
||||||
- "check-ping.ps1"
|
|
||||||
|
|
||||||
veeam_config_backup:
|
|
||||||
deploy: "{{ has_veeam_vbr | default(false) | bool }}"
|
|
||||||
reason: "Veeam Backup & Replication detected"
|
|
||||||
scripts:
|
|
||||||
- "veeam_config_backup_status.ps1"
|
|
||||||
|
|
||||||
veeam_o365:
|
|
||||||
deploy: "{{ has_veeam_vbo | default(false) | bool }}"
|
|
||||||
reason: "Veeam Backup for Microsoft 365 detected"
|
|
||||||
scripts:
|
|
||||||
- "veeam_o365_status.ps1"
|
|
||||||
|
|
||||||
citrix_sessions:
|
|
||||||
deploy: "{{ want_windows_citrix | default(false) | bool }}"
|
|
||||||
reason: "Citrix monitoring explicitly enabled"
|
|
||||||
scripts:
|
|
||||||
- "citrix_sessions_customized.ps1"
|
|
||||||
|
|
||||||
veeam_surebackup:
|
|
||||||
deploy: "{{ want_windows_surebackup | default(false) | bool }}"
|
|
||||||
reason: "Veeam SureBackup monitoring explicitly enabled"
|
|
||||||
scripts:
|
|
||||||
- "veeam_surebackup_status.ps1"
|
|
||||||
|
|
||||||
windows_backup:
|
|
||||||
deploy: "{{ want_windows_backup | default(false) | bool }}"
|
|
||||||
reason: "Windows Backup monitoring explicitly enabled"
|
|
||||||
scripts:
|
|
||||||
- "windows-backup.ps1"
|
|
||||||
@@ -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}
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
---
|
|
||||||
- name: "Maintenance | Backup system logs"
|
|
||||||
hosts: all
|
|
||||||
gather_facts: true
|
|
||||||
|
|
||||||
vars:
|
|
||||||
windows_event_log_backup_dir: 'C:\\Windows\\Temp\\EventLogBackup'
|
|
||||||
windows_event_logs_to_backup:
|
|
||||||
- Application
|
|
||||||
- System
|
|
||||||
- Security
|
|
||||||
|
|
||||||
linux_log_backup_dir: /var/backups/system-logs
|
|
||||||
linux_journal_since: "-24h"
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
- name: "Windows | Ensure event log backup directory exists"
|
|
||||||
when: ansible_facts['os_family'] == 'Windows'
|
|
||||||
ansible.windows.win_file:
|
|
||||||
path: "{{ windows_event_log_backup_dir }}"
|
|
||||||
state: directory
|
|
||||||
|
|
||||||
- name: "Windows | Export event logs"
|
|
||||||
when: ansible_facts['os_family'] == 'Windows'
|
|
||||||
ansible.windows.win_command: >-
|
|
||||||
wevtutil epl {{ item }} "{{ windows_event_log_backup_dir }}\\{{ item }}.evtx" /ow:true
|
|
||||||
loop: "{{ windows_event_logs_to_backup }}"
|
|
||||||
changed_when: true
|
|
||||||
|
|
||||||
- name: "Linux | Ensure system log backup directory exists"
|
|
||||||
when: ansible_facts['os_family'] != 'Windows'
|
|
||||||
become: true
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ linux_log_backup_dir }}"
|
|
||||||
state: directory
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0750"
|
|
||||||
|
|
||||||
- name: "Linux | Check whether systemd journal is available"
|
|
||||||
when: ansible_facts['os_family'] != 'Windows'
|
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: journalctl --version
|
|
||||||
register: journalctl_available
|
|
||||||
changed_when: false
|
|
||||||
failed_when: false
|
|
||||||
|
|
||||||
- name: "Linux | Export systemd journal"
|
|
||||||
when:
|
|
||||||
- ansible_facts['os_family'] != 'Windows'
|
|
||||||
- journalctl_available.rc == 0
|
|
||||||
become: true
|
|
||||||
ansible.builtin.shell: >-
|
|
||||||
journalctl --since {{ linux_journal_since | quote }} --no-pager
|
|
||||||
> {{ (linux_log_backup_dir ~ '/journal.log') | quote }}
|
|
||||||
args:
|
|
||||||
executable: /bin/sh
|
|
||||||
changed_when: true
|
|
||||||
|
|
||||||
- name: "Linux | Backup traditional system logs"
|
|
||||||
when:
|
|
||||||
- ansible_facts['os_family'] != 'Windows'
|
|
||||||
- journalctl_available.rc != 0
|
|
||||||
become: true
|
|
||||||
ansible.builtin.shell: |
|
|
||||||
set -e
|
|
||||||
for file in /var/log/syslog /var/log/messages /var/log/auth.log /var/log/secure; do
|
|
||||||
if [ -f "$file" ]; then
|
|
||||||
cp -p "$file" "{{ linux_log_backup_dir }}/$(basename "$file")"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
args:
|
|
||||||
executable: /bin/sh
|
|
||||||
changed_when: true
|
|
||||||
@@ -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
|
||||||
@@ -1,35 +1,70 @@
|
|||||||
---
|
---
|
||||||
- name: "Maintenance | Patch operating system"
|
# PURPOSE: Patch operating systems
|
||||||
hosts: all
|
# 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
|
gather_facts: true
|
||||||
|
pre_tasks:
|
||||||
tasks:
|
- name: AIM | Validate diagnostics option
|
||||||
- name: "Debian | Update package cache and upgrade packages"
|
ansible.builtin.assert:
|
||||||
when: ansible_facts['os_family'] == 'Debian'
|
that:
|
||||||
ansible.builtin.apt:
|
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
|
||||||
update_cache: true
|
'false']
|
||||||
upgrade: dist
|
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||||
|
quiet: true
|
||||||
- name: "RedHat | Upgrade installed packages"
|
- name: AIM | Reject mixed platform membership
|
||||||
when: ansible_facts['os_family'] == 'RedHat'
|
ansible.builtin.assert:
|
||||||
ansible.builtin.dnf:
|
that:
|
||||||
name: '*'
|
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
|
||||||
state: latest
|
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
|
||||||
|
quiet: true
|
||||||
- name: "Windows | Install available updates"
|
- name: AIM | Execution context
|
||||||
when: ansible_facts['os_family'] == 'Windows'
|
|
||||||
ansible.windows.win_updates:
|
|
||||||
category_names:
|
|
||||||
- CriticalUpdates
|
|
||||||
- SecurityUpdates
|
|
||||||
- UpdateRollups
|
|
||||||
- Updates
|
|
||||||
reboot: false
|
|
||||||
register: windows_updates
|
|
||||||
|
|
||||||
- name: "Windows | Report reboot requirement"
|
|
||||||
when:
|
|
||||||
- ansible_facts['os_family'] == 'Windows'
|
|
||||||
- windows_updates.reboot_required | default(false)
|
|
||||||
ansible.builtin.debug:
|
ansible.builtin.debug:
|
||||||
msg: "Windows updates were installed and a reboot is required. No reboot was performed."
|
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
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
---
|
|
||||||
- name: "Maintenance | Reboot systems"
|
|
||||||
hosts: all
|
|
||||||
gather_facts: true
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
- name: "Windows | Reboot system"
|
|
||||||
when: ansible_facts['os_family'] == 'Windows'
|
|
||||||
ansible.windows.win_reboot:
|
|
||||||
reboot_timeout: 1800
|
|
||||||
|
|
||||||
- name: "Linux | Reboot system"
|
|
||||||
when: ansible_facts['os_family'] != 'Windows'
|
|
||||||
ansible.builtin.reboot:
|
|
||||||
reboot_timeout: 1800
|
|
||||||
@@ -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
|
||||||
@@ -1,82 +1,37 @@
|
|||||||
---
|
---
|
||||||
- name: "Maintenance | Start stopped automatic services"
|
# PURPOSE: Start stopped automatic services
|
||||||
hosts: all
|
# DESCRIPTION: Start eligible stopped services, apply optional include/exclude lists and report partial failures.
|
||||||
gather_facts: true
|
# TARGETS: windows
|
||||||
|
# INPUTS (omitted values inherit inventory / role defaults):
|
||||||
tasks:
|
# aim_debug [bool]: false
|
||||||
- name: "Windows | Start stopped automatic services"
|
# maintenance_service_include [list]: []
|
||||||
when: ansible_facts['os_family'] == 'Windows'
|
# maintenance_service_exclude [list]: []
|
||||||
ansible.windows.win_powershell:
|
# maintenance_service_fail_on_error [bool]: true
|
||||||
script: |
|
# AUTH: existing inventory / Vault credentials; no embedded passwords.
|
||||||
$Ansible.Changed = $false
|
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
|
||||||
$started = @()
|
# playbooks/maintenance_start_stopped_services.yml --limit <host> --vault-id <customer>@prompt
|
||||||
|
- name: Maintenance | Start automatic services
|
||||||
Get-CimInstance Win32_Service |
|
hosts: windows
|
||||||
Where-Object {
|
gather_facts: false
|
||||||
$_.StartMode -eq 'Auto' -and
|
pre_tasks:
|
||||||
$_.State -ne 'Running'
|
- name: AIM | Validate diagnostics option
|
||||||
} |
|
ansible.builtin.assert:
|
||||||
ForEach-Object {
|
that:
|
||||||
try {
|
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
|
||||||
Start-Service -Name $_.Name -ErrorAction Stop
|
'false']
|
||||||
$started += $_.Name
|
fail_msg: aim_debug must be a YAML/JSON boolean.
|
||||||
$Ansible.Changed = $true
|
quiet: true
|
||||||
}
|
- name: AIM | Reject mixed platform membership
|
||||||
catch {
|
ansible.builtin.assert:
|
||||||
Write-Warning "Could not start service $($_.Name): $($_.Exception.Message)"
|
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
|
||||||
$Ansible.Result = @{
|
- name: AIM | Execution context
|
||||||
started_services = $started
|
|
||||||
}
|
|
||||||
register: started_services_windows
|
|
||||||
|
|
||||||
- name: "Windows | Show started services"
|
|
||||||
when: ansible_facts['os_family'] == 'Windows'
|
|
||||||
ansible.builtin.debug:
|
ansible.builtin.debug:
|
||||||
var: started_services_windows.result.started_services
|
msg:
|
||||||
|
host: '{{ inventory_hostname }}'
|
||||||
- name: "Linux | Collect service facts"
|
diagnostics: Enabled; secret values are never included by this task.
|
||||||
when: ansible_facts['os_family'] != 'Windows'
|
when: aim_debug | default(false) | bool
|
||||||
ansible.builtin.service_facts:
|
roles:
|
||||||
|
- role: maintenance_start_stopped_services
|
||||||
- name: "Linux | Start stopped enabled systemd services"
|
|
||||||
when:
|
|
||||||
- ansible_facts['os_family'] != 'Windows'
|
|
||||||
- ansible_facts['service_mgr'] == 'systemd'
|
|
||||||
- item.value.status | default('') == 'enabled'
|
|
||||||
- item.value.state | default('') != 'running'
|
|
||||||
become: true
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: "{{ item.key }}"
|
|
||||||
state: started
|
|
||||||
loop: "{{ ansible_facts.services | dict2items }}"
|
|
||||||
loop_control:
|
|
||||||
label: "{{ item.key }}"
|
|
||||||
register: started_services_linux
|
|
||||||
failed_when: false
|
|
||||||
|
|
||||||
- name: "Linux | Show services that were started"
|
|
||||||
when:
|
|
||||||
- ansible_facts['os_family'] != 'Windows'
|
|
||||||
- ansible_facts['service_mgr'] == 'systemd'
|
|
||||||
ansible.builtin.debug:
|
|
||||||
msg: >-
|
|
||||||
{{
|
|
||||||
started_services_linux.results
|
|
||||||
| default([])
|
|
||||||
| selectattr('changed', 'defined')
|
|
||||||
| selectattr('changed')
|
|
||||||
| map(attribute='item.key')
|
|
||||||
| list
|
|
||||||
}}
|
|
||||||
|
|
||||||
- name: "Linux | Report unsupported service manager"
|
|
||||||
when:
|
|
||||||
- ansible_facts['os_family'] != 'Windows'
|
|
||||||
- ansible_facts['service_mgr'] != 'systemd'
|
|
||||||
ansible.builtin.debug:
|
|
||||||
msg: >-
|
|
||||||
Automatic stopped-service recovery currently supports systemd hosts only.
|
|
||||||
Detected service manager: {{ ansible_facts['service_mgr'] }}
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
checkmk_linux_tmp_path: "/tmp"
|
|
||||||
checkmk_windows_tmp_path: "C:\\Windows\\Temp"
|
checkmk_linux_tmp_path: /tmp
|
||||||
checkmk_deb_filename: "check-mk-agent.deb"
|
checkmk_windows_tmp_path: C:\Windows\Temp
|
||||||
checkmk_rpm_filename: "check-mk-agent.rpm"
|
checkmk_deb_filename: check-mk-agent.deb
|
||||||
checkmk_msi_filename: "check_mk_agent.msi"
|
checkmk_rpm_filename: check-mk-agent.rpm
|
||||||
checkmk_linux_socket_name: "check-mk-agent.socket"
|
checkmk_msi_filename: check_mk_agent.msi
|
||||||
checkmk_linux_service_name: "check-mk-agent"
|
checkmk_agent_files_dir: '{{ lookup(''ansible.builtin.env'', ''AIM_CHECKMK_AGENT_FILES_DIR'') | default(role_path
|
||||||
checkmk_windows_service_name: "CheckMkService"
|
~ ''/files'', true) }}'
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
Place the Checkmk agent packages here:
|
# Staged agent packages
|
||||||
|
|
||||||
- `check-mk-agent.deb`
|
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.
|
||||||
- `check-mk-agent.rpm`
|
|
||||||
- `check_mk_agent.msi`
|
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
---
|
|
||||||
- name: "Enable & start Checkmk socket (if present)"
|
|
||||||
listen: "checkmk | linux | agent-ensure-running"
|
|
||||||
when: ansible_facts['os_family'] != "Windows"
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: "{{ checkmk_linux_socket_name }}"
|
|
||||||
enabled: true
|
|
||||||
state: started
|
|
||||||
daemon_reload: true
|
|
||||||
failed_when: false
|
|
||||||
|
|
||||||
- name: "Enable & start Checkmk service (fallback/if present)"
|
|
||||||
listen: "checkmk | linux | agent-ensure-running"
|
|
||||||
when: ansible_facts['os_family'] != "Windows"
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: "{{ checkmk_linux_service_name }}"
|
|
||||||
enabled: true
|
|
||||||
state: started
|
|
||||||
daemon_reload: true
|
|
||||||
failed_when: false
|
|
||||||
|
|
||||||
- name: "Windows | Detect Checkmk service name"
|
|
||||||
listen: "checkmk | windows | agent-ensure-running"
|
|
||||||
when: ansible_facts['os_family'] == "Windows"
|
|
||||||
ansible.windows.win_powershell:
|
|
||||||
script: |
|
|
||||||
$candidates = @('CheckMkService','Check_MK_Agent')
|
|
||||||
foreach ($n in $candidates) {
|
|
||||||
$svc = Get-Service -Name $n -ErrorAction SilentlyContinue
|
|
||||||
if ($svc) { $svc.Name; break }
|
|
||||||
}
|
|
||||||
register: cmk_detect
|
|
||||||
failed_when: false
|
|
||||||
|
|
||||||
- name: "Windows | Set detected service name"
|
|
||||||
listen: "checkmk | windows | agent-ensure-running"
|
|
||||||
when: ansible_facts['os_family'] == "Windows"
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
cmk_service_name: >-
|
|
||||||
{{
|
|
||||||
(cmk_detect.output[0] | default('') | trim)
|
|
||||||
if (cmk_detect.output | default([]) | length > 0)
|
|
||||||
else (checkmk_windows_service_name | default('CheckMkService'))
|
|
||||||
}}
|
|
||||||
|
|
||||||
- name: "Windows | Ensure Checkmk agent service running"
|
|
||||||
listen: "checkmk | windows | agent-ensure-running"
|
|
||||||
when:
|
|
||||||
- ansible_facts['os_family'] == "Windows"
|
|
||||||
- (cmk_service_name | default('')) | length > 0
|
|
||||||
ansible.windows.win_service:
|
|
||||||
name: "{{ cmk_service_name }}"
|
|
||||||
start_mode: auto
|
|
||||||
state: started
|
|
||||||
failed_when: false
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
galaxy_info:
|
|
||||||
role_name: checkmk_agent
|
|
||||||
description: Install Checkmk agent from local packages
|
|
||||||
min_ansible_version: "2.18"
|
|
||||||
dependencies: []
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
- name: "Debian | Copy Checkmk agent package"
|
|
||||||
ansible.builtin.copy:
|
|
||||||
src: "{{ checkmk_deb_filename }}"
|
|
||||||
dest: "{{ checkmk_linux_tmp_path }}/{{ checkmk_deb_filename }}"
|
|
||||||
mode: "0644"
|
|
||||||
|
|
||||||
- name: "Debian | Install Checkmk agent"
|
|
||||||
ansible.builtin.apt:
|
|
||||||
deb: "{{ checkmk_linux_tmp_path }}/{{ checkmk_deb_filename }}"
|
|
||||||
notify: "checkmk | linux | agent-ensure-running"
|
|
||||||
@@ -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
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
---
|
---
|
||||||
- name: Include Debian installation
|
|
||||||
ansible.builtin.include_tasks: debian.yml
|
|
||||||
when: ansible_facts['os_family'] == 'Debian'
|
|
||||||
|
|
||||||
- name: Include RedHat installation
|
- name: Checkmk | Supported installer
|
||||||
ansible.builtin.include_tasks: redhat.yml
|
ansible.builtin.assert:
|
||||||
when: ansible_facts['os_family'] == 'RedHat'
|
that:
|
||||||
|
- ansible_facts.os_family in ['Debian','RedHat','Windows']
|
||||||
- name: Include Windows installation
|
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
|
ansible.builtin.include_tasks: windows.yml
|
||||||
when: ansible_facts['os_family'] == 'Windows'
|
when: ansible_facts['os_family'] == "Windows"
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
- name: "RedHat | Copy Checkmk agent package"
|
|
||||||
ansible.builtin.copy:
|
|
||||||
src: "{{ checkmk_rpm_filename }}"
|
|
||||||
dest: "{{ checkmk_linux_tmp_path }}/{{ checkmk_rpm_filename }}"
|
|
||||||
mode: "0644"
|
|
||||||
|
|
||||||
- name: "RedHat | Install Checkmk agent"
|
|
||||||
ansible.builtin.package:
|
|
||||||
name: "{{ checkmk_linux_tmp_path }}/{{ checkmk_rpm_filename }}"
|
|
||||||
state: present
|
|
||||||
notify: "checkmk | linux | agent-ensure-running"
|
|
||||||
@@ -1,11 +1,28 @@
|
|||||||
---
|
---
|
||||||
- name: "Windows | Copy Checkmk agent MSI"
|
|
||||||
ansible.windows.win_copy:
|
|
||||||
src: "{{ checkmk_msi_filename }}"
|
|
||||||
dest: "{{ checkmk_windows_tmp_path }}\\{{ checkmk_msi_filename }}"
|
|
||||||
|
|
||||||
- name: "Windows | Install Checkmk agent from local MSI"
|
- 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:
|
ansible.windows.win_package:
|
||||||
path: "{{ checkmk_windows_tmp_path }}\\{{ checkmk_msi_filename }}"
|
path: '{{ checkmk_windows_tmp_path }}\{{ checkmk_msi_filename }}'
|
||||||
state: present
|
state: present
|
||||||
notify: "checkmk | windows | agent-ensure-running"
|
register: _checkmk_package_install
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
galaxy_info:
|
|
||||||
role_name: checkmk_agent_config
|
|
||||||
description: Render Windows Checkmk agent configuration from detected server roles
|
|
||||||
min_ansible_version: "2.18"
|
|
||||||
dependencies: []
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
- name: "Debug detected role flags"
|
|
||||||
when: ansible_facts['os_family'] == "Windows"
|
|
||||||
ansible.builtin.debug:
|
|
||||||
msg:
|
|
||||||
dc: "{{ is_dc | default(false) }}"
|
|
||||||
dhcp: "{{ is_dhcp_server | default(false) }}"
|
|
||||||
vbr: "{{ has_veeam_vbr | default(false) }}"
|
|
||||||
vbo: "{{ has_veeam_vbo | default(false) }}"
|
|
||||||
em: "{{ has_veeam_em | default(false) }}"
|
|
||||||
hv: "{{ is_hyperv_host | default(false) }}"
|
|
||||||
timeout_updates: "{{ checkmk_windows_updates_timeout }}"
|
|
||||||
|
|
||||||
- name: "Windows | Render check_mk.user.yml"
|
|
||||||
when: ansible_facts['os_family'] == "Windows"
|
|
||||||
ansible.windows.win_template:
|
|
||||||
src: "windows_check_mk.user.yml.j2"
|
|
||||||
dest: "{{ checkmk_windows_user_cfg }}"
|
|
||||||
backup: true
|
|
||||||
notify: "checkmk | windows | agent-ensure-running"
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
# Managed by Ansible (checkmk_agent_config)
|
|
||||||
# Windows Checkmk Agent user configuration built from detected roles.
|
|
||||||
|
|
||||||
# $CUSTOM_PLUGINS_PATH$ -> ProgramData\checkmk\agent\plugins
|
|
||||||
# $BUILTIN_PLUGINS_PATH$ -> Program Files (x86)\checkmk\service\plugins
|
|
||||||
# $CUSTOM_AGENT_PATH$ -> ProgramData\checkmk\agent
|
|
||||||
# $CUSTOM_LOCAL_PATH$ -> ProgramData\checkmk\agent\local
|
|
||||||
|
|
||||||
global:
|
|
||||||
_only_from:
|
|
||||||
_realtime:
|
|
||||||
enabled: yes
|
|
||||||
timeout: 90
|
|
||||||
port: 6559
|
|
||||||
encrypted: no
|
|
||||||
passphrase: this is my password
|
|
||||||
run:
|
|
||||||
- mem
|
|
||||||
- df
|
|
||||||
- winperf_processor
|
|
||||||
|
|
||||||
winperf:
|
|
||||||
counters:
|
|
||||||
- MSExchangeTransport Queues: msx_queues
|
|
||||||
|
|
||||||
_logfiles:
|
|
||||||
enabled: no
|
|
||||||
|
|
||||||
fileinfo:
|
|
||||||
path: []
|
|
||||||
|
|
||||||
logwatch:
|
|
||||||
logfile: []
|
|
||||||
|
|
||||||
plugins:
|
|
||||||
execution:
|
|
||||||
# --- Built-in defaults ---
|
|
||||||
- pattern: '$BUILTIN_PLUGINS_PATH$\windows_updates.vbs'
|
|
||||||
run: yes
|
|
||||||
async: yes
|
|
||||||
timeout: {{ checkmk_windows_updates_timeout }}
|
|
||||||
cache_age: {{ checkmk_windows_updates_cache }}
|
|
||||||
retry_count: 0
|
|
||||||
|
|
||||||
- pattern: '$BUILTIN_PLUGINS_PATH$\mk_inventory.vbs'
|
|
||||||
run: yes
|
|
||||||
async: yes
|
|
||||||
timeout: {{ checkmk_mk_inventory_timeout }}
|
|
||||||
cache_age: 3600
|
|
||||||
|
|
||||||
- pattern: '$BUILTIN_PLUGINS_PATH$\mssql.vbs'
|
|
||||||
run: yes
|
|
||||||
timeout: {{ checkmk_plugins_default_timeout }}
|
|
||||||
|
|
||||||
{% if has_veeam_vbr | default(false) %}
|
|
||||||
# --- Veeam Backup & Replication ---
|
|
||||||
- pattern: '$BUILTIN_PLUGINS_PATH$\veeam_backup_status.ps1'
|
|
||||||
run: yes
|
|
||||||
async: yes
|
|
||||||
timeout: {{ checkmk_plugins_default_timeout }}
|
|
||||||
cache_age: {{ checkmk_plugins_default_cache }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if is_dc | default(false) %}
|
|
||||||
# --- Domain Controller ---
|
|
||||||
- pattern: '$BUILTIN_PLUGINS_PATH$\ad_replication.bat'
|
|
||||||
run: yes
|
|
||||||
timeout: {{ checkmk_plugins_default_timeout }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if is_dhcp_server | default(false) %}
|
|
||||||
# --- DHCP Server ---
|
|
||||||
- pattern: '$BUILTIN_PLUGINS_PATH$\win_dhcp_pools.bat'
|
|
||||||
run: yes
|
|
||||||
timeout: {{ checkmk_plugins_default_timeout }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
# --- Generic patterns / precedence ---
|
|
||||||
- pattern: '$CUSTOM_PLUGINS_PATH$\*.ps1'
|
|
||||||
run: yes
|
|
||||||
async: yes
|
|
||||||
timeout: {{ checkmk_plugins_default_timeout }}
|
|
||||||
cache_age: {{ checkmk_plugins_default_cache }}
|
|
||||||
|
|
||||||
- pattern: '$CUSTOM_PLUGINS_PATH$\*.*'
|
|
||||||
run: yes
|
|
||||||
timeout: {{ checkmk_plugins_default_timeout }}
|
|
||||||
|
|
||||||
- pattern: '$BUILTIN_PLUGINS_PATH$\*.*'
|
|
||||||
run: no
|
|
||||||
timeout: {{ checkmk_plugins_default_timeout }}
|
|
||||||
|
|
||||||
- pattern: '*'
|
|
||||||
run: no
|
|
||||||
|
|
||||||
{% for p in (checkmk_extra_plugin_patterns | default([])) %}
|
|
||||||
- pattern: '{{ p.pattern }}'
|
|
||||||
{% if p.run is defined %}
|
|
||||||
run: {{ p.run | bool }}
|
|
||||||
{% endif %}
|
|
||||||
{% if p.async is defined %}
|
|
||||||
async: {{ p.async | bool }}
|
|
||||||
{% endif %}
|
|
||||||
{% if p.timeout is defined %}
|
|
||||||
timeout: {{ p.timeout }}
|
|
||||||
{% endif %}
|
|
||||||
{% if p.cache_age is defined %}
|
|
||||||
cache_age: {{ p.cache_age }}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
local:
|
|
||||||
_execution:
|
|
||||||
- pattern: '*.*'
|
|
||||||
run: yes
|
|
||||||
{% for l in (checkmk_extra_local_patterns | default([])) %}
|
|
||||||
- pattern: '{{ l.pattern }}'
|
|
||||||
{% if l.run is defined %}
|
|
||||||
run: {{ l.run | bool }}
|
|
||||||
{% endif %}
|
|
||||||
{% if l.async is defined %}
|
|
||||||
async: {{ l.async | bool }}
|
|
||||||
{% endif %}
|
|
||||||
{% if l.timeout is defined %}
|
|
||||||
timeout: {{ l.timeout }}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
mrpe:
|
|
||||||
config: []
|
|
||||||
@@ -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.
|
||||||
+3
-2
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
checkmk_windows_user_cfg: "C:\\ProgramData\\checkmk\\agent\\check_mk.user.yml"
|
# 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_timeout: 3600
|
||||||
checkmk_windows_updates_cache: 43200
|
checkmk_windows_updates_cache: 43200
|
||||||
checkmk_mk_inventory_timeout: 120
|
checkmk_mk_inventory_timeout: 120
|
||||||
checkmk_plugins_default_timeout: 120
|
checkmk_plugins_default_timeout: 120
|
||||||
checkmk_plugins_default_cache: 600
|
checkmk_plugins_default_cache: 600
|
||||||
checkmk_extra_plugin_patterns: []
|
checkmk_extra_plugin_patterns: []
|
||||||
checkmk_extra_local_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 }}'
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
checkmk_linux_local_dir: "/usr/lib/check_mk_agent/local"
|
|
||||||
checkmk_linux_config_dir: "/etc/check_mk"
|
|
||||||
checkmk_windows_local_dir: "C:\\ProgramData\\checkmk\\agent\\local"
|
|
||||||
checkmk_linux_scripts_dir: "Linux/local"
|
|
||||||
checkmk_windows_scripts_dir: "Windows/local"
|
|
||||||
|
|
||||||
# Optional checks, disabled until explicitly requested
|
|
||||||
want_linux_check_certificate: false
|
|
||||||
want_windows_citrix: false
|
|
||||||
want_windows_surebackup: false
|
|
||||||
want_windows_backup: false
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user