moose-inventory 1.0.9 → 2.0
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/ci.yml +15 -1
- data/.github/workflows/release.yml +58 -0
- data/.gitleaks.toml +9 -0
- data/.rubocop.yml +28 -0
- data/BACKLOG.md +130 -24
- data/Gemfile.lock +36 -1
- data/README.md +26 -6
- data/Rakefile +1 -1
- data/docs/release/publishing.md +44 -48
- data/docs/release/release-readiness.md +14 -0
- data/docs/security-audit-2026-05-26-rerun.md +75 -0
- data/docs/security-audit-2026-05-26.md +63 -0
- data/lib/moose_inventory/cli/group.rb +3 -0
- data/lib/moose_inventory/cli/group_add.rb +89 -73
- data/lib/moose_inventory/cli/group_addchild.rb +77 -60
- data/lib/moose_inventory/cli/group_addhost.rb +78 -65
- data/lib/moose_inventory/cli/group_rm.rb +101 -71
- data/lib/moose_inventory/cli/group_rmchild.rb +99 -53
- data/lib/moose_inventory/cli/group_rmhost.rb +64 -56
- data/lib/moose_inventory/cli/helpers.rb +76 -0
- data/lib/moose_inventory/cli/host.rb +3 -0
- data/lib/moose_inventory/cli/host_add.rb +47 -62
- data/lib/moose_inventory/cli/host_addgroup.rb +73 -64
- data/lib/moose_inventory/cli/host_rmgroup.rb +58 -55
- data/lib/moose_inventory/db/db.rb +27 -7
- data/lib/moose_inventory/inventory_context.rb +50 -0
- data/lib/moose_inventory/operations/add_associations.rb +127 -0
- data/lib/moose_inventory/operations/add_groups.rb +115 -0
- data/lib/moose_inventory/operations/add_hosts.rb +110 -0
- data/lib/moose_inventory/operations/group_child_relations.rb +118 -0
- data/lib/moose_inventory/operations/group_cleanup.rb +55 -0
- data/lib/moose_inventory/operations/remove_associations.rb +101 -0
- data/lib/moose_inventory/operations/remove_groups.rb +79 -0
- data/lib/moose_inventory/version.rb +1 -1
- data/moose-inventory.gemspec +3 -0
- data/scripts/check.sh +2 -0
- data/scripts/ci/check_permissions.sh +3 -0
- data/scripts/ci/check_rubocop.sh +28 -0
- data/scripts/ci/check_secrets.sh +26 -0
- data/scripts/ci/check_security.sh +18 -0
- data/scripts/ci/install_security_tools.sh +47 -0
- data/scripts/install_dependencies.sh +2 -0
- data/spec/lib/moose_inventory/cli/group_rm_spec.rb +40 -0
- data/spec/lib/moose_inventory/cli/group_rmchild_spec.rb +45 -0
- data/spec/lib/moose_inventory/db/db_spec.rb +162 -0
- data/spec/lib/moose_inventory/operations/add_associations_spec.rb +77 -0
- data/spec/lib/moose_inventory/operations/add_groups_spec.rb +65 -0
- data/spec/lib/moose_inventory/operations/add_hosts_spec.rb +69 -0
- data/spec/lib/moose_inventory/operations/group_child_relations_spec.rb +76 -0
- data/spec/lib/moose_inventory/operations/remove_associations_spec.rb +78 -0
- data/spec/lib/moose_inventory/operations/remove_groups_spec.rb +57 -0
- metadata +90 -1
|
@@ -24,6 +24,20 @@ GitHub Actions workflow: `.github/workflows/ci.yml`.
|
|
|
24
24
|
|
|
25
25
|
It installs native headers needed by the DB gems, runs the same `./scripts/check.sh` gate used locally, and tests the maintained Ruby version range through the GitHub Actions matrix.
|
|
26
26
|
|
|
27
|
+
## Trusted publishing gate
|
|
28
|
+
|
|
29
|
+
GitHub Actions workflow: `.github/workflows/release.yml`.
|
|
30
|
+
|
|
31
|
+
The release workflow runs when a `v*` tag is pushed. It:
|
|
32
|
+
|
|
33
|
+
1. Checks out the repository using `actions/checkout@v5`.
|
|
34
|
+
2. Installs Ruby and native database build dependencies.
|
|
35
|
+
3. Fails if the tag version does not match `Moose::Inventory::VERSION`.
|
|
36
|
+
4. Runs the full local `./scripts/check.sh` gate.
|
|
37
|
+
5. Publishes the gem with `rubygems/release-gem@v1` using RubyGems trusted publishing/OIDC.
|
|
38
|
+
|
|
39
|
+
RubyGems has a trusted publisher configured for repository `RusDavies/moose-inventory`, workflow `release.yml`, and environment `release`, so the workflow can request a short-lived publish token when a real release tag is pushed.
|
|
40
|
+
|
|
27
41
|
## Package sanity expectations
|
|
28
42
|
|
|
29
43
|
`package_sanity.sh` validates that the built gem includes at least:
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Security Audit Rerun — 2026-05-26
|
|
2
|
+
|
|
3
|
+
Repository: `RusDavies/moose-inventory`
|
|
4
|
+
Local path: `/home/skippy/.openclaw/workspace/projects/moose-inventory`
|
|
5
|
+
Audited commit at start: `a07b5c89214a3cee66170217c5b38e9ad2ae093a`
|
|
6
|
+
Audit branch: `security-audit-2026-05-26-rerun`
|
|
7
|
+
Evidence store: `.openclaw-security-audit/audit.sqlite`, audit run `2`
|
|
8
|
+
|
|
9
|
+
## Executive summary
|
|
10
|
+
|
|
11
|
+
The rerun found no exploitable application vulnerabilities in the Ruby CLI/config/database code reviewed, and all deterministic dependency, advisory, package, and secret-scanning gates passed.
|
|
12
|
+
|
|
13
|
+
One release-supply-chain hardening gap was identified and fixed during the audit: the release workflow ran `./scripts/check.sh` without installing or requiring the dedicated security tools, so a tag-based release could publish even if `gitleaks`, `osv-scanner`, or `bundler-audit` coverage was absent from that release job. CI already enforced those tools; release now does too.
|
|
14
|
+
|
|
15
|
+
## Scope
|
|
16
|
+
|
|
17
|
+
Reviewed security-relevant surfaces and changes since the prior audit:
|
|
18
|
+
|
|
19
|
+
- GitHub Actions CI and release workflows.
|
|
20
|
+
- Security-tool installation and enforcement scripts.
|
|
21
|
+
- Ruby CLI entrypoints and Thor command surfaces.
|
|
22
|
+
- YAML configuration loading.
|
|
23
|
+
- SQLite/MySQL/PostgreSQL connection setup and password handling.
|
|
24
|
+
- Recursive group deletion behavior touched by recent issue work.
|
|
25
|
+
- Gem packaging/release path.
|
|
26
|
+
|
|
27
|
+
## Deterministic results
|
|
28
|
+
|
|
29
|
+
- Full local required-tool gate: passed.
|
|
30
|
+
- RSpec: 268 examples, 0 failures.
|
|
31
|
+
- Coverage: 96.52% line coverage.
|
|
32
|
+
- Custom OSV dependency check: 45 dependencies queried, 0 vulnerable.
|
|
33
|
+
- `bundler-audit`: no vulnerabilities found.
|
|
34
|
+
- `osv-scanner`: no issues found in `Gemfile.lock`.
|
|
35
|
+
- `gitleaks`: dedicated secret scan passed.
|
|
36
|
+
- Package sanity: built and inspected `tmp/pkg/moose-inventory.gem` successfully.
|
|
37
|
+
- Semgrep Ruby registry scan: 62 tracked Ruby files scanned with 44 Ruby rules, 0 findings.
|
|
38
|
+
- GitHub Dependabot open alerts: 0.
|
|
39
|
+
- GitHub code scanning alerts: unavailable / no analysis found (`404`).
|
|
40
|
+
- GitHub secret scanning alerts: unavailable because secret scanning is disabled for this repository.
|
|
41
|
+
- Workflow YAML parse check: `ci.yml` and `release.yml` parsed successfully with Ruby Psych.
|
|
42
|
+
- Current GitHub CI before audit branch: latest `master` CI run succeeded for Ruby 3.2, 3.3, and 3.4.
|
|
43
|
+
|
|
44
|
+
## Finding fixed during audit
|
|
45
|
+
|
|
46
|
+
### SEC-RERUN-2026-05-26-01 — Release workflow did not require dedicated security tools
|
|
47
|
+
|
|
48
|
+
- Priority before fix: P2 medium, release supply-chain hardening.
|
|
49
|
+
- Exposure: tag-triggered release workflow.
|
|
50
|
+
- Affected file: `.github/workflows/release.yml`.
|
|
51
|
+
- Evidence: CI installed `gitleaks`/`osv-scanner` and set `MOOSE_INVENTORY_REQUIRE_SECURITY_TOOLS=1`, but the release workflow only ran `./scripts/check.sh`. In local mode, `scripts/ci/check_security.sh` and `scripts/ci/check_secrets.sh` intentionally skip missing optional security tools unless `MOOSE_INVENTORY_REQUIRE_SECURITY_TOOLS=1` is set.
|
|
52
|
+
- Impact: a release tag created from an unexpected commit or during a tooling/path issue could publish without the same dedicated SCA/secret-scan enforcement as CI.
|
|
53
|
+
- Fix applied: release workflow now sets up Go with cache disabled, installs the pinned security CLIs via `scripts/ci/install_security_tools.sh`, runs native dependency installation with a 5-minute timeout, and runs `./scripts/check.sh` with `MOOSE_INVENTORY_REQUIRE_SECURITY_TOOLS=1`.
|
|
54
|
+
- Verification: full local required-tool gate passed after the workflow change.
|
|
55
|
+
- Residual risk: release workflow can only be fully proven on the next real release tag because already-published `v1.0.9` must not be retagged.
|
|
56
|
+
|
|
57
|
+
## Reviewed areas with no actionable finding
|
|
58
|
+
|
|
59
|
+
- YAML config loading uses `YAML.safe_load_file` with aliases disabled and no permitted classes/symbols.
|
|
60
|
+
- SQLite database path handling creates parent directories with `FileUtils.mkdir_p`; this is local config-driven CLI behavior, not a remotely reachable path traversal surface.
|
|
61
|
+
- MySQL/PostgreSQL password handling supports `password_env`; plaintext `password` remains for compatibility but README guidance discourages committing it.
|
|
62
|
+
- CLI input reaches Sequel model operations rather than shell execution; no shell/eval sink was identified in application code.
|
|
63
|
+
- Recent recursive group deletion is explicit opt-in and keeps host fallback behavior covered by regression tests.
|
|
64
|
+
- GitHub Actions release job uses OIDC/trusted publishing and does not store a RubyGems API key in the workflow.
|
|
65
|
+
|
|
66
|
+
## Limitations
|
|
67
|
+
|
|
68
|
+
- This was a local/source and CI/release workflow audit, not an active test against live external databases or RubyGems publishing.
|
|
69
|
+
- GitHub code scanning is not configured, so there were no CodeQL/code-scanning results to review.
|
|
70
|
+
- GitHub secret scanning is disabled for the repository; local `gitleaks` coverage was used instead.
|
|
71
|
+
- The release trusted-publishing path still needs verification on the next real version tag.
|
|
72
|
+
|
|
73
|
+
## Conclusion
|
|
74
|
+
|
|
75
|
+
No open exploitable vulnerabilities remain from this rerun. The only identified security gap was release-pipeline parity with CI security tooling, and it was fixed in this audit branch.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Security audit — 2026-05-26
|
|
2
|
+
|
|
3
|
+
Scope: local static/security review of the `moose-inventory` Ruby CLI/gem at commit `8c3eaada5d70ef599961b8ca8b78e12ea4ce83c9` on branch `security-audit-2026-05-26`.
|
|
4
|
+
|
|
5
|
+
## Executive summary
|
|
6
|
+
|
|
7
|
+
No actionable security vulnerabilities were identified in this pass.
|
|
8
|
+
|
|
9
|
+
The meaningful attack surface remains local CLI execution, configuration-file loading, database access through Sequel, package/release automation, and developer tooling. There is no HTTP server, RPC endpoint, webhook handler, queue consumer, file upload parser, shell-command execution path, or plugin system in this repository.
|
|
10
|
+
|
|
11
|
+
The areas remediated in the prior 2026-05-21 audit remain in good shape: YAML config loading uses `YAML.safe_load_file`, DB credentials can be supplied through environment variables, OSV reports no known vulnerable locked RubyGems dependencies, CI/package sanity gates are present, and GitHub Dependabot has no open alerts.
|
|
12
|
+
|
|
13
|
+
## Surfaces reviewed
|
|
14
|
+
|
|
15
|
+
- CLI entrypoint: `bin/moose-inventory`
|
|
16
|
+
- Global option parsing and config loading: `lib/moose_inventory/config/config.rb`
|
|
17
|
+
- DB connection/schema/transaction code: `lib/moose_inventory/db/db.rb`
|
|
18
|
+
- Sequel models and associations: `lib/moose_inventory/db/models.rb`
|
|
19
|
+
- CLI command handlers under `lib/moose_inventory/cli/`
|
|
20
|
+
- Formatter/output serialization: `lib/moose_inventory/cli/formatter.rb`
|
|
21
|
+
- Packaging and release metadata: `moose-inventory.gemspec`, `Gemfile`, `Gemfile.lock`, `.github/workflows/ci.yml`, `.github/workflows/release.yml`
|
|
22
|
+
- Helper scripts under `scripts/`
|
|
23
|
+
- Test config/spec fixtures under `spec/`
|
|
24
|
+
|
|
25
|
+
## Findings
|
|
26
|
+
|
|
27
|
+
No P0/P1/P2/P3 actionable findings were identified.
|
|
28
|
+
|
|
29
|
+
## Notable negative findings
|
|
30
|
+
|
|
31
|
+
- Config deserialization uses `YAML.safe_load_file` with aliases disabled and no permitted classes/symbols.
|
|
32
|
+
- No runtime shell execution sinks were identified.
|
|
33
|
+
- Database access uses Sequel model/dataset/hash APIs for user-controlled names, groups, hosts, and variables; no raw SQL interpolation was identified in reviewed runtime paths.
|
|
34
|
+
- MySQL/PostgreSQL passwords can be supplied via `password_env`, and README guidance prefers environment-backed passwords over plaintext config values.
|
|
35
|
+
- No committed secrets were identified outside expected example/test placeholders.
|
|
36
|
+
- GitHub Actions release publishing uses RubyGems trusted publishing/OIDC rather than a stored RubyGems API key.
|
|
37
|
+
- Dependency advisory gate queried OSV for 41 locked RubyGems dependency records and reported zero known vulnerabilities.
|
|
38
|
+
- GitHub Dependabot open-alert query returned zero open alerts.
|
|
39
|
+
|
|
40
|
+
## Tooling evidence
|
|
41
|
+
|
|
42
|
+
- Audit evidence store initialized at `.openclaw-security-audit/audit.sqlite` with `audit_run_id=1`.
|
|
43
|
+
- Inventory: 187 files; Ruby manifests detected: `Gemfile`, `Gemfile.lock`, plus generated package-sanity manifests under `tmp/package-sanity`.
|
|
44
|
+
- Symbol extractor scanned 156 files but did not extract Ruby symbols/surfaces with the current lightweight extractor.
|
|
45
|
+
- Semgrep auto-config failed because metrics are disabled; reran explicit Ruby registry rules instead.
|
|
46
|
+
- Semgrep: `semgrep --config p/ruby --json --metrics=off --exclude .openclaw-security-audit --exclude spec/reports --exclude tmp .` scanned 62 tracked Ruby files with 44 rules and returned 0 findings.
|
|
47
|
+
- Dependency advisory gate: `./scripts/ci/check_security.sh` queried 41 RubyGems dependencies and returned 0 vulnerable dependencies.
|
|
48
|
+
- Full local gate: `./scripts/check.sh` passed with 268 examples, 0 failures, 96.52% line coverage, OSV 0 vulnerabilities, and package sanity passed.
|
|
49
|
+
- GitHub Dependabot: `gh api 'repos/RusDavies/moose-inventory/dependabot/alerts?state=open' --jq 'length'` returned `0`.
|
|
50
|
+
- GitHub code-scanning alerts could not be queried because no code-scanning analysis exists for this repository; GitHub returned `404 no analysis found`.
|
|
51
|
+
|
|
52
|
+
## Tooling limitations
|
|
53
|
+
|
|
54
|
+
- `osv-scanner`, `bundler-audit`/`bundle-audit`, `gitleaks`, `trufflehog`, `brakeman`, `flog`, and `reek` were not installed in this environment.
|
|
55
|
+
- `bundle exec rubocop` could not run because RuboCop is not part of the bundle. This is not a security gate failure, but it limits style/static-quality coverage.
|
|
56
|
+
- Secret scanning was limited to tracked-file grep patterns because dedicated secret scanners were unavailable.
|
|
57
|
+
- The audit did not perform active exploitation against external systems or live database servers.
|
|
58
|
+
|
|
59
|
+
## Residual risks / recommendations
|
|
60
|
+
|
|
61
|
+
- Consider adding a dedicated secret scanner such as `gitleaks` or `trufflehog` to local/CI security tooling if this project will accept outside contributions.
|
|
62
|
+
- Consider adding `bundler-audit` or `osv-scanner` as an optional developer tool if broader advisory coverage is desired beyond the existing custom OSV gate.
|
|
63
|
+
- Keep generated coverage and package-sanity artifacts excluded from security scans; they are noisy and not part of the runtime gem surface.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'thor'
|
|
2
2
|
require_relative './formatter.rb'
|
|
3
|
+
require_relative './helpers.rb'
|
|
3
4
|
|
|
4
5
|
module Moose
|
|
5
6
|
module Inventory
|
|
@@ -7,6 +8,8 @@ module Moose
|
|
|
7
8
|
##
|
|
8
9
|
# Class implementing the "group" methods of the CLI
|
|
9
10
|
class Group < Thor
|
|
11
|
+
include Moose::Inventory::Cli::Helpers
|
|
12
|
+
|
|
10
13
|
require_relative 'group_add'
|
|
11
14
|
require_relative 'group_get'
|
|
12
15
|
require_relative 'group_list'
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'thor'
|
|
2
|
-
require_relative '
|
|
4
|
+
require_relative 'formatter'
|
|
5
|
+
require_relative '../inventory_context'
|
|
6
|
+
require_relative '../operations/add_groups'
|
|
3
7
|
|
|
4
8
|
module Moose
|
|
5
9
|
module Inventory
|
|
@@ -10,86 +14,98 @@ module Moose
|
|
|
10
14
|
#==========================
|
|
11
15
|
desc 'add NAME', 'Add a group NAME to the inventory'
|
|
12
16
|
option :hosts
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
# rubocop:enable Metrics/LineLength
|
|
16
|
-
if argv.empty?
|
|
17
|
-
abort("ERROR: Wrong number of arguments, #{argv.length} for 1 or more.")
|
|
18
|
-
end
|
|
17
|
+
def add(*argv)
|
|
18
|
+
abort_if_missing_args(argv, 1, '1 or more')
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
options[:hosts] = '' if options[:hosts].nil?
|
|
23
|
-
hosts = options[:hosts].downcase.split(',').uniq
|
|
20
|
+
names = normalize_names(argv)
|
|
21
|
+
hosts = csv_option_names(options[:hosts])
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
abort_if_automatic_group(
|
|
24
|
+
names,
|
|
25
|
+
"ERROR: Cannot manually manipulate the automatic group 'ungrouped'\n"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
result = Moose::Inventory::Operations::AddGroups
|
|
29
|
+
.new(context: Moose::Inventory::InventoryContext.new(db: db))
|
|
30
|
+
.call(names: names, hosts: hosts)
|
|
31
|
+
render_add_groups_events(result.events)
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
db = Moose::Inventory::DB
|
|
32
|
-
fmt = Moose::Inventory::Cli::Formatter
|
|
33
|
-
|
|
34
|
-
# Transaction
|
|
35
|
-
warn_count = 0
|
|
36
|
-
db.transaction do # Transaction start
|
|
37
|
-
names.each do |name|
|
|
38
|
-
# Add the group
|
|
39
|
-
puts "Add group '#{name}':"
|
|
40
|
-
group = db.models[:group].find(name: name)
|
|
41
|
-
hosts_ds = nil
|
|
42
|
-
fmt.puts 2, '- create group...'
|
|
43
|
-
if group.nil?
|
|
44
|
-
group = db.models[:group].create(name: name)
|
|
45
|
-
fmt.puts 4, '- OK'
|
|
46
|
-
else
|
|
47
|
-
warn_count += 1
|
|
48
|
-
fmt.warn "Group '#{name}' already exists, skipping creation.\n"
|
|
49
|
-
fmt.puts 4, '- already exists, skipping.'
|
|
50
|
-
hosts_ds = group.hosts_dataset
|
|
51
|
-
fmt.puts 4, '- OK'
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Associate with hosts
|
|
55
|
-
hosts.each do |h|
|
|
56
|
-
next if h.nil? || h.empty?
|
|
57
|
-
fmt.puts 2, "- add association {group:#{name} <-> host:#{h}}..."
|
|
58
|
-
host = db.models[:host].find(name: h)
|
|
59
|
-
if host.nil?
|
|
60
|
-
warn_count += 1
|
|
61
|
-
fmt.warn "Host '#{h}' doesn't exist, but will be created.\n"
|
|
62
|
-
fmt.puts 4, "- host doesn't exist, creating now..."
|
|
63
|
-
host = db.models[:host].create(name: h)
|
|
64
|
-
fmt.puts 6, '- OK'
|
|
65
|
-
end
|
|
66
|
-
if !hosts_ds.nil? && !hosts_ds[name: h].nil?
|
|
67
|
-
warn_count += 1
|
|
68
|
-
fmt.warn "Association {group:#{name} <-> host:#{h}}"\
|
|
69
|
-
" already exists, skipping creation.\n"
|
|
70
|
-
fmt.puts 4, '- already exists, skipping.'
|
|
71
|
-
else
|
|
72
|
-
group.add_host(host)
|
|
73
|
-
end
|
|
74
|
-
fmt.puts 4, '- OK'
|
|
75
|
-
|
|
76
|
-
# Handle the host's automatic 'ungrouped' group
|
|
77
|
-
ungrouped = host.groups_dataset[name: 'ungrouped']
|
|
78
|
-
next if ungrouped.nil?
|
|
79
|
-
fmt.puts 2, '- remove automatic association {group:ungrouped'\
|
|
80
|
-
" <-> host:#{h}}..."
|
|
81
|
-
host.remove_group(ungrouped) unless ungrouped.nil?
|
|
82
|
-
fmt.puts 4, '- OK'
|
|
83
|
-
end
|
|
84
|
-
fmt.puts 2, '- all OK'
|
|
85
|
-
end
|
|
86
|
-
end # Transaction end
|
|
87
|
-
if warn_count == 0
|
|
33
|
+
if result.warning_count.zero?
|
|
88
34
|
puts 'Succeeded'
|
|
89
35
|
else
|
|
90
36
|
puts 'Succeeded, with warnings.'
|
|
91
37
|
end
|
|
92
38
|
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def render_add_groups_events(events)
|
|
43
|
+
events.each { |event| render_add_groups_event(event) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def render_add_groups_event(event)
|
|
47
|
+
payload = event.payload
|
|
48
|
+
|
|
49
|
+
return render_add_groups_event_puts(event.type, payload) if puts_event?(event.type)
|
|
50
|
+
return render_add_groups_event_warn(event.type, payload) if warn_event?(event.type)
|
|
51
|
+
|
|
52
|
+
render_add_groups_event_fmt(event.type, payload)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def puts_event?(type)
|
|
56
|
+
type == :group_started
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def warn_event?(type)
|
|
60
|
+
%i[group_exists host_missing_created association_exists].include?(type)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def render_add_groups_event_puts(type, payload)
|
|
64
|
+
puts "Add group '#{payload[:name]}':" if type == :group_started
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def render_add_groups_event_warn(type, payload)
|
|
68
|
+
case type
|
|
69
|
+
when :group_exists
|
|
70
|
+
fmt.warn "Group '#{payload[:name]}' already exists, skipping creation.\n"
|
|
71
|
+
when :host_missing_created
|
|
72
|
+
fmt.warn "Host '#{payload[:name]}' doesn't exist, but will be created.\n"
|
|
73
|
+
when :association_exists
|
|
74
|
+
fmt.warn(
|
|
75
|
+
"Association {group:#{payload[:group]} <-> host:#{payload[:host]}} " \
|
|
76
|
+
"already exists, skipping creation.\n"
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def render_add_groups_event_fmt(type, payload)
|
|
82
|
+
return render_add_groups_event_status(type, payload) if status_event?(type)
|
|
83
|
+
|
|
84
|
+
case type
|
|
85
|
+
when :creating_group
|
|
86
|
+
fmt.puts 2, '- create group...'
|
|
87
|
+
when :adding_association
|
|
88
|
+
fmt.puts 2, "- add association {group:#{payload[:group]} <-> host:#{payload[:host]}}..."
|
|
89
|
+
when :host_creating_now
|
|
90
|
+
fmt.puts 4, '- host doesn\'t exist, creating now...'
|
|
91
|
+
when :removing_automatic_group
|
|
92
|
+
fmt.puts 2, "- remove automatic association {group:ungrouped <-> host:#{payload[:host]}}..."
|
|
93
|
+
when :group_complete
|
|
94
|
+
fmt.puts 2, '- all OK'
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def status_event?(type)
|
|
99
|
+
%i[already_exists_skipping ok].include?(type)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def render_add_groups_event_status(type, payload)
|
|
103
|
+
if type == :already_exists_skipping
|
|
104
|
+
fmt.puts payload[:indent], '- already exists, skipping.'
|
|
105
|
+
else
|
|
106
|
+
fmt.puts payload[:indent], '- OK'
|
|
107
|
+
end
|
|
108
|
+
end
|
|
93
109
|
end
|
|
94
110
|
end
|
|
95
111
|
end
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'thor'
|
|
2
|
-
require_relative '
|
|
4
|
+
require_relative 'formatter'
|
|
5
|
+
require_relative '../inventory_context'
|
|
6
|
+
require_relative '../operations/group_child_relations'
|
|
3
7
|
|
|
4
8
|
module Moose
|
|
5
9
|
module Inventory
|
|
@@ -10,78 +14,91 @@ module Moose
|
|
|
10
14
|
#==========================
|
|
11
15
|
desc 'addchild PARENTGROUP CHILDGROUP_1 [CHILDGROUP_2 ... ]',
|
|
12
16
|
'Associate one or more child-groups CHILDGROUP_n with PARENTGROUP'
|
|
13
|
-
def addchild(*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
def addchild(*argv)
|
|
18
|
+
abort_if_missing_args(argv, 2, '2 or more')
|
|
19
|
+
|
|
20
|
+
pname = argv[0].downcase
|
|
21
|
+
cnames = normalize_names(argv.slice(1, argv.length - 1))
|
|
22
|
+
|
|
23
|
+
abort_if_automatic_group([pname] + cnames)
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
pname = args[0].downcase
|
|
22
|
-
cnames = args.slice(1, args.length - 1).uniq.map(&:downcase)
|
|
25
|
+
result = add_children_to_group(pname, cnames)
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
if result.warning_count.zero?
|
|
28
|
+
puts 'Succeeded.'
|
|
29
|
+
else
|
|
30
|
+
puts 'Succeeded, with warnings.'
|
|
27
31
|
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
28
35
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
def add_children_to_group(parent_name, child_names)
|
|
37
|
+
context = Moose::Inventory::InventoryContext.new(db: db)
|
|
38
|
+
operation = Moose::Inventory::Operations::GroupChildRelations.new(context: context)
|
|
32
39
|
|
|
33
|
-
# Transaction
|
|
34
|
-
warn_count = 0
|
|
35
40
|
begin
|
|
36
|
-
db.transaction do
|
|
37
|
-
puts "Associate parent group '#{
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
# Associate parent group with the child groups
|
|
47
|
-
|
|
48
|
-
groups_ds = pgroup.children_dataset
|
|
49
|
-
cnames.each do |cname|
|
|
50
|
-
fmt.puts 2, "- add association {group:#{pname} <-> group:#{cname}}..."
|
|
51
|
-
|
|
52
|
-
# Check against existing associations
|
|
53
|
-
unless groups_ds[name: cname].nil?
|
|
54
|
-
warn_count += 1
|
|
55
|
-
fmt.warn "Association {group:#{pname} <-> group:#{cname}}}"\
|
|
56
|
-
" already exists, skipping.\n"
|
|
57
|
-
fmt.puts 4, '- already exists, skipping.'
|
|
58
|
-
fmt.puts 4, '- OK'
|
|
59
|
-
next
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# Add new association
|
|
63
|
-
cgroup = db.models[:group].find(name: cname)
|
|
64
|
-
if cgroup.nil?
|
|
65
|
-
warn_count += 1
|
|
66
|
-
fmt.warn "Group '#{cname}' does not exist and will be created.\n"
|
|
67
|
-
fmt.puts 4, '- child group does not exist, creating now...'
|
|
68
|
-
cgroup = db.models[:group].create(name: cname)
|
|
69
|
-
fmt.puts 6, '- OK'
|
|
70
|
-
end
|
|
71
|
-
pgroup.add_child(cgroup)
|
|
72
|
-
fmt.puts 4, '- OK'
|
|
73
|
-
end
|
|
41
|
+
db.transaction do
|
|
42
|
+
puts "Associate parent group '#{parent_name}' with child group(s) '#{child_names.join(',')}':"
|
|
43
|
+
parent_group = fetch_existing_group_for_child_relation(context, parent_name)
|
|
44
|
+
result = operation.add_children(
|
|
45
|
+
parent_group: parent_group,
|
|
46
|
+
parent_name: parent_name,
|
|
47
|
+
child_names: child_names
|
|
48
|
+
)
|
|
49
|
+
render_addchild_events(result.events)
|
|
74
50
|
fmt.puts 2, '- all OK'
|
|
75
|
-
|
|
51
|
+
return result
|
|
52
|
+
end
|
|
76
53
|
rescue db.exceptions[:moose] => e
|
|
77
54
|
abort("ERROR: #{e}")
|
|
78
55
|
end
|
|
79
|
-
|
|
80
|
-
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def fetch_existing_group_for_child_relation(context, name)
|
|
59
|
+
fmt.puts 2, "- retrieve group '#{name}'..."
|
|
60
|
+
group = context.find_group(name)
|
|
61
|
+
abort("ERROR: The group '#{name}' does not exist.") if group.nil?
|
|
62
|
+
|
|
63
|
+
fmt.puts 4, '- OK'
|
|
64
|
+
group
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def render_addchild_events(events)
|
|
68
|
+
events.each { |event| render_addchild_event(event) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def render_addchild_event(event)
|
|
72
|
+
payload = event.payload
|
|
73
|
+
|
|
74
|
+
return render_addchild_warning(event.type, payload) if addchild_warning?(event.type)
|
|
75
|
+
return render_addchild_existing(payload) if event.type == :already_exists_skipping
|
|
76
|
+
|
|
77
|
+
case event.type
|
|
78
|
+
when :adding_child_association
|
|
79
|
+
fmt.puts 2, "- add association {group:#{payload[:parent]} <-> group:#{payload[:child]}}..."
|
|
80
|
+
when :child_group_creating_now
|
|
81
|
+
fmt.puts 4, '- child group does not exist, creating now...'
|
|
82
|
+
when :ok
|
|
83
|
+
fmt.puts payload[:indent], '- OK'
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def addchild_warning?(type)
|
|
88
|
+
%i[child_association_exists child_group_missing].include?(type)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def render_addchild_warning(type, payload)
|
|
92
|
+
if type == :child_association_exists
|
|
93
|
+
fmt.warn "Association {group:#{payload[:parent]} <-> group:#{payload[:child]}}} already exists, skipping.\n"
|
|
81
94
|
else
|
|
82
|
-
|
|
95
|
+
fmt.warn "Group '#{payload[:name]}' does not exist and will be created.\n"
|
|
83
96
|
end
|
|
84
97
|
end
|
|
98
|
+
|
|
99
|
+
def render_addchild_existing(payload)
|
|
100
|
+
fmt.puts payload[:indent], '- already exists, skipping.'
|
|
101
|
+
end
|
|
85
102
|
end
|
|
86
103
|
end
|
|
87
104
|
end
|