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.
Files changed (169) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +2 -0
  3. data/.gitignore +2 -1
  4. data/.rubocop.yml +21 -0
  5. data/BACKLOG.md +630 -8
  6. data/Gemfile +2 -0
  7. data/Gemfile.lock +1 -1
  8. data/README.md +315 -39
  9. data/Rakefile +2 -0
  10. data/bin/moose-inventory +2 -1
  11. data/docs/architecture/architecture-and-trust-boundaries.md +444 -0
  12. data/docs/compatibility/cli-output-compatibility.md +76 -0
  13. data/docs/governance/approval-register.md +37 -0
  14. data/docs/maintenance/database-backup-restore-guidance.md +162 -0
  15. data/docs/maintenance/package-maintenance-and-agent-boundaries.md +260 -0
  16. data/docs/process/conformance-gap-analysis-2026-05-28.md +192 -0
  17. data/docs/product/product-brief.md +161 -0
  18. data/docs/product/requirements-baseline.md +477 -0
  19. data/docs/qa/qa-documentation-and-release-gates.md +283 -0
  20. data/docs/release/package-provenance-hardening.md +126 -0
  21. data/docs/release/publishing.md +11 -3
  22. data/docs/release/release-environment-protection.md +70 -0
  23. data/docs/release/release-readiness.md +23 -4
  24. data/docs/security/accepted-risk-register.md +84 -0
  25. data/docs/security/security-privacy-process.md +287 -0
  26. data/docs/security-audit-2026-05-26-rerun.md +2 -2
  27. data/docs/ux/cli-workflow-notes.md +287 -0
  28. data/examples/ansible/ansible.cfg +3 -0
  29. data/examples/ansible/inventory/moose_inventory.yml +5 -0
  30. data/examples/ansible/inventory_plugins/moose_inventory.py +100 -0
  31. data/examples/ci/README.md +16 -0
  32. data/examples/ci/github-actions/inventory-review.yml +38 -0
  33. data/examples/ci/inventory/example-snapshot.yml +19 -0
  34. data/examples/ci/scripts/validate-inventory-snapshot.sh +30 -0
  35. data/lib/moose_inventory/cli/application.rb +133 -5
  36. data/lib/moose_inventory/cli/association_rendering.rb +74 -0
  37. data/lib/moose_inventory/cli/association_rendering_support.rb +89 -0
  38. data/lib/moose_inventory/cli/audit.rb +62 -0
  39. data/lib/moose_inventory/cli/audit_recording.rb +40 -0
  40. data/lib/moose_inventory/cli/child_relation_rendering.rb +110 -0
  41. data/lib/moose_inventory/cli/console.rb +135 -0
  42. data/lib/moose_inventory/cli/db.rb +64 -0
  43. data/lib/moose_inventory/cli/factory.rb +28 -0
  44. data/lib/moose_inventory/cli/formatter.rb +8 -12
  45. data/lib/moose_inventory/cli/group.rb +5 -2
  46. data/lib/moose_inventory/cli/group_add.rb +11 -9
  47. data/lib/moose_inventory/cli/group_addchild.rb +23 -65
  48. data/lib/moose_inventory/cli/group_addhost.rb +16 -67
  49. data/lib/moose_inventory/cli/group_addvar.rb +27 -47
  50. data/lib/moose_inventory/cli/group_get.rb +8 -42
  51. data/lib/moose_inventory/cli/group_list.rb +7 -40
  52. data/lib/moose_inventory/cli/group_listvars.rb +9 -55
  53. data/lib/moose_inventory/cli/group_rm.rb +12 -10
  54. data/lib/moose_inventory/cli/group_rmchild.rb +26 -82
  55. data/lib/moose_inventory/cli/group_rmhost.rb +18 -53
  56. data/lib/moose_inventory/cli/group_rmvar.rb +30 -41
  57. data/lib/moose_inventory/cli/group_tags.rb +33 -0
  58. data/lib/moose_inventory/cli/helpers.rb +68 -1
  59. data/lib/moose_inventory/cli/host.rb +6 -3
  60. data/lib/moose_inventory/cli/host_add.rb +69 -29
  61. data/lib/moose_inventory/cli/host_addgroup.rb +22 -58
  62. data/lib/moose_inventory/cli/host_addvar.rb +28 -52
  63. data/lib/moose_inventory/cli/host_get.rb +9 -37
  64. data/lib/moose_inventory/cli/host_list.rb +24 -21
  65. data/lib/moose_inventory/cli/host_listvars.rb +9 -62
  66. data/lib/moose_inventory/cli/host_rm.rb +60 -42
  67. data/lib/moose_inventory/cli/host_rmgroup.rb +25 -44
  68. data/lib/moose_inventory/cli/host_rmvar.rb +31 -45
  69. data/lib/moose_inventory/cli/host_tags.rb +33 -0
  70. data/lib/moose_inventory/cli/listvars_support.rb +55 -0
  71. data/lib/moose_inventory/cli/plan_rendering.rb +50 -0
  72. data/lib/moose_inventory/cli/relation_transaction_support.rb +51 -0
  73. data/lib/moose_inventory/cli/tag_support.rb +97 -0
  74. data/lib/moose_inventory/cli/variable_rendering.rb +67 -0
  75. data/lib/moose_inventory/config/config.rb +185 -108
  76. data/lib/moose_inventory/db/db.rb +170 -195
  77. data/lib/moose_inventory/db/exceptions.rb +6 -3
  78. data/lib/moose_inventory/db/models.rb +16 -0
  79. data/lib/moose_inventory/db/schema_migrations.rb +248 -0
  80. data/lib/moose_inventory/inventory_context.rb +68 -2
  81. data/lib/moose_inventory/operations/add_associations.rb +20 -16
  82. data/lib/moose_inventory/operations/add_groups.rb +21 -13
  83. data/lib/moose_inventory/operations/add_hosts.rb +30 -17
  84. data/lib/moose_inventory/operations/add_variables.rb +77 -0
  85. data/lib/moose_inventory/operations/entity_variable_operation_support.rb +46 -0
  86. data/lib/moose_inventory/operations/group_child_relations.rb +23 -16
  87. data/lib/moose_inventory/operations/group_cleanup.rb +23 -8
  88. data/lib/moose_inventory/operations/import_inventory_snapshot.rb +41 -0
  89. data/lib/moose_inventory/operations/inventory_doctor.rb +172 -0
  90. data/lib/moose_inventory/operations/inventory_snapshot.rb +60 -0
  91. data/lib/moose_inventory/operations/inventory_snapshot_applier.rb +112 -0
  92. data/lib/moose_inventory/operations/inventory_snapshot_preview.rb +174 -0
  93. data/lib/moose_inventory/operations/inventory_snapshot_validator.rb +134 -0
  94. data/lib/moose_inventory/operations/operation_event_support.rb +27 -0
  95. data/lib/moose_inventory/operations/query_inventory/base_query.rb +24 -0
  96. data/lib/moose_inventory/operations/query_inventory/group_queries.rb +86 -0
  97. data/lib/moose_inventory/operations/query_inventory/host_queries.rb +106 -0
  98. data/lib/moose_inventory/operations/query_inventory.rb +47 -0
  99. data/lib/moose_inventory/operations/remove_associations.rb +30 -18
  100. data/lib/moose_inventory/operations/remove_groups.rb +12 -12
  101. data/lib/moose_inventory/operations/remove_hosts.rb +68 -0
  102. data/lib/moose_inventory/operations/remove_variables.rb +67 -0
  103. data/lib/moose_inventory/runtime_options.rb +31 -0
  104. data/lib/moose_inventory/version.rb +3 -1
  105. data/lib/moose_inventory.rb +10 -7
  106. data/moose-inventory.gemspec +19 -35
  107. data/scripts/check.sh +1 -0
  108. data/scripts/ci/check_generated_artifacts.sh +41 -0
  109. data/scripts/ci/check_permissions.sh +2 -0
  110. data/scripts/ci/check_rubocop.sh +30 -25
  111. data/scripts/files.rb +5 -4
  112. data/spec/examples/ci_examples_spec.rb +37 -0
  113. data/spec/lib/moose_inventory/ansible_plugin_examples_spec.rb +29 -0
  114. data/spec/lib/moose_inventory/cli/application_doctor_spec.rb +50 -0
  115. data/spec/lib/moose_inventory/cli/application_import_export_spec.rb +100 -0
  116. data/spec/lib/moose_inventory/cli/application_spec.rb +25 -15
  117. data/spec/lib/moose_inventory/cli/audit_spec.rb +56 -0
  118. data/spec/lib/moose_inventory/cli/cli_spec.rb +15 -19
  119. data/spec/lib/moose_inventory/cli/console_spec.rb +98 -0
  120. data/spec/lib/moose_inventory/cli/factory_spec.rb +27 -0
  121. data/spec/lib/moose_inventory/cli/formatter_spec.rb +95 -3
  122. data/spec/lib/moose_inventory/cli/group_add_spec.rb +140 -116
  123. data/spec/lib/moose_inventory/cli/group_addchild_spec.rb +89 -35
  124. data/spec/lib/moose_inventory/cli/group_addhost_spec.rb +81 -84
  125. data/spec/lib/moose_inventory/cli/group_addvar_spec.rb +65 -68
  126. data/spec/lib/moose_inventory/cli/group_get_spec.rb +17 -33
  127. data/spec/lib/moose_inventory/cli/group_list_spec.rb +16 -38
  128. data/spec/lib/moose_inventory/cli/group_listvar_spec.rb +33 -40
  129. data/spec/lib/moose_inventory/cli/group_rm_spec.rb +136 -96
  130. data/spec/lib/moose_inventory/cli/group_rmchild_spec.rb +66 -41
  131. data/spec/lib/moose_inventory/cli/group_rmhost_spec.rb +76 -78
  132. data/spec/lib/moose_inventory/cli/group_rmvar_spec.rb +57 -63
  133. data/spec/lib/moose_inventory/cli/group_spec.rb +2 -0
  134. data/spec/lib/moose_inventory/cli/helpers_spec.rb +146 -0
  135. data/spec/lib/moose_inventory/cli/host_add_spec.rb +170 -116
  136. data/spec/lib/moose_inventory/cli/host_addgroup_spec.rb +100 -83
  137. data/spec/lib/moose_inventory/cli/host_addvar_spec.rb +92 -74
  138. data/spec/lib/moose_inventory/cli/host_get_spec.rb +14 -33
  139. data/spec/lib/moose_inventory/cli/host_list_spec.rb +41 -33
  140. data/spec/lib/moose_inventory/cli/host_listvar_spec.rb +45 -53
  141. data/spec/lib/moose_inventory/cli/host_rm_spec.rb +66 -48
  142. data/spec/lib/moose_inventory/cli/host_rmgroup_spec.rb +73 -83
  143. data/spec/lib/moose_inventory/cli/host_rmvar_spec.rb +56 -63
  144. data/spec/lib/moose_inventory/cli/host_spec.rb +2 -0
  145. data/spec/lib/moose_inventory/cli/tags_spec.rb +81 -0
  146. data/spec/lib/moose_inventory/config/config_spec.rb +41 -3
  147. data/spec/lib/moose_inventory/db/db_spec.rb +396 -36
  148. data/spec/lib/moose_inventory/db/exceptions_spec.rb +18 -0
  149. data/spec/lib/moose_inventory/db/models_spec.rb +7 -3
  150. data/spec/lib/moose_inventory/db_lifecycle_spec.rb +73 -0
  151. data/spec/lib/moose_inventory/inventory_context_spec.rb +10 -0
  152. data/spec/lib/moose_inventory/operations/add_associations_spec.rb +34 -0
  153. data/spec/lib/moose_inventory/operations/add_groups_spec.rb +15 -0
  154. data/spec/lib/moose_inventory/operations/add_hosts_spec.rb +13 -0
  155. data/spec/lib/moose_inventory/operations/add_variables_spec.rb +103 -0
  156. data/spec/lib/moose_inventory/operations/group_child_relations_spec.rb +46 -0
  157. data/spec/lib/moose_inventory/operations/import_inventory_snapshot_spec.rb +226 -0
  158. data/spec/lib/moose_inventory/operations/inventory_doctor_spec.rb +77 -0
  159. data/spec/lib/moose_inventory/operations/inventory_snapshot_spec.rb +50 -0
  160. data/spec/lib/moose_inventory/operations/operation_event_support_spec.rb +78 -0
  161. data/spec/lib/moose_inventory/operations/query_inventory_spec.rb +146 -0
  162. data/spec/lib/moose_inventory/operations/remove_associations_spec.rb +35 -0
  163. data/spec/lib/moose_inventory/operations/remove_groups_spec.rb +21 -0
  164. data/spec/lib/moose_inventory/operations/remove_hosts_spec.rb +55 -0
  165. data/spec/lib/moose_inventory/operations/remove_variables_spec.rb +83 -0
  166. data/spec/shared/shared_config_setup.rb +4 -3
  167. data/spec/spec_helper.rb +50 -40
  168. data/spec/support/cli_harness.rb +33 -0
  169. metadata +80 -41
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moose
4
+ module Inventory
5
+ module Cli
6
+ # Shared rendering helpers for child-group relation commands.
7
+ module ChildRelationRendering
8
+ private
9
+
10
+ def addchild_emitter
11
+ lambda do |event|
12
+ render_addchild_event(event)
13
+ end
14
+ end
15
+
16
+ def rmchild_emitter
17
+ lambda do |event|
18
+ render_rmchild_event(event)
19
+ end
20
+ end
21
+
22
+ def render_addchild_event(event)
23
+ payload = event.payload
24
+
25
+ return render_addchild_warning(event.type, payload) if addchild_warning?(event.type)
26
+ return render_addchild_existing(payload) if event.type == :already_exists_skipping
27
+ return render_child_relation_dry_run_summary if event.type == :dry_run_summary
28
+
29
+ case event.type
30
+ when :adding_child_association
31
+ fmt.puts 2, "- add association {group:#{payload[:parent]} <-> group:#{payload[:child]}}..."
32
+ when :child_group_creating_now
33
+ fmt.puts 4, '- child group does not exist, creating now...'
34
+ when :ok
35
+ fmt.puts payload[:indent], '- OK'
36
+ end
37
+ end
38
+
39
+ def render_child_relation_dry_run_summary
40
+ puts 'Dry run complete. No changes applied.'
41
+ end
42
+
43
+ def addchild_warning?(type)
44
+ %i[child_association_exists child_group_missing].include?(type)
45
+ end
46
+
47
+ def render_addchild_warning(type, payload)
48
+ if type == :child_association_exists
49
+ fmt.warn "Association {group:#{payload[:parent]} <-> group:#{payload[:child]}} already exists, skipping.\n"
50
+ else
51
+ fmt.warn "Group '#{payload[:name]}' does not exist and will be created.\n"
52
+ end
53
+ end
54
+
55
+ def render_addchild_existing(payload)
56
+ fmt.puts payload[:indent], '- already exists, skipping.'
57
+ end
58
+
59
+ def render_rmchild_event(event)
60
+ payload = event.payload
61
+
62
+ return render_rmchild_warning(payload) if event.type == :child_association_missing
63
+ return render_rmchild_missing(payload) if event.type == :missing_skipping
64
+ return render_rmchild_progress(event.type, payload) if rmchild_progress_event?(event.type)
65
+ return render_child_relation_dry_run_summary if event.type == :dry_run_summary
66
+
67
+ render_rmchild_status(event.type, payload)
68
+ end
69
+
70
+ def rmchild_progress_event?(type)
71
+ %i[
72
+ removing_child_association
73
+ recursively_delete_orphaned_group
74
+ removing_recursive_child_association
75
+ ].include?(type)
76
+ end
77
+
78
+ def render_rmchild_progress(type, payload)
79
+ case type
80
+ when :removing_child_association
81
+ fmt.puts 2, "- remove association {group:#{payload[:parent]} <-> group:#{payload[:child]}}..."
82
+ when :recursively_delete_orphaned_group
83
+ fmt.puts 2, "- Recursively delete orphaned group '#{payload[:name]}'..."
84
+ when :removing_recursive_child_association
85
+ fmt.puts 4, "- Remove association {group:#{payload[:parent]} <-> group:#{payload[:child]}}..."
86
+ end
87
+ end
88
+
89
+ def render_rmchild_status(type, payload)
90
+ case type
91
+ when :adding_automatic_group_to_host
92
+ fmt.puts payload[:indent], "- Adding automatic association {group:ungrouped <-> host:#{payload[:host]}}..."
93
+ when :destroying_group
94
+ fmt.puts payload[:indent], "- Destroy group '#{payload[:name]}'..."
95
+ when :ok
96
+ fmt.puts payload[:indent], '- OK'
97
+ end
98
+ end
99
+
100
+ def render_rmchild_warning(payload)
101
+ fmt.warn "Association {group:#{payload[:parent]} <-> group:#{payload[:child]}} does not exist, skipping.\n"
102
+ end
103
+
104
+ def render_rmchild_missing(payload)
105
+ fmt.puts payload[:indent], "- doesn't exist, skipping."
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'shellwords'
4
+
5
+ module Moose
6
+ module Inventory
7
+ module Cli
8
+ # Small read-only interactive console for browsing inventory state.
9
+ class Console
10
+ COMMANDS = ['help', 'hosts', 'groups', 'host NAME', 'group NAME',
11
+ 'tags host NAME', 'tags group NAME', 'audit [LIMIT]', 'quit'].freeze
12
+
13
+ def initialize(context:, input: $stdin, output: $stdout)
14
+ @context = context
15
+ @input = input
16
+ @output = output
17
+ end
18
+
19
+ def run
20
+ output.puts 'Moose Inventory console (read-only). Type help or quit.'
21
+ input.each_line do |line|
22
+ parts = parse_command(line)
23
+ next if parts.nil? || parts.empty?
24
+
25
+ break if quit_command?(parts)
26
+
27
+ dispatch(parts)
28
+ end
29
+ output.puts 'Goodbye.'
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :context, :input, :output
35
+
36
+ def parse_command(line)
37
+ command = line.strip
38
+ return [] if command.empty?
39
+
40
+ Shellwords.split(command)
41
+ rescue ArgumentError => e
42
+ output.puts "Invalid command syntax: #{e.message}"
43
+ nil
44
+ end
45
+
46
+ def dispatch(parts)
47
+ handlers = {
48
+ 'help' => -> { render_exact(parts, 'help') { render_help } },
49
+ 'hosts' => -> { render_exact(parts, 'hosts') { render_hosts } },
50
+ 'groups' => -> { render_exact(parts, 'groups') { render_groups } },
51
+ 'host' => -> { render_entity(:host, parts) },
52
+ 'group' => -> { render_entity(:group, parts) },
53
+ 'tags' => -> { render_tags(parts) },
54
+ 'audit' => -> { render_audit(parts) }
55
+ }
56
+ handlers.fetch(parts.first, -> { output.puts "Unknown command: #{parts.join(' ')}" }).call
57
+ end
58
+
59
+ def quit_command?(parts)
60
+ parts.length == 1 && %w[quit exit].include?(parts.first)
61
+ end
62
+
63
+ def render_exact(parts, usage)
64
+ return output.puts("Usage: #{usage}") unless parts.length == 1
65
+
66
+ yield
67
+ end
68
+
69
+ def render_help
70
+ output.puts 'Commands:'
71
+ COMMANDS.each { |command| output.puts "- #{command}" }
72
+ end
73
+
74
+ def render_hosts
75
+ names = context.all_hosts.map(&:name).sort
76
+ output.puts(names.empty? ? 'No hosts.' : "Hosts: #{names.join(', ')}")
77
+ end
78
+
79
+ def render_groups
80
+ names = context.all_groups.map(&:name).sort
81
+ output.puts(names.empty? ? 'No groups.' : "Groups: #{names.join(', ')}")
82
+ end
83
+
84
+ def render_entity(type, parts)
85
+ return output.puts("Usage: #{type} NAME") unless parts.length == 2
86
+
87
+ name = parts[1]
88
+
89
+ entity = context.public_send("find_#{type}", name)
90
+ return output.puts("#{type.capitalize} '#{name}' not found.") if entity.nil?
91
+
92
+ output.puts "#{type.capitalize}: #{name}"
93
+ output.puts "Groups: #{entity.groups_dataset.map(:name).sort.join(', ')}" if type == :host
94
+ output.puts "Hosts: #{entity.hosts_dataset.map(:name).sort.join(', ')}" if type == :group
95
+ output.puts "Tags: #{entity.tags_dataset.map(:name).sort.join(', ')}"
96
+ end
97
+
98
+ def render_tags(parts)
99
+ type = parts[1]
100
+ name = parts[2]
101
+ return output.puts('Usage: tags host|group NAME') unless parts.length == 3 && %w[host group].include?(type)
102
+
103
+ entity = context.public_send("find_#{type}", name)
104
+ return output.puts("#{type.capitalize} '#{name}' not found.") if entity.nil?
105
+
106
+ tags = entity.tags_dataset.map(:name).sort
107
+ output.puts tags.empty? ? "#{type.capitalize} '#{name}' has no tags." : tags.join(', ')
108
+ end
109
+
110
+ def render_audit(parts)
111
+ limit = audit_limit(parts[1]) if parts.length <= 2
112
+ return output.puts('Usage: audit [LIMIT]') if parts.length > 2 || limit.nil?
113
+
114
+ events = context.audit_events(limit: limit)
115
+ return output.puts('No audit events recorded.') if events.empty?
116
+
117
+ events.each do |event|
118
+ output.puts "#{event.id} #{event.created_at} #{event.command} #{event.entity_type}=#{event.entity_name}"
119
+ end
120
+ end
121
+
122
+ def audit_limit(value)
123
+ return 10 if value.nil?
124
+
125
+ parsed = Integer(value)
126
+ return parsed if parsed.positive?
127
+
128
+ nil
129
+ rescue ArgumentError
130
+ nil
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'json'
5
+
6
+ require_relative 'formatter'
7
+
8
+ module Moose
9
+ module Inventory
10
+ module Cli
11
+ # Database lifecycle commands.
12
+ class Db < Thor
13
+ desc 'status', 'Show database lifecycle status'
14
+ def status
15
+ render_status(Moose::Inventory::DB.status)
16
+ end
17
+
18
+ desc 'doctor', 'Check database schema state'
19
+ def doctor
20
+ status = Moose::Inventory::DB.status
21
+ missing = status[:tables].reject { |_name, present| present }.keys
22
+ if missing.empty? && status[:schema_version] == status[:expected_schema_version]
23
+ puts 'Database doctor found no issues.'
24
+ return
25
+ end
26
+
27
+ puts 'Database doctor found issue(s):'
28
+ puts "- Missing tables: #{missing.join(', ')}" unless missing.empty?
29
+ if status[:schema_version] != status[:expected_schema_version]
30
+ puts "- Schema version is #{status[:schema_version].inspect}; expected #{status[:expected_schema_version]}."
31
+ end
32
+ exit(1)
33
+ end
34
+
35
+ desc 'migrate', 'Create missing schema tables and record current schema version'
36
+ def migrate
37
+ status = Moose::Inventory::DB.migrate!
38
+ puts "Database schema is at version #{status[:schema_version]}."
39
+ end
40
+
41
+ desc 'backup FILE', 'Back up the configured sqlite3 database file'
42
+ def backup(file)
43
+ destination = Moose::Inventory::DB.backup(file)
44
+ puts "Backed up database to #{destination}."
45
+ rescue Moose::Inventory::DB.exceptions[:moose] => e
46
+ abort("ERROR: #{e.message}")
47
+ end
48
+
49
+ private
50
+
51
+ def render_status(status)
52
+ puts "Adapter: #{status[:adapter]}"
53
+ puts "Schema version: #{status[:schema_version] || 'unknown'}"
54
+ puts "Expected schema version: #{status[:expected_schema_version]}"
55
+ puts "SQLite file: #{status[:sqlite_file]}" unless status[:sqlite_file].nil?
56
+ puts 'Tables:'
57
+ status[:tables].each do |name, present|
58
+ puts "- #{name}: #{present ? 'present' : 'missing'}"
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../operations/query_inventory'
4
+
5
+ module Moose
6
+ module Inventory
7
+ module Cli
8
+ # Small factory for command-side operations and query wrappers.
9
+ class Factory
10
+ def initialize(context:)
11
+ @context = context
12
+ end
13
+
14
+ def operation(operation_class, **)
15
+ operation_class.new(context: context, **)
16
+ end
17
+
18
+ def query_inventory
19
+ @query_inventory ||= Moose::Inventory::Operations::QueryInventory.new(context: context)
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :context
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
  require 'yaml'
3
5
  require 'indentation'
@@ -5,24 +7,18 @@ require 'indentation'
5
7
  module Moose
6
8
  module Inventory
7
9
  module Cli
8
- ##
9
- # TODO: Documentation
10
10
  module Formatter
11
11
  # rubocop:disable Style/ModuleFunction
12
12
  extend self
13
13
  # rubocop:enable Style/ModuleFunction
14
14
 
15
- def self.dump(arg, format = nil)
15
+ def self.dump(arg, format = 'json')
16
16
  out(arg, format)
17
17
  end
18
18
 
19
- def self.out(arg, format = nil)
19
+ def self.out(arg, format = 'json')
20
20
  return if arg.nil?
21
21
 
22
- if format.nil?
23
- format = Moose::Inventory::Config._confopts[:format].downcase
24
- end
25
-
26
22
  case format
27
23
  when 'yaml', 'y'
28
24
  $stdout.puts arg.to_yaml
@@ -50,7 +46,7 @@ module Moose
50
46
  when 'STDOUT'
51
47
  $stdout.puts msg.indent(indent)
52
48
  when 'STDERR'
53
- $stderr.puts msg.indent(indent)
49
+ $stderr.print("#{msg.indent(indent)}\n")
54
50
  else
55
51
  abort("Output stream '#{stream}' is not known.")
56
52
  end
@@ -67,12 +63,12 @@ module Moose
67
63
  end
68
64
  end
69
65
 
70
- def info(_indent, _msg, stream = 'STDOUT')
66
+ def info(indent, msg, stream = 'STDOUT')
71
67
  case stream
72
68
  when 'STDOUT'
73
- $stdout.print 'INFO: {msg}'
69
+ $stdout.print "INFO: #{msg}".indent(indent)
74
70
  when 'STDERR'
75
- $stderr.print 'INFO: {msg}'
71
+ $stderr.print "INFO: #{msg}".indent(indent)
76
72
  else
77
73
  abort("Output stream '#{stream}' is not known.")
78
74
  end
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thor'
2
- require_relative './formatter.rb'
3
- require_relative './helpers.rb'
4
+ require_relative 'formatter'
5
+ require_relative 'helpers'
4
6
 
5
7
  module Moose
6
8
  module Inventory
@@ -21,6 +23,7 @@ module Moose
21
23
  require_relative 'group_addvar'
22
24
  require_relative 'group_listvars'
23
25
  require_relative 'group_rmvar'
26
+ require_relative 'group_tags'
24
27
  end
25
28
  end
26
29
  end
@@ -14,8 +14,11 @@ module Moose
14
14
  #==========================
15
15
  desc 'add NAME', 'Add a group NAME to the inventory'
16
16
  option :hosts
17
+ option :dry_run, type: :boolean
18
+ option :plan_format, type: :string, desc: 'Emit dry-run plan events as yaml|json|pjson'
17
19
  def add(*argv)
18
20
  abort_if_missing_args(argv, 1, '1 or more')
21
+ validate_machine_plan_request!
19
22
 
20
23
  names = normalize_names(argv)
21
24
  hosts = csv_option_names(options[:hosts])
@@ -25,16 +28,14 @@ module Moose
25
28
  "ERROR: Cannot manually manipulate the automatic group 'ungrouped'\n"
26
29
  )
27
30
 
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)
31
+ result = build_operation(Moose::Inventory::Operations::AddGroups)
32
+ .call(names: names, hosts: hosts, dry_run: options[:dry_run])
33
+ return if machine_plan_output_rendered?(result, command: 'group add')
32
34
 
33
- if result.warning_count.zero?
34
- puts 'Succeeded'
35
- else
36
- puts 'Succeeded, with warnings.'
37
- end
35
+ record_audit({ command: 'group add', action: 'add', entity_type: 'group',
36
+ entity_names: names }, result: result, dry_run: options[:dry_run])
37
+ render_add_groups_events(result.events)
38
+ print_warning_summary(result, success_message: 'Succeeded')
38
39
  end
39
40
 
40
41
  private
@@ -48,6 +49,7 @@ module Moose
48
49
 
49
50
  return render_add_groups_event_puts(event.type, payload) if puts_event?(event.type)
50
51
  return render_add_groups_event_warn(event.type, payload) if warn_event?(event.type)
52
+ return puts 'Dry run complete. No changes applied.' if event.type == :dry_run_summary
51
53
 
52
54
  render_add_groups_event_fmt(event.type, payload)
53
55
  end
@@ -14,8 +14,11 @@ module Moose
14
14
  #==========================
15
15
  desc 'addchild PARENTGROUP CHILDGROUP_1 [CHILDGROUP_2 ... ]',
16
16
  'Associate one or more child-groups CHILDGROUP_n with PARENTGROUP'
17
+ option :dry_run, type: :boolean
18
+ option :plan_format, type: :string, desc: 'Emit dry-run plan events as yaml|json|pjson'
17
19
  def addchild(*argv)
18
20
  abort_if_missing_args(argv, 2, '2 or more')
21
+ validate_machine_plan_request!
19
22
 
20
23
  pname = argv[0].downcase
21
24
  cnames = normalize_names(argv.slice(1, argv.length - 1))
@@ -23,81 +26,36 @@ module Moose
23
26
  abort_if_automatic_group([pname] + cnames)
24
27
 
25
28
  result = add_children_to_group(pname, cnames)
29
+ return if machine_plan_output_rendered?(result, command: 'group addchild')
26
30
 
27
- if result.warning_count.zero?
28
- puts 'Succeeded.'
29
- else
30
- puts 'Succeeded, with warnings.'
31
- end
31
+ record_audit({ command: 'group addchild', action: 'associate_child', entity_type: 'group',
32
+ entity_names: pname }, result: result, dry_run: options[:dry_run])
33
+ print_warning_summary(result)
32
34
  end
33
35
 
34
36
  private
35
37
 
36
38
  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)
39
-
40
- begin
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)
50
- fmt.puts 2, '- all OK'
51
- return result
52
- end
53
- rescue db.exceptions[:moose] => e
54
- abort("ERROR: #{e}")
39
+ operation = build_operation(Moose::Inventory::Operations::GroupChildRelations)
40
+ run_group_relation_transaction(
41
+ heading: "Associate parent group '#{parent_name}' with child group(s) '#{child_names.join(',')}':",
42
+ on_error: method(:exception_to_s)
43
+ ) do
44
+ parent_group = fetch_existing_group_or_abort(parent_name)
45
+ result = operation.add_children(
46
+ parent_group: parent_group,
47
+ parent_name: parent_name,
48
+ child_names: child_names,
49
+ dry_run: options[:dry_run]
50
+ )
51
+ render_addchild_events(result.events) unless machine_plan_output_requested?
52
+ result
55
53
  end
56
54
  end
57
55
 
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
56
  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"
94
- else
95
- fmt.warn "Group '#{payload[:name]}' does not exist and will be created.\n"
96
- end
97
- end
98
-
99
- def render_addchild_existing(payload)
100
- fmt.puts payload[:indent], '- already exists, skipping.'
57
+ emitter = addchild_emitter
58
+ events.each { |event| emitter.call(event) }
101
59
  end
102
60
  end
103
61
  end
@@ -14,8 +14,11 @@ module Moose
14
14
  #==========================
15
15
  desc 'addhost NAME HOSTNAME',
16
16
  'Associate a host HOSTNAME with the group NAME'
17
+ option :dry_run, type: :boolean
18
+ option :plan_format, type: :string, desc: 'Emit dry-run plan events as yaml|json|pjson'
17
19
  def addhost(*args)
18
20
  abort_if_missing_args(args, 2, '2 or more')
21
+ validate_machine_plan_request!
19
22
 
20
23
  name = args[0].downcase
21
24
  hosts = normalize_names(args.slice(1, args.length - 1))
@@ -23,83 +26,29 @@ module Moose
23
26
  abort_if_automatic_group([name])
24
27
 
25
28
  result = add_hosts_to_group(name, hosts)
29
+ return if machine_plan_output_rendered?(result, command: 'group addhost')
26
30
 
27
- if result.warning_count.zero?
28
- puts 'Succeeded.'
29
- else
30
- puts 'Succeeded, with warnings.'
31
- end
31
+ record_audit({ command: 'group addhost', action: 'associate', entity_type: 'group',
32
+ entity_names: name }, result: result, dry_run: options[:dry_run])
33
+ print_warning_summary(result)
32
34
  end
33
35
 
34
36
  private
35
37
 
36
38
  def add_hosts_to_group(name, hosts)
37
- context = Moose::Inventory::InventoryContext.new(db: db)
38
- operation = Moose::Inventory::Operations::AddAssociations.new(context: context)
39
-
40
- begin
41
- db.transaction do
42
- puts "Associate group '#{name}' with host(s) '#{hosts.join(',')}':"
43
- group = fetch_existing_group_for_addhost(context, name)
44
- result = operation.group_to_hosts(group: group, group_name: name, host_names: hosts)
45
- render_group_addhost_events(result.events)
46
- fmt.puts 2, '- all OK'
47
- return result
48
- end
49
- rescue db.exceptions[:moose] => e
50
- abort("ERROR: #{e.message}")
39
+ operation = build_operation(Moose::Inventory::Operations::AddAssociations)
40
+ run_group_relation_transaction(heading: "Associate group '#{name}' with host(s) '#{hosts.join(',')}':") do
41
+ group = fetch_existing_group_or_abort(name)
42
+ result = operation.group_to_hosts(group: group, group_name: name, host_names: hosts,
43
+ dry_run: options[:dry_run])
44
+ render_group_addhost_events(result.events) unless machine_plan_output_requested?
45
+ result
51
46
  end
52
47
  end
53
48
 
54
- def fetch_existing_group_for_addhost(context, name)
55
- fmt.puts 2, "- retrieve group '#{name}'..."
56
- group = context.find_group(name)
57
- abort("ERROR: The group '#{name}' does not exist.") if group.nil?
58
-
59
- fmt.puts 4, '- OK'
60
- group
61
- end
62
-
63
49
  def render_group_addhost_events(events)
64
- events.each { |event| render_group_addhost_event(event) }
65
- end
66
-
67
- def render_group_addhost_event(event)
68
- payload = event.payload
69
-
70
- return render_group_addhost_warning(event.type, payload) if group_addhost_warning?(event.type)
71
- return render_group_addhost_status(payload) if event.type == :already_exists_skipping
72
-
73
- render_group_addhost_output(event.type, payload)
74
- end
75
-
76
- def group_addhost_warning?(type)
77
- %i[group_host_association_exists host_missing_created].include?(type)
78
- end
79
-
80
- def render_group_addhost_warning(type, payload)
81
- if type == :group_host_association_exists
82
- fmt.warn "Association {group:#{payload[:group]} <-> host:#{payload[:host]}} already exists, skipping.\n"
83
- else
84
- fmt.warn "Host '#{payload[:name]}' does not exist and will be created.\n"
85
- end
86
- end
87
-
88
- def render_group_addhost_status(payload)
89
- fmt.puts payload[:indent], '- already exists, skipping.'
90
- end
91
-
92
- def render_group_addhost_output(type, payload)
93
- case type
94
- when :adding_group_host_association
95
- fmt.puts 2, "- add association {group:#{payload[:group]} <-> host:#{payload[:host]}}..."
96
- when :host_creating_now
97
- fmt.puts 4, '- host does not exist, creating now...'
98
- when :removing_automatic_group
99
- fmt.puts 2, "- remove automatic association {group:ungrouped <-> host:#{payload[:host]}}..."
100
- when :ok
101
- fmt.puts payload[:indent], '- OK'
102
- end
50
+ emitter = host_group_association_addition_emitter(perspective: :group)
51
+ events.each { |event| emitter.call(event) }
103
52
  end
104
53
  end
105
54
  end