moose-inventory 2.0 → 2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +2 -0
- data/.gitignore +2 -1
- data/.rubocop.yml +21 -0
- data/BACKLOG.md +630 -8
- data/Gemfile +2 -0
- data/Gemfile.lock +1 -1
- data/README.md +315 -39
- data/Rakefile +2 -0
- data/bin/moose-inventory +2 -1
- data/docs/architecture/architecture-and-trust-boundaries.md +444 -0
- data/docs/compatibility/cli-output-compatibility.md +76 -0
- data/docs/governance/approval-register.md +37 -0
- data/docs/maintenance/database-backup-restore-guidance.md +162 -0
- data/docs/maintenance/package-maintenance-and-agent-boundaries.md +260 -0
- data/docs/process/conformance-gap-analysis-2026-05-28.md +192 -0
- data/docs/product/product-brief.md +161 -0
- data/docs/product/requirements-baseline.md +477 -0
- data/docs/qa/qa-documentation-and-release-gates.md +283 -0
- data/docs/release/package-provenance-hardening.md +126 -0
- data/docs/release/publishing.md +11 -3
- data/docs/release/release-environment-protection.md +70 -0
- data/docs/release/release-readiness.md +23 -4
- data/docs/security/accepted-risk-register.md +84 -0
- data/docs/security/security-privacy-process.md +287 -0
- data/docs/security-audit-2026-05-26-rerun.md +2 -2
- data/docs/ux/cli-workflow-notes.md +287 -0
- data/examples/ansible/ansible.cfg +3 -0
- data/examples/ansible/inventory/moose_inventory.yml +5 -0
- data/examples/ansible/inventory_plugins/moose_inventory.py +100 -0
- data/examples/ci/README.md +16 -0
- data/examples/ci/github-actions/inventory-review.yml +38 -0
- data/examples/ci/inventory/example-snapshot.yml +19 -0
- data/examples/ci/scripts/validate-inventory-snapshot.sh +30 -0
- data/lib/moose_inventory/cli/application.rb +133 -5
- data/lib/moose_inventory/cli/association_rendering.rb +74 -0
- data/lib/moose_inventory/cli/association_rendering_support.rb +89 -0
- data/lib/moose_inventory/cli/audit.rb +62 -0
- data/lib/moose_inventory/cli/audit_recording.rb +40 -0
- data/lib/moose_inventory/cli/child_relation_rendering.rb +110 -0
- data/lib/moose_inventory/cli/console.rb +135 -0
- data/lib/moose_inventory/cli/db.rb +64 -0
- data/lib/moose_inventory/cli/factory.rb +28 -0
- data/lib/moose_inventory/cli/formatter.rb +8 -12
- data/lib/moose_inventory/cli/group.rb +5 -2
- data/lib/moose_inventory/cli/group_add.rb +11 -9
- data/lib/moose_inventory/cli/group_addchild.rb +23 -65
- data/lib/moose_inventory/cli/group_addhost.rb +16 -67
- data/lib/moose_inventory/cli/group_addvar.rb +27 -47
- data/lib/moose_inventory/cli/group_get.rb +8 -42
- data/lib/moose_inventory/cli/group_list.rb +7 -40
- data/lib/moose_inventory/cli/group_listvars.rb +9 -55
- data/lib/moose_inventory/cli/group_rm.rb +12 -10
- data/lib/moose_inventory/cli/group_rmchild.rb +26 -82
- data/lib/moose_inventory/cli/group_rmhost.rb +18 -53
- data/lib/moose_inventory/cli/group_rmvar.rb +30 -41
- data/lib/moose_inventory/cli/group_tags.rb +33 -0
- data/lib/moose_inventory/cli/helpers.rb +68 -1
- data/lib/moose_inventory/cli/host.rb +6 -3
- data/lib/moose_inventory/cli/host_add.rb +69 -29
- data/lib/moose_inventory/cli/host_addgroup.rb +22 -58
- data/lib/moose_inventory/cli/host_addvar.rb +28 -52
- data/lib/moose_inventory/cli/host_get.rb +9 -37
- data/lib/moose_inventory/cli/host_list.rb +24 -21
- data/lib/moose_inventory/cli/host_listvars.rb +9 -62
- data/lib/moose_inventory/cli/host_rm.rb +60 -42
- data/lib/moose_inventory/cli/host_rmgroup.rb +25 -44
- data/lib/moose_inventory/cli/host_rmvar.rb +31 -45
- data/lib/moose_inventory/cli/host_tags.rb +33 -0
- data/lib/moose_inventory/cli/listvars_support.rb +55 -0
- data/lib/moose_inventory/cli/plan_rendering.rb +50 -0
- data/lib/moose_inventory/cli/relation_transaction_support.rb +51 -0
- data/lib/moose_inventory/cli/tag_support.rb +97 -0
- data/lib/moose_inventory/cli/variable_rendering.rb +67 -0
- data/lib/moose_inventory/config/config.rb +185 -108
- data/lib/moose_inventory/db/db.rb +170 -195
- data/lib/moose_inventory/db/exceptions.rb +6 -3
- data/lib/moose_inventory/db/models.rb +16 -0
- data/lib/moose_inventory/db/schema_migrations.rb +248 -0
- data/lib/moose_inventory/inventory_context.rb +68 -2
- data/lib/moose_inventory/operations/add_associations.rb +20 -16
- data/lib/moose_inventory/operations/add_groups.rb +21 -13
- data/lib/moose_inventory/operations/add_hosts.rb +30 -17
- data/lib/moose_inventory/operations/add_variables.rb +77 -0
- data/lib/moose_inventory/operations/entity_variable_operation_support.rb +46 -0
- data/lib/moose_inventory/operations/group_child_relations.rb +23 -16
- data/lib/moose_inventory/operations/group_cleanup.rb +23 -8
- data/lib/moose_inventory/operations/import_inventory_snapshot.rb +41 -0
- data/lib/moose_inventory/operations/inventory_doctor.rb +172 -0
- data/lib/moose_inventory/operations/inventory_snapshot.rb +60 -0
- data/lib/moose_inventory/operations/inventory_snapshot_applier.rb +112 -0
- data/lib/moose_inventory/operations/inventory_snapshot_preview.rb +174 -0
- data/lib/moose_inventory/operations/inventory_snapshot_validator.rb +134 -0
- data/lib/moose_inventory/operations/operation_event_support.rb +27 -0
- data/lib/moose_inventory/operations/query_inventory/base_query.rb +24 -0
- data/lib/moose_inventory/operations/query_inventory/group_queries.rb +86 -0
- data/lib/moose_inventory/operations/query_inventory/host_queries.rb +106 -0
- data/lib/moose_inventory/operations/query_inventory.rb +47 -0
- data/lib/moose_inventory/operations/remove_associations.rb +30 -18
- data/lib/moose_inventory/operations/remove_groups.rb +12 -12
- data/lib/moose_inventory/operations/remove_hosts.rb +68 -0
- data/lib/moose_inventory/operations/remove_variables.rb +67 -0
- data/lib/moose_inventory/runtime_options.rb +31 -0
- data/lib/moose_inventory/version.rb +3 -1
- data/lib/moose_inventory.rb +10 -7
- data/moose-inventory.gemspec +19 -35
- data/scripts/check.sh +1 -0
- data/scripts/ci/check_generated_artifacts.sh +41 -0
- data/scripts/ci/check_permissions.sh +2 -0
- data/scripts/ci/check_rubocop.sh +30 -25
- data/scripts/files.rb +5 -4
- data/spec/examples/ci_examples_spec.rb +37 -0
- data/spec/lib/moose_inventory/ansible_plugin_examples_spec.rb +29 -0
- data/spec/lib/moose_inventory/cli/application_doctor_spec.rb +50 -0
- data/spec/lib/moose_inventory/cli/application_import_export_spec.rb +100 -0
- data/spec/lib/moose_inventory/cli/application_spec.rb +25 -15
- data/spec/lib/moose_inventory/cli/audit_spec.rb +56 -0
- data/spec/lib/moose_inventory/cli/cli_spec.rb +15 -19
- data/spec/lib/moose_inventory/cli/console_spec.rb +98 -0
- data/spec/lib/moose_inventory/cli/factory_spec.rb +27 -0
- data/spec/lib/moose_inventory/cli/formatter_spec.rb +95 -3
- data/spec/lib/moose_inventory/cli/group_add_spec.rb +140 -116
- data/spec/lib/moose_inventory/cli/group_addchild_spec.rb +89 -35
- data/spec/lib/moose_inventory/cli/group_addhost_spec.rb +81 -84
- data/spec/lib/moose_inventory/cli/group_addvar_spec.rb +65 -68
- data/spec/lib/moose_inventory/cli/group_get_spec.rb +17 -33
- data/spec/lib/moose_inventory/cli/group_list_spec.rb +16 -38
- data/spec/lib/moose_inventory/cli/group_listvar_spec.rb +33 -40
- data/spec/lib/moose_inventory/cli/group_rm_spec.rb +136 -96
- data/spec/lib/moose_inventory/cli/group_rmchild_spec.rb +66 -41
- data/spec/lib/moose_inventory/cli/group_rmhost_spec.rb +76 -78
- data/spec/lib/moose_inventory/cli/group_rmvar_spec.rb +57 -63
- data/spec/lib/moose_inventory/cli/group_spec.rb +2 -0
- data/spec/lib/moose_inventory/cli/helpers_spec.rb +146 -0
- data/spec/lib/moose_inventory/cli/host_add_spec.rb +170 -116
- data/spec/lib/moose_inventory/cli/host_addgroup_spec.rb +100 -83
- data/spec/lib/moose_inventory/cli/host_addvar_spec.rb +92 -74
- data/spec/lib/moose_inventory/cli/host_get_spec.rb +14 -33
- data/spec/lib/moose_inventory/cli/host_list_spec.rb +41 -33
- data/spec/lib/moose_inventory/cli/host_listvar_spec.rb +45 -53
- data/spec/lib/moose_inventory/cli/host_rm_spec.rb +66 -48
- data/spec/lib/moose_inventory/cli/host_rmgroup_spec.rb +73 -83
- data/spec/lib/moose_inventory/cli/host_rmvar_spec.rb +56 -63
- data/spec/lib/moose_inventory/cli/host_spec.rb +2 -0
- data/spec/lib/moose_inventory/cli/tags_spec.rb +81 -0
- data/spec/lib/moose_inventory/config/config_spec.rb +41 -3
- data/spec/lib/moose_inventory/db/db_spec.rb +396 -36
- data/spec/lib/moose_inventory/db/exceptions_spec.rb +18 -0
- data/spec/lib/moose_inventory/db/models_spec.rb +7 -3
- data/spec/lib/moose_inventory/db_lifecycle_spec.rb +73 -0
- data/spec/lib/moose_inventory/inventory_context_spec.rb +10 -0
- data/spec/lib/moose_inventory/operations/add_associations_spec.rb +34 -0
- data/spec/lib/moose_inventory/operations/add_groups_spec.rb +15 -0
- data/spec/lib/moose_inventory/operations/add_hosts_spec.rb +13 -0
- data/spec/lib/moose_inventory/operations/add_variables_spec.rb +103 -0
- data/spec/lib/moose_inventory/operations/group_child_relations_spec.rb +46 -0
- data/spec/lib/moose_inventory/operations/import_inventory_snapshot_spec.rb +226 -0
- data/spec/lib/moose_inventory/operations/inventory_doctor_spec.rb +77 -0
- data/spec/lib/moose_inventory/operations/inventory_snapshot_spec.rb +50 -0
- data/spec/lib/moose_inventory/operations/operation_event_support_spec.rb +78 -0
- data/spec/lib/moose_inventory/operations/query_inventory_spec.rb +146 -0
- data/spec/lib/moose_inventory/operations/remove_associations_spec.rb +35 -0
- data/spec/lib/moose_inventory/operations/remove_groups_spec.rb +21 -0
- data/spec/lib/moose_inventory/operations/remove_hosts_spec.rb +55 -0
- data/spec/lib/moose_inventory/operations/remove_variables_spec.rb +83 -0
- data/spec/shared/shared_config_setup.rb +4 -3
- data/spec/spec_helper.rb +50 -40
- data/spec/support/cli_harness.rb +33 -0
- metadata +80 -41
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright: (c) Russell Davies
|
|
3
|
+
# MIT License
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import subprocess
|
|
9
|
+
|
|
10
|
+
from ansible.errors import AnsibleParserError
|
|
11
|
+
from ansible.plugins.inventory import BaseInventoryPlugin
|
|
12
|
+
|
|
13
|
+
DOCUMENTATION = r'''
|
|
14
|
+
name: moose_inventory
|
|
15
|
+
plugin_type: inventory
|
|
16
|
+
short_description: Moose Inventory plugin
|
|
17
|
+
description:
|
|
18
|
+
- Loads inventory from the C(moose-inventory) command line tool.
|
|
19
|
+
- Keeps Moose Inventory configuration and environment selection in YAML instead of a shell shim.
|
|
20
|
+
options:
|
|
21
|
+
plugin:
|
|
22
|
+
description: Token that ensures this is a source file for this plugin.
|
|
23
|
+
required: true
|
|
24
|
+
choices: ['moose_inventory']
|
|
25
|
+
executable:
|
|
26
|
+
description: Moose Inventory executable path.
|
|
27
|
+
required: false
|
|
28
|
+
default: moose-inventory
|
|
29
|
+
config:
|
|
30
|
+
description: Moose Inventory config file passed to C(--config).
|
|
31
|
+
required: false
|
|
32
|
+
env:
|
|
33
|
+
description: Moose Inventory environment section passed to C(--env).
|
|
34
|
+
required: false
|
|
35
|
+
'''
|
|
36
|
+
|
|
37
|
+
EXAMPLES = r'''
|
|
38
|
+
plugin: moose_inventory
|
|
39
|
+
executable: moose-inventory
|
|
40
|
+
config: ./example.conf
|
|
41
|
+
env: dev
|
|
42
|
+
'''
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class InventoryModule(BaseInventoryPlugin):
|
|
46
|
+
NAME = 'moose_inventory'
|
|
47
|
+
|
|
48
|
+
def verify_file(self, path):
|
|
49
|
+
return super().verify_file(path) and path.endswith(('moose_inventory.yml', 'moose_inventory.yaml'))
|
|
50
|
+
|
|
51
|
+
def parse(self, inventory, loader, path, cache=True):
|
|
52
|
+
super().parse(inventory, loader, path, cache=cache)
|
|
53
|
+
config = self._read_config_data(path)
|
|
54
|
+
executable = config.get('executable', 'moose-inventory')
|
|
55
|
+
moose_config = config.get('config')
|
|
56
|
+
env = config.get('env')
|
|
57
|
+
|
|
58
|
+
groups = self._run_moose(executable, moose_config, env, ['--ansible', 'group', 'list'])
|
|
59
|
+
hosts = self._run_moose(executable, moose_config, env, ['host', 'list'])
|
|
60
|
+
|
|
61
|
+
self._apply_groups(groups)
|
|
62
|
+
self._apply_hosts(hosts)
|
|
63
|
+
|
|
64
|
+
def _run_moose(self, executable, config, env, args):
|
|
65
|
+
command = [executable]
|
|
66
|
+
if config:
|
|
67
|
+
command.extend(['--config', config])
|
|
68
|
+
if env:
|
|
69
|
+
command.extend(['--env', env])
|
|
70
|
+
command.extend(args)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
completed = subprocess.run(command, check=True, capture_output=True, text=True)
|
|
74
|
+
except (OSError, subprocess.CalledProcessError) as error:
|
|
75
|
+
raise AnsibleParserError('moose-inventory command failed: %s' % error) from error
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
return json.loads(completed.stdout or '{}')
|
|
79
|
+
except json.JSONDecodeError as error:
|
|
80
|
+
raise AnsibleParserError('moose-inventory returned invalid JSON: %s' % error) from error
|
|
81
|
+
|
|
82
|
+
def _apply_groups(self, groups):
|
|
83
|
+
for group_name, payload in groups.items():
|
|
84
|
+
self.inventory.add_group(group_name)
|
|
85
|
+
for host_name in payload.get('hosts', []):
|
|
86
|
+
self.inventory.add_host(host_name, group=group_name)
|
|
87
|
+
for child_name in payload.get('children', []):
|
|
88
|
+
self.inventory.add_group(child_name)
|
|
89
|
+
self.inventory.add_child(group_name, child_name)
|
|
90
|
+
for key, value in payload.get('vars', {}).items():
|
|
91
|
+
self.inventory.set_variable(group_name, key, value)
|
|
92
|
+
|
|
93
|
+
def _apply_hosts(self, hosts):
|
|
94
|
+
for host_name, payload in hosts.items():
|
|
95
|
+
self.inventory.add_host(host_name)
|
|
96
|
+
for group_name in payload.get('groups', []):
|
|
97
|
+
self.inventory.add_group(group_name)
|
|
98
|
+
self.inventory.add_host(host_name, group=group_name)
|
|
99
|
+
for key, value in payload.get('hostvars', {}).items():
|
|
100
|
+
self.inventory.set_variable(host_name, key, value)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Moose Inventory CI/CD Examples
|
|
2
|
+
|
|
3
|
+
These examples show how to validate an inventory snapshot in CI without using production database credentials.
|
|
4
|
+
|
|
5
|
+
- `inventory/example-snapshot.yml` is a small review snapshot fixture.
|
|
6
|
+
- `scripts/validate-inventory-snapshot.sh` imports a snapshot into a temporary SQLite database, runs `doctor`, exports a canonical snapshot, lists hosts, and produces an Ansible-compatible inventory artifact.
|
|
7
|
+
- `github-actions/inventory-review.yml` is a copy/paste GitHub Actions workflow example. It is intentionally stored under `examples/` rather than `.github/workflows/` so projects can adapt it before enabling it.
|
|
8
|
+
|
|
9
|
+
The example writes artifacts to `tmp/inventory-ci-artifacts` by default:
|
|
10
|
+
|
|
11
|
+
- `doctor.txt`
|
|
12
|
+
- `inventory.yml`
|
|
13
|
+
- `hosts.json`
|
|
14
|
+
- `ansible-inventory.json`
|
|
15
|
+
|
|
16
|
+
Use this pattern for pull-request review gates before applying inventory changes to a shared or production Moose Inventory database.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Inventory review example
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
pull_request:
|
|
6
|
+
paths:
|
|
7
|
+
- 'inventory/**'
|
|
8
|
+
- 'examples/ci/inventory/**'
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
validate-inventory:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- name: Check out repository
|
|
18
|
+
uses: actions/checkout@v5
|
|
19
|
+
|
|
20
|
+
- name: Set up Ruby
|
|
21
|
+
uses: ruby/setup-ruby@v1
|
|
22
|
+
with:
|
|
23
|
+
ruby-version: '3.4'
|
|
24
|
+
bundler-cache: true
|
|
25
|
+
|
|
26
|
+
- name: Validate proposed inventory snapshot
|
|
27
|
+
env:
|
|
28
|
+
MOOSE_INVENTORY_CMD: bundle exec ruby -Ilib bin/moose-inventory
|
|
29
|
+
run: |
|
|
30
|
+
examples/ci/scripts/validate-inventory-snapshot.sh \
|
|
31
|
+
examples/ci/inventory/example-snapshot.yml \
|
|
32
|
+
tmp/inventory-ci-artifacts
|
|
33
|
+
|
|
34
|
+
- name: Upload inventory review artifacts
|
|
35
|
+
uses: actions/upload-artifact@v4
|
|
36
|
+
with:
|
|
37
|
+
name: inventory-review
|
|
38
|
+
path: tmp/inventory-ci-artifacts
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
snapshot="${1:-examples/ci/inventory/example-snapshot.yml}"
|
|
5
|
+
artifact_dir="${2:-tmp/inventory-ci-artifacts}"
|
|
6
|
+
moose_cmd="${MOOSE_INVENTORY_CMD:-moose-inventory}"
|
|
7
|
+
work_dir="$(mktemp -d)"
|
|
8
|
+
trap 'rm -rf "$work_dir"' EXIT
|
|
9
|
+
|
|
10
|
+
mkdir -p "$artifact_dir"
|
|
11
|
+
config_file="$work_dir/moose-inventory-ci.yml"
|
|
12
|
+
db_file="$work_dir/inventory.db"
|
|
13
|
+
|
|
14
|
+
cat > "$config_file" <<YAML
|
|
15
|
+
---
|
|
16
|
+
general:
|
|
17
|
+
defaultenv: ci
|
|
18
|
+
ci:
|
|
19
|
+
db:
|
|
20
|
+
adapter: sqlite3
|
|
21
|
+
file: "$db_file"
|
|
22
|
+
YAML
|
|
23
|
+
|
|
24
|
+
$moose_cmd --config "$config_file" --env ci import "$snapshot"
|
|
25
|
+
$moose_cmd --config "$config_file" --env ci doctor > "$artifact_dir/doctor.txt"
|
|
26
|
+
$moose_cmd --config "$config_file" --env ci --format yaml export "$artifact_dir/inventory.yml"
|
|
27
|
+
$moose_cmd --config "$config_file" --env ci --format pjson host list > "$artifact_dir/hosts.json"
|
|
28
|
+
$moose_cmd --config "$config_file" --env ci --ansible group list > "$artifact_dir/ansible-inventory.json"
|
|
29
|
+
|
|
30
|
+
echo "Inventory CI artifacts written to $artifact_dir"
|
|
@@ -1,20 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
1
4
|
require 'thor'
|
|
2
|
-
require_relative '../version
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
require_relative '
|
|
5
|
+
require_relative '../version'
|
|
6
|
+
require 'yaml'
|
|
7
|
+
|
|
8
|
+
require_relative '../config/config'
|
|
9
|
+
require_relative '../operations/import_inventory_snapshot'
|
|
10
|
+
require_relative '../operations/inventory_doctor'
|
|
11
|
+
require_relative '../operations/inventory_snapshot'
|
|
12
|
+
require_relative 'formatter'
|
|
13
|
+
require_relative 'helpers'
|
|
14
|
+
require_relative 'audit'
|
|
15
|
+
require_relative 'console'
|
|
16
|
+
require_relative 'db'
|
|
17
|
+
require_relative 'group'
|
|
18
|
+
require_relative 'host'
|
|
6
19
|
|
|
7
20
|
module Moose
|
|
8
21
|
module Inventory
|
|
9
22
|
module Cli
|
|
10
23
|
##
|
|
11
|
-
#
|
|
24
|
+
# Top-level Thor application for moose-inventory.
|
|
25
|
+
# rubocop:disable Metrics/ClassLength
|
|
12
26
|
class Application < Thor
|
|
27
|
+
include Moose::Inventory::Cli::Helpers
|
|
28
|
+
|
|
13
29
|
desc 'version', 'Get the code version'
|
|
14
30
|
def version
|
|
15
31
|
puts "Version #{Moose::Inventory::VERSION}"
|
|
16
32
|
end
|
|
17
33
|
|
|
34
|
+
desc 'doctor', 'Run inventory health checks'
|
|
35
|
+
option :format, type: :string, desc: 'Emit doctor report as yaml|json|pjson'
|
|
36
|
+
def doctor
|
|
37
|
+
report = build_operation(Moose::Inventory::Operations::InventoryDoctor).call
|
|
38
|
+
render_doctor_report(report)
|
|
39
|
+
exit(1) unless report[:ok]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
desc 'export [FILE]', 'Export a canonical inventory snapshot'
|
|
43
|
+
def export(file = nil)
|
|
44
|
+
snapshot = build_operation(Moose::Inventory::Operations::InventorySnapshot).export
|
|
45
|
+
output = serialize_snapshot(snapshot)
|
|
46
|
+
|
|
47
|
+
if file.nil?
|
|
48
|
+
puts output
|
|
49
|
+
else
|
|
50
|
+
File.write(file, output)
|
|
51
|
+
puts "Exported inventory snapshot to #{file}."
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
desc 'import FILE', 'Import and validate an inventory snapshot'
|
|
56
|
+
option :preview, type: :boolean, desc: 'Preview snapshot import changes without writing'
|
|
57
|
+
option :preview_format, type: :string, desc: 'Emit preview as yaml|json|pjson'
|
|
58
|
+
def import(file)
|
|
59
|
+
snapshot = YAML.safe_load_file(file, aliases: false)
|
|
60
|
+
operation = build_operation(Moose::Inventory::Operations::ImportInventorySnapshot)
|
|
61
|
+
return render_import_preview(operation.preview(snapshot: snapshot)) if options[:preview]
|
|
62
|
+
|
|
63
|
+
abort('ERROR: --preview-format requires --preview.') if options[:preview_format]
|
|
64
|
+
|
|
65
|
+
result = operation.call(snapshot: snapshot)
|
|
66
|
+
record_audit({ command: 'import', action: 'import', entity_type: 'inventory',
|
|
67
|
+
entity_names: file }, result: result)
|
|
68
|
+
puts "Imported inventory snapshot from #{file}."
|
|
69
|
+
puts "Created hosts: #{result.created_hosts}"
|
|
70
|
+
puts "Created groups: #{result.created_groups}"
|
|
71
|
+
puts "Variables changed: #{result.updated_variables}"
|
|
72
|
+
puts "Associations added: #{result.associations}"
|
|
73
|
+
rescue Psych::SyntaxError => e
|
|
74
|
+
abort("ERROR: Could not parse inventory snapshot '#{file}': #{e.message}")
|
|
75
|
+
rescue db.exceptions[:moose] => e
|
|
76
|
+
abort("ERROR: #{e.message}")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
map 'db' => :database
|
|
80
|
+
desc 'audit ACTION', 'Inspect append-only inventory change history'
|
|
81
|
+
subcommand 'audit', Moose::Inventory::Cli::Audit
|
|
82
|
+
|
|
83
|
+
desc 'database ACTION', 'Inspect and manage database lifecycle state'
|
|
84
|
+
subcommand 'database', Moose::Inventory::Cli::Db
|
|
85
|
+
|
|
86
|
+
desc 'console', 'Open a small read-only inventory browsing console'
|
|
87
|
+
def console
|
|
88
|
+
Moose::Inventory::Cli::Console.new(context: inventory_context).run
|
|
89
|
+
end
|
|
90
|
+
|
|
18
91
|
desc 'group ACTION',
|
|
19
92
|
'Manipulate groups in the inventory. ' \
|
|
20
93
|
'ACTION can be add, rm, get, list, addhost, rmhost, addchild, rmchild, addvar, rmvar'
|
|
@@ -24,7 +97,62 @@ module Moose
|
|
|
24
97
|
'Manipulate hosts in the inventory. ' \
|
|
25
98
|
'ACTION can be add, rm, get, list, addgroup, rmgroup, addvar, rmvar'
|
|
26
99
|
subcommand 'host', Moose::Inventory::Cli::Host
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def render_doctor_report(report)
|
|
104
|
+
if options[:format]
|
|
105
|
+
puts serialize_data(report, options[:format].downcase)
|
|
106
|
+
else
|
|
107
|
+
render_human_doctor_report(report)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def render_human_doctor_report(report)
|
|
112
|
+
if report[:ok]
|
|
113
|
+
puts 'Inventory doctor found no issues.'
|
|
114
|
+
return
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
puts "Inventory doctor found #{report[:issue_count]} issue(s):"
|
|
118
|
+
report[:issues].each do |entry|
|
|
119
|
+
puts "- [#{entry[:severity]}] #{entry[:id]}: #{entry[:message]}"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def render_import_preview(preview)
|
|
124
|
+
if options[:preview_format]
|
|
125
|
+
puts serialize_data(preview, options[:preview_format].downcase)
|
|
126
|
+
else
|
|
127
|
+
render_human_import_preview(preview)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def render_human_import_preview(preview)
|
|
132
|
+
puts 'Snapshot import preview. No changes applied.'
|
|
133
|
+
preview.fetch('summary').each do |key, value|
|
|
134
|
+
puts "#{key.tr('_', ' ').capitalize}: #{value}"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def serialize_snapshot(snapshot)
|
|
139
|
+
serialize_data(snapshot, output_format)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def serialize_data(data, format)
|
|
143
|
+
case format
|
|
144
|
+
when 'yaml', 'y'
|
|
145
|
+
data.to_yaml
|
|
146
|
+
when 'prettyjson', 'pjson', 'p'
|
|
147
|
+
JSON.pretty_generate(data)
|
|
148
|
+
when 'json', 'j'
|
|
149
|
+
data.to_json
|
|
150
|
+
else
|
|
151
|
+
abort("Output format '#{format}' is not yet supported.")
|
|
152
|
+
end
|
|
153
|
+
end
|
|
27
154
|
end
|
|
155
|
+
# rubocop:enable Metrics/ClassLength
|
|
28
156
|
end
|
|
29
157
|
end
|
|
30
158
|
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'association_rendering_support'
|
|
4
|
+
|
|
5
|
+
module Moose
|
|
6
|
+
module Inventory
|
|
7
|
+
module Cli
|
|
8
|
+
# Shared rendering helpers for host/group association commands.
|
|
9
|
+
module AssociationRendering
|
|
10
|
+
include Moose::Inventory::Cli::AssociationRenderingSupport
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def host_group_association_addition_emitter(perspective:)
|
|
15
|
+
lambda do |event|
|
|
16
|
+
render_host_group_association_addition_event(event, perspective:)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def host_group_association_removal_emitter(perspective:)
|
|
21
|
+
lambda do |event|
|
|
22
|
+
render_host_group_association_removal_event(event, perspective:)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def render_host_group_association_addition_event(event, perspective:)
|
|
27
|
+
payload = event.payload
|
|
28
|
+
|
|
29
|
+
return render_addition_warning(event.type, payload, perspective:) if addition_warning_event?(event.type)
|
|
30
|
+
return render_addition_existing(payload, perspective:) if event.type == :already_exists_skipping
|
|
31
|
+
return render_association_dry_run_summary if event.type == :dry_run_summary
|
|
32
|
+
|
|
33
|
+
render_addition_action_event(event.type, payload, perspective:)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def render_addition_action_event(type, payload, perspective:)
|
|
37
|
+
case type
|
|
38
|
+
when :adding_host_group_association, :adding_group_host_association
|
|
39
|
+
fmt.puts 2, "- #{verb_for(:add, perspective)} association #{association_label(payload, perspective:)}..."
|
|
40
|
+
when :group_creating_now, :host_creating_now
|
|
41
|
+
fmt.puts 4, "- #{missing_entity_label(perspective)} does not exist, creating now..."
|
|
42
|
+
when :removing_automatic_group
|
|
43
|
+
label = automatic_group_label(payload[:host], perspective:)
|
|
44
|
+
fmt.puts 2, "- #{verb_for(:remove, perspective)} automatic association #{label}..."
|
|
45
|
+
when :ok
|
|
46
|
+
fmt.puts payload[:indent], '- OK'
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def render_association_dry_run_summary
|
|
51
|
+
puts 'Dry run complete. No changes applied.'
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def render_host_group_association_removal_event(event, perspective:)
|
|
55
|
+
payload = event.payload
|
|
56
|
+
|
|
57
|
+
return render_removal_warning(payload, perspective:) if removal_warning_event?(event.type)
|
|
58
|
+
return render_removal_missing(payload, perspective:) if event.type == :missing_skipping
|
|
59
|
+
return render_association_dry_run_summary if event.type == :dry_run_summary
|
|
60
|
+
|
|
61
|
+
case event.type
|
|
62
|
+
when :removing_host_group_association, :removing_group_host_association
|
|
63
|
+
fmt.puts 2, "- #{verb_for(:remove, perspective)} association #{association_label(payload, perspective:)}..."
|
|
64
|
+
when :adding_automatic_group
|
|
65
|
+
label = automatic_group_label(payload[:host], perspective:)
|
|
66
|
+
fmt.puts 2, "- #{verb_for(:add, perspective)} automatic association #{label}..."
|
|
67
|
+
when :ok
|
|
68
|
+
fmt.puts payload[:indent], '- OK'
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moose
|
|
4
|
+
module Inventory
|
|
5
|
+
module Cli
|
|
6
|
+
# Shared string helpers for host/group association commands.
|
|
7
|
+
module AssociationRenderingSupport
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def addition_warning_event?(type)
|
|
11
|
+
%i[
|
|
12
|
+
host_group_association_exists
|
|
13
|
+
group_host_association_exists
|
|
14
|
+
group_missing_created
|
|
15
|
+
host_missing_created
|
|
16
|
+
].include?(type)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def removal_warning_event?(type)
|
|
20
|
+
%i[host_group_association_missing group_host_association_missing].include?(type)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def render_addition_warning(type, payload, perspective:)
|
|
24
|
+
if %i[host_group_association_exists group_host_association_exists].include?(type)
|
|
25
|
+
fmt.warn warning_text(
|
|
26
|
+
"Association #{association_label(payload, perspective:)} already exists, skipping.",
|
|
27
|
+
perspective:
|
|
28
|
+
)
|
|
29
|
+
else
|
|
30
|
+
fmt.warn warning_text(
|
|
31
|
+
"#{missing_entity_label(perspective).capitalize} '#{payload[:name]}' does not exist and will be created.",
|
|
32
|
+
perspective:
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def render_addition_existing(payload, perspective:)
|
|
38
|
+
fmt.puts payload[:indent], "- #{existing_status_text(perspective)}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def render_removal_warning(payload, perspective:)
|
|
42
|
+
fmt.warn "Association #{association_label(payload, perspective:)} doesn't exist, skipping.\n"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def render_removal_missing(payload, perspective:)
|
|
46
|
+
fmt.puts payload[:indent], "- #{missing_status_text(perspective)}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def association_label(payload, perspective:)
|
|
50
|
+
if perspective == :host
|
|
51
|
+
"{host:#{payload[:host]} <-> group:#{payload[:group]}}"
|
|
52
|
+
else
|
|
53
|
+
"{group:#{payload[:group]} <-> host:#{payload[:host]}}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def automatic_group_label(host, perspective:)
|
|
58
|
+
if perspective == :host
|
|
59
|
+
"{host:#{host} <-> group:ungrouped}"
|
|
60
|
+
else
|
|
61
|
+
"{group:ungrouped <-> host:#{host}}"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def verb_for(action, perspective)
|
|
66
|
+
return action.to_s.capitalize if perspective == :host
|
|
67
|
+
|
|
68
|
+
action.to_s
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def missing_entity_label(perspective)
|
|
72
|
+
perspective == :host ? 'Group' : 'host'
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def existing_status_text(perspective)
|
|
76
|
+
perspective == :host ? 'Already exists, skipping.' : 'already exists, skipping.'
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def missing_status_text(perspective)
|
|
80
|
+
perspective == :host ? "Doesn't exist, skipping." : "doesn't exist, skipping."
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def warning_text(text, perspective:)
|
|
84
|
+
perspective == :host ? text : "#{text}\n"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'thor'
|
|
5
|
+
|
|
6
|
+
module Moose
|
|
7
|
+
module Inventory
|
|
8
|
+
module Cli
|
|
9
|
+
# Audit log inspection commands.
|
|
10
|
+
class Audit < Thor
|
|
11
|
+
include Moose::Inventory::Cli::Helpers
|
|
12
|
+
|
|
13
|
+
desc 'list', 'List recent append-only audit events'
|
|
14
|
+
option :limit, type: :numeric, default: 20
|
|
15
|
+
option :format, type: :string, desc: 'Emit audit events as yaml|json|pjson'
|
|
16
|
+
def list
|
|
17
|
+
events = inventory_context.audit_events(limit: options[:limit]).map { |event| serialize_event(event) }
|
|
18
|
+
if options[:format]
|
|
19
|
+
fmt.dump(events, options[:format].downcase)
|
|
20
|
+
else
|
|
21
|
+
render_human_events(events)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def serialize_event(event)
|
|
28
|
+
{
|
|
29
|
+
id: event.id,
|
|
30
|
+
created_at: event.created_at,
|
|
31
|
+
actor: event.actor,
|
|
32
|
+
command: event.command,
|
|
33
|
+
action: event.action,
|
|
34
|
+
entity_type: event.entity_type,
|
|
35
|
+
entity_name: event.entity_name,
|
|
36
|
+
details: parse_details(event.details)
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def parse_details(details)
|
|
41
|
+
return nil if details.nil? || details.empty?
|
|
42
|
+
|
|
43
|
+
JSON.parse(details)
|
|
44
|
+
rescue JSON::ParserError
|
|
45
|
+
details
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def render_human_events(events)
|
|
49
|
+
if events.empty?
|
|
50
|
+
puts 'No audit events recorded.'
|
|
51
|
+
return
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
events.each do |event|
|
|
55
|
+
puts "#{event[:id]} #{event[:created_at]} #{event[:command]} " \
|
|
56
|
+
"#{event[:entity_type]}=#{event[:entity_name]} action=#{event[:action]}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Moose
|
|
6
|
+
module Inventory
|
|
7
|
+
module Cli
|
|
8
|
+
# Shared append-only audit recording helpers for mutating CLI commands.
|
|
9
|
+
module AuditRecording
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def record_audit(metadata, result:, dry_run: false)
|
|
13
|
+
return if dry_run
|
|
14
|
+
|
|
15
|
+
inventory_context.record_audit_event(
|
|
16
|
+
command: metadata.fetch(:command),
|
|
17
|
+
action: metadata.fetch(:action),
|
|
18
|
+
actor: ENV.fetch('USER', nil),
|
|
19
|
+
entity_type: metadata.fetch(:entity_type),
|
|
20
|
+
entity_name: Array(metadata.fetch(:entity_names)).join(','),
|
|
21
|
+
details: audit_details(result)
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def audit_details(result)
|
|
26
|
+
JSON.generate(
|
|
27
|
+
warning_count: result.respond_to?(:warning_count) ? result.warning_count : 0,
|
|
28
|
+
events: audit_events_from_result(result)
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def audit_events_from_result(result)
|
|
33
|
+
return [] unless result.respond_to?(:events)
|
|
34
|
+
|
|
35
|
+
result.events.map { |event| { type: event.type, payload: event.payload } }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|