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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +15 -1
  3. data/.github/workflows/release.yml +58 -0
  4. data/.gitleaks.toml +9 -0
  5. data/.rubocop.yml +28 -0
  6. data/BACKLOG.md +130 -24
  7. data/Gemfile.lock +36 -1
  8. data/README.md +26 -6
  9. data/Rakefile +1 -1
  10. data/docs/release/publishing.md +44 -48
  11. data/docs/release/release-readiness.md +14 -0
  12. data/docs/security-audit-2026-05-26-rerun.md +75 -0
  13. data/docs/security-audit-2026-05-26.md +63 -0
  14. data/lib/moose_inventory/cli/group.rb +3 -0
  15. data/lib/moose_inventory/cli/group_add.rb +89 -73
  16. data/lib/moose_inventory/cli/group_addchild.rb +77 -60
  17. data/lib/moose_inventory/cli/group_addhost.rb +78 -65
  18. data/lib/moose_inventory/cli/group_rm.rb +101 -71
  19. data/lib/moose_inventory/cli/group_rmchild.rb +99 -53
  20. data/lib/moose_inventory/cli/group_rmhost.rb +64 -56
  21. data/lib/moose_inventory/cli/helpers.rb +76 -0
  22. data/lib/moose_inventory/cli/host.rb +3 -0
  23. data/lib/moose_inventory/cli/host_add.rb +47 -62
  24. data/lib/moose_inventory/cli/host_addgroup.rb +73 -64
  25. data/lib/moose_inventory/cli/host_rmgroup.rb +58 -55
  26. data/lib/moose_inventory/db/db.rb +27 -7
  27. data/lib/moose_inventory/inventory_context.rb +50 -0
  28. data/lib/moose_inventory/operations/add_associations.rb +127 -0
  29. data/lib/moose_inventory/operations/add_groups.rb +115 -0
  30. data/lib/moose_inventory/operations/add_hosts.rb +110 -0
  31. data/lib/moose_inventory/operations/group_child_relations.rb +118 -0
  32. data/lib/moose_inventory/operations/group_cleanup.rb +55 -0
  33. data/lib/moose_inventory/operations/remove_associations.rb +101 -0
  34. data/lib/moose_inventory/operations/remove_groups.rb +79 -0
  35. data/lib/moose_inventory/version.rb +1 -1
  36. data/moose-inventory.gemspec +3 -0
  37. data/scripts/check.sh +2 -0
  38. data/scripts/ci/check_permissions.sh +3 -0
  39. data/scripts/ci/check_rubocop.sh +28 -0
  40. data/scripts/ci/check_secrets.sh +26 -0
  41. data/scripts/ci/check_security.sh +18 -0
  42. data/scripts/ci/install_security_tools.sh +47 -0
  43. data/scripts/install_dependencies.sh +2 -0
  44. data/spec/lib/moose_inventory/cli/group_rm_spec.rb +40 -0
  45. data/spec/lib/moose_inventory/cli/group_rmchild_spec.rb +45 -0
  46. data/spec/lib/moose_inventory/db/db_spec.rb +162 -0
  47. data/spec/lib/moose_inventory/operations/add_associations_spec.rb +77 -0
  48. data/spec/lib/moose_inventory/operations/add_groups_spec.rb +65 -0
  49. data/spec/lib/moose_inventory/operations/add_hosts_spec.rb +69 -0
  50. data/spec/lib/moose_inventory/operations/group_child_relations_spec.rb +76 -0
  51. data/spec/lib/moose_inventory/operations/remove_associations_spec.rb +78 -0
  52. data/spec/lib/moose_inventory/operations/remove_groups_spec.rb +57 -0
  53. 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 './formatter.rb'
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
- # rubocop:disable Metrics/LineLength
14
- def add(*argv) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
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
- # Arguments
21
- names = argv.uniq.map(&:downcase)
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
- # sanity
26
- if names.include?('ungrouped')
27
- abort("ERROR: Cannot manually manipulate the automatic group 'ungrouped'\n")
28
- end
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
- # Convenience
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 './formatter.rb'
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(*_argv)
14
- # Sanity check
15
- if args.length < 2
16
- abort("ERROR: Wrong number of arguments, #{args.length} "\
17
- 'for 2 or more.')
18
- end
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
- # Arguments
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
- # Sanity
25
- if pname == 'ungrouped' || cnames.include?('ungrouped')
26
- abort("ERROR: Cannot manually manipulate the automatic group 'ungrouped'.")
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
- # Convenience
30
- db = Moose::Inventory::DB
31
- fmt = Moose::Inventory::Cli::Formatter
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 # Transaction start
37
- puts "Associate parent group '#{pname}' with child group(s) '#{cnames.join(',')}':"
38
- # Get the target group
39
- fmt.puts 2, "- retrieve group '#{pname}'..."
40
- pgroup = db.models[:group].find(name: pname)
41
- if pgroup.nil?
42
- abort("ERROR: The group '#{pname}' does not exist.")
43
- end
44
- fmt.puts 4, '- OK'
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
- end # Transaction end
51
+ return result
52
+ end
76
53
  rescue db.exceptions[:moose] => e
77
54
  abort("ERROR: #{e}")
78
55
  end
79
- if warn_count == 0
80
- puts 'Succeeded.'
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
- puts 'Succeeded, with warnings.'
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