moose-inventory 1.0.9 → 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.
Files changed (176) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +15 -1
  3. data/.github/workflows/release.yml +60 -0
  4. data/.gitignore +2 -1
  5. data/.gitleaks.toml +9 -0
  6. data/.rubocop.yml +49 -0
  7. data/BACKLOG.md +752 -24
  8. data/Gemfile +2 -0
  9. data/Gemfile.lock +36 -1
  10. data/README.md +340 -44
  11. data/Rakefile +2 -0
  12. data/bin/moose-inventory +2 -1
  13. data/docs/architecture/architecture-and-trust-boundaries.md +444 -0
  14. data/docs/compatibility/cli-output-compatibility.md +76 -0
  15. data/docs/governance/approval-register.md +37 -0
  16. data/docs/maintenance/database-backup-restore-guidance.md +162 -0
  17. data/docs/maintenance/package-maintenance-and-agent-boundaries.md +260 -0
  18. data/docs/process/conformance-gap-analysis-2026-05-28.md +192 -0
  19. data/docs/product/product-brief.md +161 -0
  20. data/docs/product/requirements-baseline.md +477 -0
  21. data/docs/qa/qa-documentation-and-release-gates.md +283 -0
  22. data/docs/release/package-provenance-hardening.md +126 -0
  23. data/docs/release/publishing.md +54 -50
  24. data/docs/release/release-environment-protection.md +70 -0
  25. data/docs/release/release-readiness.md +37 -4
  26. data/docs/security/accepted-risk-register.md +84 -0
  27. data/docs/security/security-privacy-process.md +287 -0
  28. data/docs/security-audit-2026-05-26-rerun.md +75 -0
  29. data/docs/security-audit-2026-05-26.md +63 -0
  30. data/docs/ux/cli-workflow-notes.md +287 -0
  31. data/examples/ansible/ansible.cfg +3 -0
  32. data/examples/ansible/inventory/moose_inventory.yml +5 -0
  33. data/examples/ansible/inventory_plugins/moose_inventory.py +100 -0
  34. data/examples/ci/README.md +16 -0
  35. data/examples/ci/github-actions/inventory-review.yml +38 -0
  36. data/examples/ci/inventory/example-snapshot.yml +19 -0
  37. data/examples/ci/scripts/validate-inventory-snapshot.sh +30 -0
  38. data/lib/moose_inventory/cli/application.rb +133 -5
  39. data/lib/moose_inventory/cli/association_rendering.rb +74 -0
  40. data/lib/moose_inventory/cli/association_rendering_support.rb +89 -0
  41. data/lib/moose_inventory/cli/audit.rb +62 -0
  42. data/lib/moose_inventory/cli/audit_recording.rb +40 -0
  43. data/lib/moose_inventory/cli/child_relation_rendering.rb +110 -0
  44. data/lib/moose_inventory/cli/console.rb +135 -0
  45. data/lib/moose_inventory/cli/db.rb +64 -0
  46. data/lib/moose_inventory/cli/factory.rb +28 -0
  47. data/lib/moose_inventory/cli/formatter.rb +8 -12
  48. data/lib/moose_inventory/cli/group.rb +7 -1
  49. data/lib/moose_inventory/cli/group_add.rb +91 -73
  50. data/lib/moose_inventory/cli/group_addchild.rb +41 -66
  51. data/lib/moose_inventory/cli/group_addhost.rb +33 -71
  52. data/lib/moose_inventory/cli/group_addvar.rb +27 -47
  53. data/lib/moose_inventory/cli/group_get.rb +8 -42
  54. data/lib/moose_inventory/cli/group_list.rb +7 -40
  55. data/lib/moose_inventory/cli/group_listvars.rb +9 -55
  56. data/lib/moose_inventory/cli/group_rm.rb +105 -73
  57. data/lib/moose_inventory/cli/group_rmchild.rb +47 -57
  58. data/lib/moose_inventory/cli/group_rmhost.rb +34 -61
  59. data/lib/moose_inventory/cli/group_rmvar.rb +30 -41
  60. data/lib/moose_inventory/cli/group_tags.rb +33 -0
  61. data/lib/moose_inventory/cli/helpers.rb +143 -0
  62. data/lib/moose_inventory/cli/host.rb +8 -2
  63. data/lib/moose_inventory/cli/host_add.rb +91 -66
  64. data/lib/moose_inventory/cli/host_addgroup.rb +39 -66
  65. data/lib/moose_inventory/cli/host_addvar.rb +28 -52
  66. data/lib/moose_inventory/cli/host_get.rb +9 -37
  67. data/lib/moose_inventory/cli/host_list.rb +24 -21
  68. data/lib/moose_inventory/cli/host_listvars.rb +9 -62
  69. data/lib/moose_inventory/cli/host_rm.rb +60 -42
  70. data/lib/moose_inventory/cli/host_rmgroup.rb +39 -55
  71. data/lib/moose_inventory/cli/host_rmvar.rb +31 -45
  72. data/lib/moose_inventory/cli/host_tags.rb +33 -0
  73. data/lib/moose_inventory/cli/listvars_support.rb +55 -0
  74. data/lib/moose_inventory/cli/plan_rendering.rb +50 -0
  75. data/lib/moose_inventory/cli/relation_transaction_support.rb +51 -0
  76. data/lib/moose_inventory/cli/tag_support.rb +97 -0
  77. data/lib/moose_inventory/cli/variable_rendering.rb +67 -0
  78. data/lib/moose_inventory/config/config.rb +185 -108
  79. data/lib/moose_inventory/db/db.rb +188 -193
  80. data/lib/moose_inventory/db/exceptions.rb +6 -3
  81. data/lib/moose_inventory/db/models.rb +16 -0
  82. data/lib/moose_inventory/db/schema_migrations.rb +248 -0
  83. data/lib/moose_inventory/inventory_context.rb +116 -0
  84. data/lib/moose_inventory/operations/add_associations.rb +131 -0
  85. data/lib/moose_inventory/operations/add_groups.rb +123 -0
  86. data/lib/moose_inventory/operations/add_hosts.rb +123 -0
  87. data/lib/moose_inventory/operations/add_variables.rb +77 -0
  88. data/lib/moose_inventory/operations/entity_variable_operation_support.rb +46 -0
  89. data/lib/moose_inventory/operations/group_child_relations.rb +125 -0
  90. data/lib/moose_inventory/operations/group_cleanup.rb +70 -0
  91. data/lib/moose_inventory/operations/import_inventory_snapshot.rb +41 -0
  92. data/lib/moose_inventory/operations/inventory_doctor.rb +172 -0
  93. data/lib/moose_inventory/operations/inventory_snapshot.rb +60 -0
  94. data/lib/moose_inventory/operations/inventory_snapshot_applier.rb +112 -0
  95. data/lib/moose_inventory/operations/inventory_snapshot_preview.rb +174 -0
  96. data/lib/moose_inventory/operations/inventory_snapshot_validator.rb +134 -0
  97. data/lib/moose_inventory/operations/operation_event_support.rb +27 -0
  98. data/lib/moose_inventory/operations/query_inventory/base_query.rb +24 -0
  99. data/lib/moose_inventory/operations/query_inventory/group_queries.rb +86 -0
  100. data/lib/moose_inventory/operations/query_inventory/host_queries.rb +106 -0
  101. data/lib/moose_inventory/operations/query_inventory.rb +47 -0
  102. data/lib/moose_inventory/operations/remove_associations.rb +113 -0
  103. data/lib/moose_inventory/operations/remove_groups.rb +79 -0
  104. data/lib/moose_inventory/operations/remove_hosts.rb +68 -0
  105. data/lib/moose_inventory/operations/remove_variables.rb +67 -0
  106. data/lib/moose_inventory/runtime_options.rb +31 -0
  107. data/lib/moose_inventory/version.rb +3 -1
  108. data/lib/moose_inventory.rb +10 -7
  109. data/moose-inventory.gemspec +22 -35
  110. data/scripts/check.sh +3 -0
  111. data/scripts/ci/check_generated_artifacts.sh +41 -0
  112. data/scripts/ci/check_permissions.sh +5 -0
  113. data/scripts/ci/check_rubocop.sh +33 -0
  114. data/scripts/ci/check_secrets.sh +26 -0
  115. data/scripts/ci/check_security.sh +18 -0
  116. data/scripts/ci/install_security_tools.sh +47 -0
  117. data/scripts/files.rb +5 -4
  118. data/scripts/install_dependencies.sh +2 -0
  119. data/spec/examples/ci_examples_spec.rb +37 -0
  120. data/spec/lib/moose_inventory/ansible_plugin_examples_spec.rb +29 -0
  121. data/spec/lib/moose_inventory/cli/application_doctor_spec.rb +50 -0
  122. data/spec/lib/moose_inventory/cli/application_import_export_spec.rb +100 -0
  123. data/spec/lib/moose_inventory/cli/application_spec.rb +25 -15
  124. data/spec/lib/moose_inventory/cli/audit_spec.rb +56 -0
  125. data/spec/lib/moose_inventory/cli/cli_spec.rb +15 -19
  126. data/spec/lib/moose_inventory/cli/console_spec.rb +98 -0
  127. data/spec/lib/moose_inventory/cli/factory_spec.rb +27 -0
  128. data/spec/lib/moose_inventory/cli/formatter_spec.rb +95 -3
  129. data/spec/lib/moose_inventory/cli/group_add_spec.rb +140 -116
  130. data/spec/lib/moose_inventory/cli/group_addchild_spec.rb +89 -35
  131. data/spec/lib/moose_inventory/cli/group_addhost_spec.rb +81 -84
  132. data/spec/lib/moose_inventory/cli/group_addvar_spec.rb +65 -68
  133. data/spec/lib/moose_inventory/cli/group_get_spec.rb +17 -33
  134. data/spec/lib/moose_inventory/cli/group_list_spec.rb +16 -38
  135. data/spec/lib/moose_inventory/cli/group_listvar_spec.rb +33 -40
  136. data/spec/lib/moose_inventory/cli/group_rm_spec.rb +165 -85
  137. data/spec/lib/moose_inventory/cli/group_rmchild_spec.rb +100 -30
  138. data/spec/lib/moose_inventory/cli/group_rmhost_spec.rb +76 -78
  139. data/spec/lib/moose_inventory/cli/group_rmvar_spec.rb +57 -63
  140. data/spec/lib/moose_inventory/cli/group_spec.rb +2 -0
  141. data/spec/lib/moose_inventory/cli/helpers_spec.rb +146 -0
  142. data/spec/lib/moose_inventory/cli/host_add_spec.rb +170 -116
  143. data/spec/lib/moose_inventory/cli/host_addgroup_spec.rb +100 -83
  144. data/spec/lib/moose_inventory/cli/host_addvar_spec.rb +92 -74
  145. data/spec/lib/moose_inventory/cli/host_get_spec.rb +14 -33
  146. data/spec/lib/moose_inventory/cli/host_list_spec.rb +41 -33
  147. data/spec/lib/moose_inventory/cli/host_listvar_spec.rb +45 -53
  148. data/spec/lib/moose_inventory/cli/host_rm_spec.rb +66 -48
  149. data/spec/lib/moose_inventory/cli/host_rmgroup_spec.rb +73 -83
  150. data/spec/lib/moose_inventory/cli/host_rmvar_spec.rb +56 -63
  151. data/spec/lib/moose_inventory/cli/host_spec.rb +2 -0
  152. data/spec/lib/moose_inventory/cli/tags_spec.rb +81 -0
  153. data/spec/lib/moose_inventory/config/config_spec.rb +41 -3
  154. data/spec/lib/moose_inventory/db/db_spec.rb +551 -29
  155. data/spec/lib/moose_inventory/db/exceptions_spec.rb +18 -0
  156. data/spec/lib/moose_inventory/db/models_spec.rb +7 -3
  157. data/spec/lib/moose_inventory/db_lifecycle_spec.rb +73 -0
  158. data/spec/lib/moose_inventory/inventory_context_spec.rb +10 -0
  159. data/spec/lib/moose_inventory/operations/add_associations_spec.rb +111 -0
  160. data/spec/lib/moose_inventory/operations/add_groups_spec.rb +80 -0
  161. data/spec/lib/moose_inventory/operations/add_hosts_spec.rb +82 -0
  162. data/spec/lib/moose_inventory/operations/add_variables_spec.rb +103 -0
  163. data/spec/lib/moose_inventory/operations/group_child_relations_spec.rb +122 -0
  164. data/spec/lib/moose_inventory/operations/import_inventory_snapshot_spec.rb +226 -0
  165. data/spec/lib/moose_inventory/operations/inventory_doctor_spec.rb +77 -0
  166. data/spec/lib/moose_inventory/operations/inventory_snapshot_spec.rb +50 -0
  167. data/spec/lib/moose_inventory/operations/operation_event_support_spec.rb +78 -0
  168. data/spec/lib/moose_inventory/operations/query_inventory_spec.rb +146 -0
  169. data/spec/lib/moose_inventory/operations/remove_associations_spec.rb +113 -0
  170. data/spec/lib/moose_inventory/operations/remove_groups_spec.rb +78 -0
  171. data/spec/lib/moose_inventory/operations/remove_hosts_spec.rb +55 -0
  172. data/spec/lib/moose_inventory/operations/remove_variables_spec.rb +83 -0
  173. data/spec/shared/shared_config_setup.rb +4 -3
  174. data/spec/spec_helper.rb +50 -40
  175. data/spec/support/cli_harness.rb +33 -0
  176. metadata +163 -35
@@ -0,0 +1,41 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # Generated/local artifacts are useful during development but must not become
5
+ # source inputs, package contents, or review noise. Keep this list aligned with
6
+ # .gitignore and scanner excludes.
7
+ generated_paths=(
8
+ ".openclaw-security-audit"
9
+ "coverage"
10
+ "pkg"
11
+ "spec/reports"
12
+ "tmp"
13
+ )
14
+
15
+ tracked=()
16
+ for path in "${generated_paths[@]}"; do
17
+ while IFS= read -r file; do
18
+ tracked+=("$file")
19
+ done < <(git ls-files "$path" "$path/**")
20
+ done
21
+
22
+ if (( ${#tracked[@]} > 0 )); then
23
+ echo "Generated artifact paths are tracked and must be removed from source commits:" >&2
24
+ printf ' %s\n' "${tracked[@]}" >&2
25
+ exit 1
26
+ fi
27
+
28
+ ignored_failures=()
29
+ for path in "${generated_paths[@]}"; do
30
+ if ! git check-ignore -q "$path/placeholder"; then
31
+ ignored_failures+=("$path/")
32
+ fi
33
+ done
34
+
35
+ if (( ${#ignored_failures[@]} > 0 )); then
36
+ echo "Generated artifact paths are not ignored:" >&2
37
+ printf ' %s\n' "${ignored_failures[@]}" >&2
38
+ exit 1
39
+ fi
40
+
41
+ echo "Generated artifact guard passed."
@@ -3,9 +3,14 @@ set -euo pipefail
3
3
 
4
4
  allowed_executables=(
5
5
  "bin/moose-inventory"
6
+ "examples/ci/scripts/validate-inventory-snapshot.sh"
6
7
  "scripts/check.sh"
7
8
  "scripts/ci/check_permissions.sh"
9
+ "scripts/ci/check_rubocop.sh"
10
+ "scripts/ci/check_secrets.sh"
11
+ "scripts/ci/check_generated_artifacts.sh"
8
12
  "scripts/ci/check_security.sh"
13
+ "scripts/ci/install_security_tools.sh"
9
14
  "scripts/ci/package_sanity.sh"
10
15
  "scripts/files.rb"
11
16
  "scripts/install_dependencies.sh"
@@ -0,0 +1,33 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ repo_root="$(git rev-parse --show-toplevel)"
5
+ cd "$repo_root"
6
+
7
+ rubocop_files=(
8
+ Gemfile
9
+ Rakefile
10
+ )
11
+
12
+ while IFS= read -r -d '' file; do
13
+ rubocop_files+=("$file")
14
+ done < <(find bin -maxdepth 1 -type f -print0 | sort -z)
15
+
16
+ while IFS= read -r -d '' file; do
17
+ rubocop_files+=("$file")
18
+ done < <(
19
+ find \
20
+ lib \
21
+ scripts \
22
+ spec \
23
+ -path 'spec/reports' -prune -o \
24
+ -path 'spec/reports/*' -prune -o \
25
+ -type f \( -name '*.rb' -o -name '*.gemspec' \) \
26
+ -print0 | sort -z
27
+ )
28
+
29
+ while IFS= read -r -d '' file; do
30
+ rubocop_files+=("$file")
31
+ done < <(find . -maxdepth 1 -type f -name '*.gemspec' -print0 | sort -z)
32
+
33
+ bundle exec rubocop "${rubocop_files[@]}"
@@ -0,0 +1,26 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ BIN_DIR="${MOOSE_INVENTORY_SECURITY_TOOLS_BIN:-$PWD/tmp/security-tools/bin}"
5
+ if command -v gitleaks >/dev/null 2>&1; then
6
+ GITLEAKS=(gitleaks)
7
+ elif [ -x "$BIN_DIR/gitleaks" ]; then
8
+ GITLEAKS=("$BIN_DIR/gitleaks")
9
+ else
10
+ if [ "${MOOSE_INVENTORY_REQUIRE_SECURITY_TOOLS:-0}" = "1" ]; then
11
+ echo "gitleaks is required but was not found. Run scripts/ci/install_security_tools.sh first." >&2
12
+ exit 2
13
+ fi
14
+ echo "gitleaks not found; skipping dedicated secret scan."
15
+ exit 0
16
+ fi
17
+
18
+ "${GITLEAKS[@]}" detect \
19
+ --no-git \
20
+ --source . \
21
+ --config .gitleaks.toml \
22
+ --redact \
23
+ --no-banner \
24
+ --log-level warn
25
+
26
+ echo "Gitleaks secret scan passed."
@@ -48,3 +48,21 @@ if findings:
48
48
  print(f'- {name} {version}: {vuln_id} {summary}', file=sys.stderr)
49
49
  sys.exit(1)
50
50
  PY
51
+
52
+ bundle exec bundle-audit check --update
53
+
54
+ BIN_DIR="${MOOSE_INVENTORY_SECURITY_TOOLS_BIN:-$PWD/tmp/security-tools/bin}"
55
+ if command -v osv-scanner >/dev/null 2>&1; then
56
+ OSV_SCANNER=(osv-scanner)
57
+ elif [ -x "$BIN_DIR/osv-scanner" ]; then
58
+ OSV_SCANNER=("$BIN_DIR/osv-scanner")
59
+ else
60
+ if [ "${MOOSE_INVENTORY_REQUIRE_SECURITY_TOOLS:-0}" = "1" ]; then
61
+ echo "osv-scanner is required but was not found. Run scripts/ci/install_security_tools.sh first." >&2
62
+ exit 2
63
+ fi
64
+ echo "osv-scanner not found; skipping osv-scanner lockfile scan."
65
+ exit 0
66
+ fi
67
+
68
+ "${OSV_SCANNER[@]}" scan source --lockfile Gemfile.lock .
@@ -0,0 +1,47 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # Installs optional security audit CLIs used by CI. They are kept out of the
5
+ # gem runtime/development bundle because they are Go command-line tools, not
6
+ # Ruby dependencies.
7
+
8
+ BIN_DIR="${MOOSE_INVENTORY_SECURITY_TOOLS_BIN:-$PWD/tmp/security-tools/bin}"
9
+ GITLEAKS_VERSION="${GITLEAKS_VERSION:-v8.30.0}"
10
+ OSV_SCANNER_VERSION="${OSV_SCANNER_VERSION:-v2.2.3}"
11
+
12
+ mkdir -p "$BIN_DIR"
13
+
14
+ if ! command -v go >/dev/null 2>&1; then
15
+ echo "Go is required to install gitleaks/osv-scanner. Install Go or use a prebuilt package." >&2
16
+ exit 2
17
+ fi
18
+
19
+ install_go_tool() {
20
+ local name="$1"
21
+ local module="$2"
22
+ local version="$3"
23
+
24
+ if command -v "$name" >/dev/null 2>&1; then
25
+ echo "$name already available at $(command -v "$name")"
26
+ return
27
+ fi
28
+
29
+ if [ -x "$BIN_DIR/$name" ]; then
30
+ echo "$name already installed at $BIN_DIR/$name"
31
+ return
32
+ fi
33
+
34
+ echo "Installing $name $version into $BIN_DIR"
35
+ GOBIN="$BIN_DIR" go install "$module@$version"
36
+ }
37
+
38
+ install_go_tool gitleaks github.com/zricethezav/gitleaks/v8 "$GITLEAKS_VERSION"
39
+ install_go_tool osv-scanner github.com/google/osv-scanner/v2/cmd/osv-scanner "$OSV_SCANNER_VERSION"
40
+
41
+ if [ -n "${GITHUB_PATH:-}" ]; then
42
+ echo "$BIN_DIR" >> "$GITHUB_PATH"
43
+ fi
44
+
45
+ export PATH="$BIN_DIR:$PATH"
46
+ gitleaks version || true
47
+ osv-scanner --version
data/scripts/files.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'yaml'
4
5
 
@@ -8,8 +9,8 @@ test_files = files.grep(%r{^(test|spec|features)/})
8
9
  require_paths = ['lib']
9
10
 
10
11
  out = {}
11
- out['Executables'.to_sym] = executables
12
- out['Test_Files'.to_sym] = test_files
13
- out['Require_Paths'.to_sym] = require_paths
12
+ out[:Executables] = executables
13
+ out[:Test_Files] = test_files
14
+ out[:Require_Paths] = require_paths
14
15
 
15
- puts out.to_yaml.to_s
16
+ puts out.to_yaml
@@ -4,6 +4,8 @@ set -euo pipefail
4
4
  sudo dnf groupinstall -y "C Development Tools and Libraries" "Development Tools"
5
5
  sudo dnf install -y \
6
6
  ansible \
7
+ gitleaks \
8
+ golang \
7
9
  ruby \
8
10
  ruby-devel \
9
11
  rubygem-bundler \
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'spec_helper'
5
+ require 'tmpdir'
6
+ require 'yaml'
7
+
8
+ RSpec.describe 'CI/CD examples' do
9
+ it 'ships parseable inventory and GitHub Actions examples' do
10
+ expect(YAML.safe_load_file('examples/ci/inventory/example-snapshot.yml')).to include('version' => 1)
11
+ workflow = YAML.safe_load_file('examples/ci/github-actions/inventory-review.yml')
12
+
13
+ expect(workflow).to include('name' => 'Inventory review example')
14
+ end
15
+
16
+ it 'keeps the snapshot validation script syntax-valid' do
17
+ expect(system('bash', '-n', 'examples/ci/scripts/validate-inventory-snapshot.sh')).to eq(true)
18
+ end
19
+
20
+ it 'validates a snapshot and writes review artifacts without production credentials' do
21
+ Dir.mktmpdir do |dir|
22
+ command = 'bundle exec ruby -Ilib bin/moose-inventory'
23
+ result = system(
24
+ { 'MOOSE_INVENTORY_CMD' => command },
25
+ 'examples/ci/scripts/validate-inventory-snapshot.sh',
26
+ 'examples/ci/inventory/example-snapshot.yml',
27
+ dir
28
+ )
29
+
30
+ expect(result).to eq(true)
31
+ expect(File).to exist(File.join(dir, 'doctor.txt'))
32
+ expect(YAML.safe_load_file(File.join(dir, 'inventory.yml'))).to include('hosts')
33
+ expect(JSON.parse(File.read(File.join(dir, 'hosts.json')))).to include('web01')
34
+ expect(JSON.parse(File.read(File.join(dir, 'ansible-inventory.json')))).to include('web')
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'yaml'
5
+
6
+ RSpec.describe 'Ansible inventory plugin examples' do
7
+ it 'ships a plugin, inventory source, and ansible.cfg example' do
8
+ expect(File).to exist('examples/ansible/inventory_plugins/moose_inventory.py')
9
+ expect(File).to exist('examples/ansible/inventory/moose_inventory.yml')
10
+ expect(File).to exist('examples/ansible/ansible.cfg')
11
+ end
12
+
13
+ it 'uses the moose_inventory plugin in the example inventory source' do
14
+ config = YAML.safe_load_file('examples/ansible/inventory/moose_inventory.yml')
15
+
16
+ expect(config).to include(
17
+ 'plugin' => 'moose_inventory',
18
+ 'executable' => 'moose-inventory',
19
+ 'config' => './example.conf',
20
+ 'env' => 'dev'
21
+ )
22
+ end
23
+
24
+ it 'keeps the plugin source syntax-valid' do
25
+ result = system('python3', '-m', 'py_compile', 'examples/ansible/inventory_plugins/moose_inventory.py')
26
+
27
+ expect(result).to eq(true)
28
+ end
29
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/BlockLength
4
+ require 'spec_helper'
5
+
6
+ RSpec.describe Moose::Inventory::Cli::Application do
7
+ before(:all) do
8
+ setup_cli_harness(command_class: described_class)
9
+ end
10
+
11
+ before(:each) do
12
+ reset_cli_harness
13
+ end
14
+
15
+ it 'prints a human success report when no issues are found' do
16
+ web = @db.models[:group].create(name: 'web')
17
+ host = @db.models[:host].create(name: 'web01')
18
+ host.add_group(web)
19
+
20
+ actual = runner { @app.start(%w[doctor]) }
21
+
22
+ expected(actual, aborted: false, STDOUT: "Inventory doctor found no issues.\n", STDERR: '')
23
+ end
24
+
25
+ it 'prints a human report and exits nonzero when issues are found' do
26
+ @db.models[:group].create(name: 'ungrouped')
27
+ host = @db.models[:host].create(name: 'lonely')
28
+ host.add_group(@db.models[:group].find(name: 'ungrouped'))
29
+
30
+ actual = runner { @app.start(%w[doctor]) }
31
+
32
+ expect(actual[:aborted]).to eq(true)
33
+ expect(actual[:STDOUT]).to include('Inventory doctor found')
34
+ expect(actual[:STDOUT]).to include('host_only_in_ungrouped')
35
+ end
36
+
37
+ it 'prints a machine-readable report when requested' do
38
+ @db.models[:group].create(name: 'ungrouped')
39
+ host = @db.models[:host].create(name: 'lonely')
40
+ host.add_group(@db.models[:group].find(name: 'ungrouped'))
41
+
42
+ actual = runner { @app.start(%w[doctor --format json]) }
43
+
44
+ expect(actual[:aborted]).to eq(true)
45
+ report = JSON.parse(actual[:STDOUT])
46
+ expect(report['ok']).to eq(false)
47
+ expect(report['issues'].map { |issue| issue['id'] }).to include('host_only_in_ungrouped')
48
+ end
49
+ end
50
+ # rubocop:enable Metrics/BlockLength
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/BlockLength
4
+ require 'spec_helper'
5
+ require 'tmpdir'
6
+
7
+ RSpec.describe Moose::Inventory::Cli::Application do
8
+ before(:all) do
9
+ setup_cli_harness(command_class: described_class)
10
+ end
11
+
12
+ before(:each) do
13
+ reset_cli_harness
14
+ end
15
+
16
+ it 'exports the current inventory snapshot as yaml' do
17
+ runner { @app.start(%w[host add web01]) }
18
+ runner { @app.start(%w[group add web]) }
19
+ runner { @app.start(%w[host addgroup web01 web]) }
20
+
21
+ actual = runner { @app.start(%w[export]) }
22
+
23
+ expect(actual[:unexpected]).to eq(false)
24
+ expect(actual[:aborted]).to eq(false)
25
+ expect(actual[:STDERR]).to eq('')
26
+ snapshot = YAML.safe_load(actual[:STDOUT])
27
+ expect(snapshot['version']).to eq(1)
28
+ expect(snapshot.dig('hosts', 'web01', 'groups')).to include('web')
29
+ expect(snapshot['groups']).to have_key('web')
30
+ end
31
+
32
+ it 'imports a validated inventory snapshot file' do
33
+ Dir.mktmpdir do |dir|
34
+ path = File.join(dir, 'inventory.yml')
35
+ File.write(
36
+ path,
37
+ {
38
+ 'version' => 1,
39
+ 'hosts' => { 'web01' => { 'groups' => ['web'], 'tags' => [], 'vars' => { 'env' => 'prod' } } },
40
+ 'groups' => { 'web' => { 'children' => [], 'tags' => [], 'vars' => { 'role' => 'frontend' } } }
41
+ }.to_yaml
42
+ )
43
+
44
+ actual = runner { @app.start(['import', path]) }
45
+
46
+ expect(actual[:unexpected]).to eq(false)
47
+ expect(actual[:aborted]).to eq(false)
48
+ expect(actual[:STDERR]).to eq('')
49
+ expect(actual[:STDOUT]).to include("Imported inventory snapshot from #{path}.")
50
+ host = @db.models[:host].find(name: 'web01')
51
+ expect(host.groups_dataset[name: 'web']).not_to be_nil
52
+ expect(host.hostvars_dataset[name: 'env'][:value]).to eq('prod')
53
+ end
54
+ end
55
+
56
+ it 'previews an inventory snapshot import without writing' do
57
+ runner { @app.start(%w[group add web]) }
58
+
59
+ Dir.mktmpdir do |dir|
60
+ path = File.join(dir, 'inventory.yml')
61
+ File.write(
62
+ path,
63
+ {
64
+ 'version' => 1,
65
+ 'hosts' => { 'web01' => { 'groups' => ['web'], 'tags' => [], 'vars' => { 'env' => 'prod' } } },
66
+ 'groups' => { 'web' => { 'children' => [], 'tags' => [], 'vars' => {} } }
67
+ }.to_yaml
68
+ )
69
+
70
+ actual = runner { @app.start(['import', path, '--preview', '--preview-format', 'json']) }
71
+
72
+ expect(actual[:unexpected]).to eq(false)
73
+ expect(actual[:aborted]).to eq(false)
74
+ expect(actual[:STDERR]).to eq('')
75
+ preview = JSON.parse(actual[:STDOUT])
76
+ expect(preview['schema_version']).to eq('snapshot-import-preview-v1')
77
+ expect(preview['changes_applied']).to eq(false)
78
+ expect(preview.dig('summary', 'hosts_created')).to eq(1)
79
+ expect(preview.dig('creates', 'hosts')).to eq(['web01'])
80
+ expect(@db.models[:host].find(name: 'web01')).to be_nil
81
+ end
82
+ end
83
+
84
+ it 'aborts on invalid snapshot input without writing' do
85
+ Dir.mktmpdir do |dir|
86
+ path = File.join(dir, 'inventory.yml')
87
+ File.write(
88
+ path,
89
+ { 'version' => 1, 'hosts' => { 'web01' => { 'groups' => ['missing'], 'vars' => {} } }, 'groups' => {} }.to_yaml
90
+ )
91
+
92
+ actual = runner { @app.start(['import', path]) }
93
+
94
+ expect(actual[:aborted]).to eq(true)
95
+ expect(actual[:STDERR]).to include("Invalid inventory snapshot: host 'web01' references unknown group 'missing'.")
96
+ expect(@db.models[:host].count).to eq(0)
97
+ end
98
+ end
99
+ end
100
+ # rubocop:enable Metrics/BlockLength
@@ -1,15 +1,25 @@
1
- # require 'spec_helper'
2
- #
3
- # RSpec.describe Moose::Inventory::Cli::Group do
4
- # before do
5
- # # Set up the configuration object
6
- # mockargs = "--format yaml --env testing --config ./test.config"
7
- # Moose::Inventory::Config
8
- # end
9
- #
10
- # describe ".add" do
11
- # it '"add test" should add a group called test' do
12
- # expect(["group","add","test").to_eq(2)
13
- # end
14
- # end
15
- # end
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Moose::Inventory::Cli::Application do
6
+ describe 'version' do
7
+ it 'prints the current Moose Inventory version' do
8
+ output = capture(:STDOUT) do
9
+ described_class.start(%w[version])
10
+ end
11
+
12
+ expect(output).to eq("Version #{Moose::Inventory::VERSION}\n")
13
+ end
14
+ end
15
+
16
+ describe 'subcommands' do
17
+ it 'registers the group subcommand' do
18
+ expect(described_class.subcommands).to include('group')
19
+ end
20
+
21
+ it 'registers the host subcommand' do
22
+ expect(described_class.subcommands).to include('host')
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'spec_helper'
5
+
6
+ # rubocop:disable Metrics/BlockLength
7
+ RSpec.describe Moose::Inventory::Cli::Audit do
8
+ before(:all) do
9
+ setup_cli_harness(command_class: Moose::Inventory::Cli::Audit)
10
+ end
11
+
12
+ before(:each) do
13
+ reset_cli_harness
14
+ end
15
+
16
+ it 'starts with an empty audit log' do
17
+ actual = runner { @app.start(%w[audit list]) }
18
+
19
+ expected(actual, aborted: false, STDOUT: "No audit events recorded.\n", STDERR: '')
20
+ end
21
+
22
+ it 'records successful mutating host commands' do
23
+ add = runner { @app.start(%w[host add app01]) }
24
+ expect(add[:unexpected]).to eq(false)
25
+
26
+ event = @db.models[:audit_event].last
27
+ expect(event.command).to eq('host add')
28
+ expect(event.action).to eq('add')
29
+ expect(event.entity_type).to eq('host')
30
+ expect(event.entity_name).to eq('app01')
31
+ expect(JSON.parse(event.details)).to include('events')
32
+ end
33
+
34
+ it 'does not record dry-run commands' do
35
+ actual = runner { @app.start(%w[host add --dry-run app01]) }
36
+ expect(actual[:unexpected]).to eq(false)
37
+
38
+ expect(@db.models[:audit_event].count).to eq(0)
39
+ end
40
+
41
+ it 'lists audit events as machine-readable JSON' do
42
+ runner { @app.start(%w[group add web]) }
43
+
44
+ actual = runner { @app.start(%w[audit list --format json]) }
45
+ parsed = JSON.parse(actual[:STDOUT])
46
+
47
+ expect(actual[:unexpected]).to eq(false)
48
+ expect(parsed.first).to include(
49
+ 'command' => 'group add',
50
+ 'action' => 'add',
51
+ 'entity_type' => 'group',
52
+ 'entity_name' => 'web'
53
+ )
54
+ end
55
+ end
56
+ # rubocop:enable Metrics/BlockLength
@@ -1,25 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
- RSpec.describe Moose::Inventory::Cli::Application do
4
- before do
5
- @app = Moose::Inventory::Cli::Application
6
- end
5
+ RSpec.describe Moose::Inventory::Cli do
6
+ describe '.start' do
7
+ it 'wires config, db, and application explicitly' do
8
+ args = ['--format', 'yaml']
9
+ config = instance_double('Config')
10
+ db = instance_double('DB')
11
+ application = class_double('Application')
7
12
 
8
- describe '.version' do
9
- # --------------------
10
- it 'method should be responsive' do
11
- result = @app.instance_methods(false).include?(:version)
12
- expect(result).to eq(true)
13
- end
13
+ expect(config).to receive(:init).with(args).ordered
14
+ expect(db).to receive(:init).ordered
15
+ expect(config).to receive(:application_args).ordered.and_return(['version'])
16
+ expect(application).to receive(:start).with(['version']).ordered
14
17
 
15
- # --------------------
16
- # it 'should output version information' do
17
- # actual = runner { @app.version }
18
- #
19
- # desired = {}
20
- # desired[:STDERR] = "Version #{Moose::Inventory::VERSION}"
21
- #
22
- # expected(actual, desired)
23
- # end
18
+ described_class.start(args, config: config, db: db, application: application)
19
+ end
24
20
  end
25
21
  end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'stringio'
5
+
6
+ # rubocop:disable Metrics/BlockLength
7
+ RSpec.describe Moose::Inventory::Cli::Console do
8
+ before(:all) do
9
+ setup_cli_harness(command_class: Moose::Inventory::Cli::Application)
10
+ end
11
+
12
+ before(:each) do
13
+ reset_cli_harness
14
+ end
15
+
16
+ def run_console(input)
17
+ original_stdin = $stdin
18
+ $stdin = StringIO.new(input)
19
+ runner { @app.start(%w[console]) }
20
+ ensure
21
+ $stdin = original_stdin
22
+ end
23
+
24
+ it 'prints help and exits' do
25
+ actual = run_console("help\nquit\n")
26
+
27
+ expect(actual[:unexpected]).to eq(false)
28
+ expect(actual[:STDOUT]).to include('Moose Inventory console (read-only). Type help or quit.')
29
+ expect(actual[:STDOUT]).to include('- hosts')
30
+ expect(actual[:STDOUT]).to include('- group NAME')
31
+ expect(actual[:STDOUT]).to include('Goodbye.')
32
+ end
33
+
34
+ it 'browses hosts, groups, tags, and entity detail without mutating' do
35
+ runner { @app.start(%w[group add web]) }
36
+ runner { @app.start(%w[host add web01 --groups web]) }
37
+ runner { @app.start(%w[host addtag web01 prod]) }
38
+ before_audit_count = @db.models[:audit_event].count
39
+
40
+ actual = run_console("hosts\ngroups\nhost web01\ntags host web01\nquit\n")
41
+
42
+ expect(actual[:unexpected]).to eq(false)
43
+ expect(actual[:STDOUT]).to include('Hosts: web01')
44
+ expect(actual[:STDOUT]).to include('Groups: web')
45
+ expect(actual[:STDOUT]).to include('Host: web01')
46
+ expect(actual[:STDOUT]).to include('Tags: prod')
47
+ expect(@db.models[:audit_event].count).to eq(before_audit_count)
48
+ end
49
+
50
+ it 'reports unknown and missing entities safely' do
51
+ actual = run_console("nonsense\nhost missing\nquit\n")
52
+
53
+ expect(actual[:unexpected]).to eq(false)
54
+ expect(actual[:STDOUT]).to include('Unknown command: nonsense')
55
+ expect(actual[:STDOUT]).to include("Host 'missing' not found.")
56
+ end
57
+
58
+ it 'supports quoted read-only entity names' do
59
+ group = @db.models[:group].create(name: 'prod group')
60
+ host = @db.models[:host].create(name: 'web 01')
61
+ tag = @db.models[:tag].create(name: 'critical host')
62
+ host.add_group(group)
63
+ host.add_tag(tag)
64
+ before_audit_count = @db.models[:audit_event].count
65
+
66
+ actual = run_console("host \"web 01\"\ntags host \"web 01\"\ngroup 'prod group'\nquit\n")
67
+
68
+ expect(actual[:unexpected]).to eq(false)
69
+ expect(actual[:STDOUT]).to include('Host: web 01')
70
+ expect(actual[:STDOUT]).to include('Groups: prod group')
71
+ expect(actual[:STDOUT]).to include('critical host')
72
+ expect(actual[:STDOUT]).to include('Group: prod group')
73
+ expect(actual[:STDOUT]).to include('Hosts: web 01')
74
+ expect(@db.models[:audit_event].count).to eq(before_audit_count)
75
+ end
76
+
77
+ it 'reports command-specific usage for extra or invalid arguments' do
78
+ actual = run_console("host one two\ntags host\naudit nope\naudit 0\nhelp extra\nquit\n")
79
+
80
+ expect(actual[:unexpected]).to eq(false)
81
+ expect(actual[:STDOUT]).to include('Usage: host NAME')
82
+ expect(actual[:STDOUT]).to include('Usage: tags host|group NAME')
83
+ expect(actual[:STDOUT].scan('Usage: audit [LIMIT]').length).to eq(2)
84
+ expect(actual[:STDOUT]).to include('Usage: help')
85
+ end
86
+
87
+ it 'reports unclosed quoted input without leaving read-only mode' do
88
+ before_audit_count = @db.models[:audit_event].count
89
+
90
+ actual = run_console("host \"unterminated\nquit\n")
91
+
92
+ expect(actual[:unexpected]).to eq(false)
93
+ expect(actual[:STDOUT]).to include('Invalid command syntax:')
94
+ expect(actual[:STDOUT]).to include('Goodbye.')
95
+ expect(@db.models[:audit_event].count).to eq(before_audit_count)
96
+ end
97
+ end
98
+ # rubocop:enable Metrics/BlockLength