moose-inventory 2.0 → 2.1.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 (171) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +2 -0
  3. data/.gitignore +6 -1
  4. data/.rubocop.yml +21 -0
  5. data/BACKLOG.md +638 -9
  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 +78 -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/security-audit-2026-05-29-snapshot-import-fuzz.md +58 -0
  28. data/docs/ux/cli-workflow-notes.md +287 -0
  29. data/examples/ansible/ansible.cfg +3 -0
  30. data/examples/ansible/inventory/moose_inventory.yml +5 -0
  31. data/examples/ansible/inventory_plugins/moose_inventory.py +100 -0
  32. data/examples/ci/README.md +16 -0
  33. data/examples/ci/github-actions/inventory-review.yml +38 -0
  34. data/examples/ci/inventory/example-snapshot.yml +19 -0
  35. data/examples/ci/scripts/validate-inventory-snapshot.sh +30 -0
  36. data/lib/moose_inventory/cli/application.rb +135 -5
  37. data/lib/moose_inventory/cli/association_rendering.rb +74 -0
  38. data/lib/moose_inventory/cli/association_rendering_support.rb +89 -0
  39. data/lib/moose_inventory/cli/audit.rb +62 -0
  40. data/lib/moose_inventory/cli/audit_recording.rb +40 -0
  41. data/lib/moose_inventory/cli/child_relation_rendering.rb +110 -0
  42. data/lib/moose_inventory/cli/console.rb +135 -0
  43. data/lib/moose_inventory/cli/db.rb +64 -0
  44. data/lib/moose_inventory/cli/factory.rb +28 -0
  45. data/lib/moose_inventory/cli/formatter.rb +8 -12
  46. data/lib/moose_inventory/cli/group.rb +5 -2
  47. data/lib/moose_inventory/cli/group_add.rb +11 -9
  48. data/lib/moose_inventory/cli/group_addchild.rb +23 -65
  49. data/lib/moose_inventory/cli/group_addhost.rb +16 -67
  50. data/lib/moose_inventory/cli/group_addvar.rb +27 -47
  51. data/lib/moose_inventory/cli/group_get.rb +8 -42
  52. data/lib/moose_inventory/cli/group_list.rb +7 -40
  53. data/lib/moose_inventory/cli/group_listvars.rb +9 -55
  54. data/lib/moose_inventory/cli/group_rm.rb +12 -10
  55. data/lib/moose_inventory/cli/group_rmchild.rb +26 -82
  56. data/lib/moose_inventory/cli/group_rmhost.rb +18 -53
  57. data/lib/moose_inventory/cli/group_rmvar.rb +30 -41
  58. data/lib/moose_inventory/cli/group_tags.rb +33 -0
  59. data/lib/moose_inventory/cli/helpers.rb +68 -1
  60. data/lib/moose_inventory/cli/host.rb +6 -3
  61. data/lib/moose_inventory/cli/host_add.rb +69 -29
  62. data/lib/moose_inventory/cli/host_addgroup.rb +22 -58
  63. data/lib/moose_inventory/cli/host_addvar.rb +28 -52
  64. data/lib/moose_inventory/cli/host_get.rb +9 -37
  65. data/lib/moose_inventory/cli/host_list.rb +24 -21
  66. data/lib/moose_inventory/cli/host_listvars.rb +9 -62
  67. data/lib/moose_inventory/cli/host_rm.rb +60 -42
  68. data/lib/moose_inventory/cli/host_rmgroup.rb +25 -44
  69. data/lib/moose_inventory/cli/host_rmvar.rb +31 -45
  70. data/lib/moose_inventory/cli/host_tags.rb +33 -0
  71. data/lib/moose_inventory/cli/listvars_support.rb +55 -0
  72. data/lib/moose_inventory/cli/plan_rendering.rb +50 -0
  73. data/lib/moose_inventory/cli/relation_transaction_support.rb +51 -0
  74. data/lib/moose_inventory/cli/tag_support.rb +97 -0
  75. data/lib/moose_inventory/cli/variable_rendering.rb +67 -0
  76. data/lib/moose_inventory/config/config.rb +185 -108
  77. data/lib/moose_inventory/db/db.rb +170 -195
  78. data/lib/moose_inventory/db/exceptions.rb +6 -3
  79. data/lib/moose_inventory/db/models.rb +16 -0
  80. data/lib/moose_inventory/db/schema_migrations.rb +248 -0
  81. data/lib/moose_inventory/inventory_context.rb +68 -2
  82. data/lib/moose_inventory/operations/add_associations.rb +20 -16
  83. data/lib/moose_inventory/operations/add_groups.rb +21 -13
  84. data/lib/moose_inventory/operations/add_hosts.rb +30 -17
  85. data/lib/moose_inventory/operations/add_variables.rb +77 -0
  86. data/lib/moose_inventory/operations/entity_variable_operation_support.rb +46 -0
  87. data/lib/moose_inventory/operations/group_child_relations.rb +23 -16
  88. data/lib/moose_inventory/operations/group_cleanup.rb +23 -8
  89. data/lib/moose_inventory/operations/import_inventory_snapshot.rb +41 -0
  90. data/lib/moose_inventory/operations/inventory_doctor.rb +172 -0
  91. data/lib/moose_inventory/operations/inventory_snapshot.rb +60 -0
  92. data/lib/moose_inventory/operations/inventory_snapshot_applier.rb +112 -0
  93. data/lib/moose_inventory/operations/inventory_snapshot_preview.rb +174 -0
  94. data/lib/moose_inventory/operations/inventory_snapshot_validator.rb +174 -0
  95. data/lib/moose_inventory/operations/operation_event_support.rb +27 -0
  96. data/lib/moose_inventory/operations/query_inventory/base_query.rb +24 -0
  97. data/lib/moose_inventory/operations/query_inventory/group_queries.rb +86 -0
  98. data/lib/moose_inventory/operations/query_inventory/host_queries.rb +106 -0
  99. data/lib/moose_inventory/operations/query_inventory.rb +47 -0
  100. data/lib/moose_inventory/operations/remove_associations.rb +30 -18
  101. data/lib/moose_inventory/operations/remove_groups.rb +12 -12
  102. data/lib/moose_inventory/operations/remove_hosts.rb +68 -0
  103. data/lib/moose_inventory/operations/remove_variables.rb +67 -0
  104. data/lib/moose_inventory/runtime_options.rb +31 -0
  105. data/lib/moose_inventory/version.rb +3 -1
  106. data/lib/moose_inventory.rb +10 -7
  107. data/moose-inventory.gemspec +19 -35
  108. data/scripts/check.sh +1 -0
  109. data/scripts/ci/check_generated_artifacts.sh +41 -0
  110. data/scripts/ci/check_permissions.sh +2 -0
  111. data/scripts/ci/check_rubocop.sh +30 -25
  112. data/scripts/ci/check_security.sh +4 -1
  113. data/scripts/files.rb +5 -4
  114. data/spec/examples/ci_examples_spec.rb +37 -0
  115. data/spec/lib/moose_inventory/ansible_plugin_examples_spec.rb +29 -0
  116. data/spec/lib/moose_inventory/cli/application_doctor_spec.rb +50 -0
  117. data/spec/lib/moose_inventory/cli/application_import_export_spec.rb +132 -0
  118. data/spec/lib/moose_inventory/cli/application_spec.rb +25 -15
  119. data/spec/lib/moose_inventory/cli/audit_spec.rb +56 -0
  120. data/spec/lib/moose_inventory/cli/cli_spec.rb +15 -19
  121. data/spec/lib/moose_inventory/cli/console_spec.rb +98 -0
  122. data/spec/lib/moose_inventory/cli/factory_spec.rb +27 -0
  123. data/spec/lib/moose_inventory/cli/formatter_spec.rb +95 -3
  124. data/spec/lib/moose_inventory/cli/group_add_spec.rb +140 -116
  125. data/spec/lib/moose_inventory/cli/group_addchild_spec.rb +89 -35
  126. data/spec/lib/moose_inventory/cli/group_addhost_spec.rb +81 -84
  127. data/spec/lib/moose_inventory/cli/group_addvar_spec.rb +65 -68
  128. data/spec/lib/moose_inventory/cli/group_get_spec.rb +17 -33
  129. data/spec/lib/moose_inventory/cli/group_list_spec.rb +16 -38
  130. data/spec/lib/moose_inventory/cli/group_listvar_spec.rb +33 -40
  131. data/spec/lib/moose_inventory/cli/group_rm_spec.rb +136 -96
  132. data/spec/lib/moose_inventory/cli/group_rmchild_spec.rb +66 -41
  133. data/spec/lib/moose_inventory/cli/group_rmhost_spec.rb +76 -78
  134. data/spec/lib/moose_inventory/cli/group_rmvar_spec.rb +57 -63
  135. data/spec/lib/moose_inventory/cli/group_spec.rb +2 -0
  136. data/spec/lib/moose_inventory/cli/helpers_spec.rb +146 -0
  137. data/spec/lib/moose_inventory/cli/host_add_spec.rb +170 -116
  138. data/spec/lib/moose_inventory/cli/host_addgroup_spec.rb +100 -83
  139. data/spec/lib/moose_inventory/cli/host_addvar_spec.rb +92 -74
  140. data/spec/lib/moose_inventory/cli/host_get_spec.rb +14 -33
  141. data/spec/lib/moose_inventory/cli/host_list_spec.rb +41 -33
  142. data/spec/lib/moose_inventory/cli/host_listvar_spec.rb +45 -53
  143. data/spec/lib/moose_inventory/cli/host_rm_spec.rb +66 -48
  144. data/spec/lib/moose_inventory/cli/host_rmgroup_spec.rb +73 -83
  145. data/spec/lib/moose_inventory/cli/host_rmvar_spec.rb +56 -63
  146. data/spec/lib/moose_inventory/cli/host_spec.rb +2 -0
  147. data/spec/lib/moose_inventory/cli/tags_spec.rb +81 -0
  148. data/spec/lib/moose_inventory/config/config_spec.rb +41 -3
  149. data/spec/lib/moose_inventory/db/db_spec.rb +396 -36
  150. data/spec/lib/moose_inventory/db/exceptions_spec.rb +18 -0
  151. data/spec/lib/moose_inventory/db/models_spec.rb +7 -3
  152. data/spec/lib/moose_inventory/db_lifecycle_spec.rb +73 -0
  153. data/spec/lib/moose_inventory/inventory_context_spec.rb +10 -0
  154. data/spec/lib/moose_inventory/operations/add_associations_spec.rb +34 -0
  155. data/spec/lib/moose_inventory/operations/add_groups_spec.rb +15 -0
  156. data/spec/lib/moose_inventory/operations/add_hosts_spec.rb +13 -0
  157. data/spec/lib/moose_inventory/operations/add_variables_spec.rb +103 -0
  158. data/spec/lib/moose_inventory/operations/group_child_relations_spec.rb +46 -0
  159. data/spec/lib/moose_inventory/operations/import_inventory_snapshot_spec.rb +239 -0
  160. data/spec/lib/moose_inventory/operations/inventory_doctor_spec.rb +77 -0
  161. data/spec/lib/moose_inventory/operations/inventory_snapshot_spec.rb +50 -0
  162. data/spec/lib/moose_inventory/operations/operation_event_support_spec.rb +78 -0
  163. data/spec/lib/moose_inventory/operations/query_inventory_spec.rb +146 -0
  164. data/spec/lib/moose_inventory/operations/remove_associations_spec.rb +35 -0
  165. data/spec/lib/moose_inventory/operations/remove_groups_spec.rb +21 -0
  166. data/spec/lib/moose_inventory/operations/remove_hosts_spec.rb +55 -0
  167. data/spec/lib/moose_inventory/operations/remove_variables_spec.rb +83 -0
  168. data/spec/shared/shared_config_setup.rb +4 -3
  169. data/spec/spec_helper.rb +50 -40
  170. data/spec/support/cli_harness.rb +33 -0
  171. metadata +81 -41
@@ -19,107 +19,51 @@ module Moose
19
19
  desc: 'Delete child groups that become orphaned'
20
20
  desc 'rmchild PARENTGROUP CHILDGROUP_1 [CHILDGROUP_2 ... ]',
21
21
  'Dissociate one or more child-groups CHILDGROUP_n from PARENTGROUP'
22
+ option :dry_run, type: :boolean
23
+ option :yes, type: :boolean, desc: 'Confirm destructive dissociation without prompting'
24
+ option :plan_format, type: :string, desc: 'Emit dry-run plan events as yaml|json|pjson'
22
25
  def rmchild(*argv)
23
26
  abort_if_missing_args(argv, 2, '2 or more')
27
+ validate_machine_plan_request!
24
28
 
25
29
  pname = argv[0].downcase
26
30
  cnames = normalize_names(argv.slice(1, argv.length - 1))
27
31
 
28
32
  abort_if_automatic_group([pname] + cnames)
33
+ confirm_destructive_action!("group rmchild #{pname} #{cnames.join(',')}")
29
34
 
30
35
  result = remove_children_from_group(pname, cnames)
36
+ return if machine_plan_output_rendered?(result, command: 'group rmchild')
31
37
 
32
- if result.warning_count.zero?
33
- puts 'Succeeded.'
34
- else
35
- puts 'Succeeded, with warnings.'
36
- end
38
+ record_audit({ command: 'group rmchild', action: 'dissociate_child', entity_type: 'group',
39
+ entity_names: pname }, result: result, dry_run: options[:dry_run])
40
+ print_warning_summary(result)
37
41
  end
38
42
 
39
43
  private
40
44
 
41
45
  def remove_children_from_group(parent_name, child_names)
42
- context = Moose::Inventory::InventoryContext.new(db: db)
43
- operation = Moose::Inventory::Operations::GroupChildRelations.new(context: context)
44
-
45
- begin
46
- db.transaction do
47
- puts "Dissociate parent group '#{parent_name}' from child group(s) '#{child_names.join(',')}':"
48
- parent_group = fetch_existing_group_for_rmchild(context, parent_name)
49
- result = operation.remove_children(
50
- parent_group: parent_group,
51
- parent_name: parent_name,
52
- child_names: child_names,
53
- delete_orphans: options[:delete_orphans]
54
- )
55
- render_rmchild_events(result.events)
56
- fmt.puts 2, '- all OK'
57
- return result
58
- end
59
- rescue db.exceptions[:moose] => e
60
- abort("ERROR: #{e}")
46
+ operation = build_operation(Moose::Inventory::Operations::GroupChildRelations)
47
+ run_group_relation_transaction(
48
+ heading: "Dissociate parent group '#{parent_name}' from child group(s) '#{child_names.join(',')}':",
49
+ on_error: method(:exception_to_s)
50
+ ) do
51
+ parent_group = fetch_existing_group_or_abort(parent_name)
52
+ result = operation.remove_children(
53
+ parent_group: parent_group,
54
+ parent_name: parent_name,
55
+ child_names: child_names,
56
+ delete_orphans: options[:delete_orphans],
57
+ dry_run: options[:dry_run]
58
+ )
59
+ render_rmchild_events(result.events) unless machine_plan_output_requested?
60
+ result
61
61
  end
62
62
  end
63
63
 
64
- def fetch_existing_group_for_rmchild(context, name)
65
- fmt.puts 2, "- retrieve group '#{name}'..."
66
- group = context.find_group(name)
67
- abort("ERROR: The group '#{name}' does not exist.") if group.nil?
68
-
69
- fmt.puts 4, '- OK'
70
- group
71
- end
72
-
73
64
  def render_rmchild_events(events)
74
- events.each { |event| render_rmchild_event(event) }
75
- end
76
-
77
- def render_rmchild_event(event)
78
- payload = event.payload
79
-
80
- return render_rmchild_warning(payload) if event.type == :child_association_missing
81
- return render_rmchild_missing(payload) if event.type == :missing_skipping
82
- return render_rmchild_progress(event.type, payload) if rmchild_progress_event?(event.type)
83
-
84
- render_rmchild_status(event.type, payload)
85
- end
86
-
87
- def rmchild_progress_event?(type)
88
- %i[
89
- removing_child_association
90
- recursively_delete_orphaned_group
91
- removing_recursive_child_association
92
- ].include?(type)
93
- end
94
-
95
- def render_rmchild_progress(type, payload)
96
- case type
97
- when :removing_child_association
98
- fmt.puts 2, "- remove association {group:#{payload[:parent]} <-> group:#{payload[:child]}}..."
99
- when :recursively_delete_orphaned_group
100
- fmt.puts 2, "- Recursively delete orphaned group '#{payload[:name]}'..."
101
- when :removing_recursive_child_association
102
- fmt.puts 4, "- Remove association {group:#{payload[:parent]} <-> group:#{payload[:child]}}..."
103
- end
104
- end
105
-
106
- def render_rmchild_status(type, payload)
107
- case type
108
- when :adding_automatic_group_to_host
109
- fmt.puts payload[:indent], "- Adding automatic association {group:ungrouped <-> host:#{payload[:host]}}..."
110
- when :destroying_group
111
- fmt.puts payload[:indent], "- Destroy group '#{payload[:name]}'..."
112
- when :ok
113
- fmt.puts payload[:indent], '- OK'
114
- end
115
- end
116
-
117
- def render_rmchild_warning(payload)
118
- fmt.warn "Association {group:#{payload[:parent]} <-> group:#{payload[:child]}} does not exist, skipping.\n"
119
- end
120
-
121
- def render_rmchild_missing(payload)
122
- fmt.puts payload[:indent], "- doesn't exist, skipping."
65
+ emitter = rmchild_emitter
66
+ events.each { |event| emitter.call(event) }
123
67
  end
124
68
  end
125
69
  end
@@ -14,78 +14,43 @@ module Moose
14
14
  #==========================
15
15
  desc 'rmhost GROUPNAME HOSTNAME_1 [HOSTNAME_2 ...]',
16
16
  'Dissociate the hosts HOSTNAME_n from the group NAME'
17
+ option :dry_run, type: :boolean
18
+ option :yes, type: :boolean, desc: 'Confirm destructive dissociation without prompting'
19
+ option :plan_format, type: :string, desc: 'Emit dry-run plan events as yaml|json|pjson'
17
20
  def rmhost(*args)
18
21
  abort_if_missing_args(args, 2, '2 or more')
22
+ validate_machine_plan_request!
19
23
 
20
24
  name = args[0].downcase
21
25
  hosts = normalize_names(args.slice(1, args.length - 1))
22
26
 
23
27
  abort_if_automatic_group([name])
28
+ confirm_destructive_action!("group rmhost #{name} #{hosts.join(',')}")
24
29
 
25
30
  result = remove_hosts_from_group(name, hosts)
31
+ return if machine_plan_output_rendered?(result, command: 'group rmhost')
26
32
 
27
- if result.warning_count.zero?
28
- puts 'Succeeded.'
29
- else
30
- puts 'Succeeded, with warnings.'
31
- end
33
+ record_audit({ command: 'group rmhost', action: 'dissociate', entity_type: 'group',
34
+ entity_names: name }, result: result, dry_run: options[:dry_run])
35
+ print_warning_summary(result)
32
36
  end
33
37
 
34
38
  private
35
39
 
36
40
  def remove_hosts_from_group(name, hosts)
37
- context = Moose::Inventory::InventoryContext.new(db: db)
38
- operation = Moose::Inventory::Operations::RemoveAssociations.new(context: context)
39
-
40
- begin
41
- db.transaction do
42
- puts "Dissociate group '#{name}' from host(s) '#{hosts.join(',')}':"
43
- group = fetch_existing_group_for_rmhost(context, name)
44
- result = operation.group_from_hosts(group: group, group_name: name, host_names: hosts)
45
- render_group_rmhost_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}")
41
+ operation = build_operation(Moose::Inventory::Operations::RemoveAssociations)
42
+ run_group_relation_transaction(heading: "Dissociate group '#{name}' from host(s) '#{hosts.join(',')}':") do
43
+ group = fetch_existing_group_or_abort(name)
44
+ result = operation.group_from_hosts(group: group, group_name: name, host_names: hosts,
45
+ dry_run: options[:dry_run])
46
+ render_group_rmhost_events(result.events) unless machine_plan_output_requested?
47
+ result
51
48
  end
52
49
  end
53
50
 
54
- def fetch_existing_group_for_rmhost(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
51
  def render_group_rmhost_events(events)
64
- events.each { |event| render_group_rmhost_event(event) }
65
- end
66
-
67
- def render_group_rmhost_event(event)
68
- payload = event.payload
69
-
70
- return render_group_rmhost_warning(payload) if event.type == :group_host_association_missing
71
- return render_group_rmhost_missing(payload) if event.type == :missing_skipping
72
-
73
- case event.type
74
- when :removing_group_host_association
75
- fmt.puts 2, "- remove association {group:#{payload[:group]} <-> host:#{payload[:host]}}..."
76
- when :adding_automatic_group
77
- fmt.puts 2, "- add automatic association {group:ungrouped <-> host:#{payload[:host]}}..."
78
- when :ok
79
- fmt.puts payload[:indent], '- OK'
80
- end
81
- end
82
-
83
- def render_group_rmhost_warning(payload)
84
- fmt.warn "Association {group:#{payload[:group]} <-> host:#{payload[:host]}} doesn't exist, skipping.\n"
85
- end
86
-
87
- def render_group_rmhost_missing(payload)
88
- fmt.puts payload[:indent], "- doesn't exist, skipping."
52
+ emitter = host_group_association_removal_emitter(perspective: :group)
53
+ events.each { |event| emitter.call(event) }
89
54
  end
90
55
  end
91
56
  end
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thor'
2
- require_relative './formatter.rb'
4
+ require_relative '../inventory_context'
5
+ require_relative '../operations/remove_variables'
3
6
 
4
7
  module Moose
5
8
  module Inventory
@@ -10,54 +13,40 @@ module Moose
10
13
  #==========================
11
14
  desc 'rmvar NAME VARNAME',
12
15
  'Remove a variable VARNAME from the group NAME'
16
+ option :dry_run, type: :boolean
17
+ option :yes, type: :boolean, desc: 'Confirm destructive removal without prompting'
18
+ option :plan_format, type: :string, desc: 'Emit dry-run plan events as yaml|json|pjson'
13
19
  def rmvar(*args)
14
- if args.length < 2
15
- abort('ERROR: Wrong number of arguments, ' \
16
- "#{args.length} for 2 or more.")
17
- end
18
-
19
- # Convenience
20
- db = Moose::Inventory::DB
21
- fmt = Moose::Inventory::Cli::Formatter
20
+ abort_if_missing_args(args, 2, '2 or more')
21
+ validate_machine_plan_request!
22
22
 
23
- # Arguments
24
23
  name = args[0].downcase
25
24
  vars = args.slice(1, args.length - 1).uniq
25
+ confirm_destructive_action!("group rmvar #{name} #{vars.join(',')}")
26
+ operation = build_operation(Moose::Inventory::Operations::RemoveVariables,
27
+ entity_type: :group,
28
+ emitter: machine_plan_emitter(group_rmvar_emitter(name, vars)))
26
29
 
27
- # Transaction
28
- db.transaction do # Transaction start
29
- puts "Remove variable(s) '#{vars.join(',')}' from group '#{name}':"
30
+ result = db.transaction do
31
+ operation.call(name: name, vars: vars, dry_run: options[:dry_run])
32
+ end
33
+
34
+ return if machine_plan_output_rendered?(result, command: 'group rmvar')
30
35
 
31
- fmt.puts 2, "- retrieve group '#{name}'..."
32
- group = db.models[:group].find(name: name)
33
- if group.nil?
34
- fail db.exceptions[:moose],
35
- "The group '#{name}' does not exist."
36
- end
37
- fmt.puts 4, '- OK'
36
+ record_audit({ command: 'group rmvar', action: 'remove_variable', entity_type: 'group',
37
+ entity_names: name }, result: result, dry_run: options[:dry_run])
38
+ print_success_summary
39
+ end
38
40
 
39
- groupvars_ds = group.groupvars_dataset
40
- vars.each do |v|
41
- fmt.puts 2, "- remove variable '#{v}'..."
42
- vararray = v.split('=')
43
- if v.start_with?('=') || v.scan('=').count > 1
44
- fail db.exceptions[:moose],
45
- "Incorrect format in {#{v}}. " \
46
- 'Expected \'key\' or \'key=value\'.'
47
- end
41
+ private
48
42
 
49
- # Check against existing associations
50
- groupvar = groupvars_ds[name: vararray[0]]
51
- unless groupvar.nil?
52
- # remove the association
53
- group.remove_groupvar(groupvar)
54
- groupvar.destroy
55
- end
56
- fmt.puts 4, '- OK'
57
- end
58
- fmt.puts 2, '- all OK'
59
- end # Transaction end
60
- puts 'Succeeded.'
43
+ def group_rmvar_emitter(name, vars)
44
+ variable_operation_emitter(
45
+ action: :remove,
46
+ entity_label: 'group',
47
+ entity_name: name,
48
+ variables_label: vars.join(',')
49
+ )
61
50
  end
62
51
  end
63
52
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moose
4
+ module Inventory
5
+ module Cli
6
+ class Group
7
+ desc 'addtag GROUP TAG_1 [TAG_2 ...]', 'Add metadata tags to a group'
8
+ def addtag(*args)
9
+ abort_if_missing_args(args, 2, '2 or more')
10
+
11
+ add_tags('group', args[0].downcase, args.slice(1, args.length - 1))
12
+ end
13
+
14
+ desc 'rmtag GROUP TAG_1 [TAG_2 ...]', 'Remove metadata tags from a group'
15
+ option :yes, type: :boolean, desc: 'Confirm destructive tag removal without prompting'
16
+ def rmtag(*args)
17
+ abort_if_missing_args(args, 2, '2 or more')
18
+ confirm_destructive_action!("group rmtag #{args[0].downcase} #{args.slice(1, args.length - 1).join(',')}")
19
+
20
+ remove_tags('group', args[0].downcase, args.slice(1, args.length - 1))
21
+ end
22
+
23
+ desc 'listtags GROUP', 'List metadata tags for a group'
24
+ option :format, type: :string, desc: 'Emit tags as yaml|json|pjson'
25
+ def listtags(*args)
26
+ abort_if_missing_args(args, 1, '1')
27
+
28
+ list_tags('group', args[0].downcase)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,11 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../inventory_context'
4
+ require_relative 'audit_recording'
5
+ require_relative 'association_rendering'
6
+ require_relative 'child_relation_rendering'
7
+ require_relative 'factory'
8
+ require_relative 'listvars_support'
9
+ require_relative 'plan_rendering'
10
+ require_relative 'relation_transaction_support'
11
+ require_relative 'tag_support'
12
+ require_relative 'variable_rendering'
13
+
3
14
  module Moose
4
15
  module Inventory
5
16
  module Cli
6
17
  ##
7
18
  # Shared helpers for Thor command classes.
8
19
  module Helpers
20
+ include Moose::Inventory::Cli::AssociationRendering
21
+ include Moose::Inventory::Cli::AuditRecording
22
+ include Moose::Inventory::Cli::ChildRelationRendering
23
+ include Moose::Inventory::Cli::ListvarsSupport
24
+ include Moose::Inventory::Cli::PlanRendering
25
+ include Moose::Inventory::Cli::RelationTransactionSupport
26
+ include Moose::Inventory::Cli::TagSupport
27
+ include Moose::Inventory::Cli::VariableRendering
28
+
9
29
  AUTOMATIC_GROUP = 'ungrouped'
10
30
 
11
31
  private
@@ -14,10 +34,38 @@ module Moose
14
34
  Moose::Inventory::DB
15
35
  end
16
36
 
37
+ def inventory_context
38
+ @inventory_context ||= Moose::Inventory::InventoryContext.new(db: db)
39
+ end
40
+
41
+ def cli_factory
42
+ @cli_factory ||= Moose::Inventory::Cli::Factory.new(context: inventory_context)
43
+ end
44
+
45
+ def build_operation(operation_class, **)
46
+ cli_factory.operation(operation_class, **)
47
+ end
48
+
49
+ def inventory_query
50
+ cli_factory.query_inventory
51
+ end
52
+
17
53
  def fmt
18
54
  Moose::Inventory::Cli::Formatter
19
55
  end
20
56
 
57
+ def runtime_options
58
+ Moose::Inventory::Config.runtime_options
59
+ end
60
+
61
+ def output_format
62
+ runtime_options.output_format
63
+ end
64
+
65
+ def ansible_mode?
66
+ runtime_options.ansible?
67
+ end
68
+
21
69
  def normalize_names(values)
22
70
  values.uniq.map(&:downcase)
23
71
  end
@@ -38,12 +86,31 @@ module Moose
38
86
  abort(message || "ERROR: Cannot manually manipulate the automatic group '#{AUTOMATIC_GROUP}'.")
39
87
  end
40
88
 
89
+ def confirm_destructive_action!(description)
90
+ return if options[:dry_run] || options[:yes]
91
+
92
+ abort("ERROR: #{description} is destructive. Re-run with --yes to confirm, or use --dry-run to preview.")
93
+ end
94
+
41
95
  def association_exists?(dataset, name)
42
96
  !dataset.nil? && !dataset[name: name].nil?
43
97
  end
44
98
 
99
+ def exception_to_s(error)
100
+ error.to_s
101
+ end
102
+
103
+ def print_warning_summary(result, success_message: 'Succeeded.', warning_message: 'Succeeded, with warnings.')
104
+ warning_count = result.respond_to?(:warning_count) ? result.warning_count : 0
105
+ print_success_summary(warning_count.zero? ? success_message : warning_message)
106
+ end
107
+
108
+ def print_success_summary(message = 'Succeeded.')
109
+ puts message
110
+ end
111
+
45
112
  def automatic_group
46
- db.models[:group].find_or_create(name: AUTOMATIC_GROUP)
113
+ inventory_context.automatic_group
47
114
  end
48
115
 
49
116
  def remove_automatic_group_from_host(host, indent:, message:)
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thor'
2
4
  require 'json'
3
5
 
4
- require_relative './formatter.rb'
5
- require_relative './helpers.rb'
6
- require_relative '../db/exceptions.rb'
6
+ require_relative 'formatter'
7
+ require_relative 'helpers'
8
+ require_relative '../db/exceptions'
7
9
 
8
10
  module Moose
9
11
  module Inventory
@@ -22,6 +24,7 @@ module Moose
22
24
  require_relative 'host_addvar'
23
25
  require_relative 'host_listvars'
24
26
  require_relative 'host_rmvar'
27
+ require_relative 'host_tags'
25
28
  end
26
29
  end
27
30
  end
@@ -15,12 +15,28 @@ module Moose
15
15
  ##
16
16
  # Class implementing the "host" methods of the CLI
17
17
  class Host
18
+ ADD_HOST_EVENT_RENDERERS = {
19
+ host_started: :render_add_host_started,
20
+ creating_host: :render_add_host_creation,
21
+ host_exists: :render_add_host_exists_warning,
22
+ ok: :render_add_host_ok,
23
+ adding_association: :render_add_host_association,
24
+ group_missing_created: :render_add_host_missing_group_warning,
25
+ association_exists: :render_add_host_association_exists_warning,
26
+ adding_automatic_group: :render_add_host_automatic_group,
27
+ host_complete: :render_add_host_complete,
28
+ dry_run_summary: :render_dry_run_summary
29
+ }.freeze
30
+
18
31
  #==========================
19
32
  desc 'add HOSTNAME_1 [HOSTNAME_2 ...]',
20
33
  'Add a hosts HOSTNAME_n to the inventory'
21
34
  option :groups
35
+ option :dry_run, type: :boolean
36
+ option :plan_format, type: :string, desc: 'Emit dry-run plan events as yaml|json|pjson'
22
37
  def add(*argv)
23
38
  abort_if_missing_args(argv, 1, '1 or more')
39
+ validate_machine_plan_request!
24
40
 
25
41
  # Arguments
26
42
  names = normalize_names(argv)
@@ -32,11 +48,14 @@ module Moose
32
48
  # Sanity
33
49
  abort_if_automatic_group(groups)
34
50
 
35
- result = Moose::Inventory::Operations::AddHosts
36
- .new(context: Moose::Inventory::InventoryContext.new(db: db))
37
- .call(names: names, groups: groups)
51
+ result = build_operation(Moose::Inventory::Operations::AddHosts)
52
+ .call(names: names, groups: groups, dry_run: options[:dry_run])
53
+ return if machine_plan_output_rendered?(result, command: 'host add')
54
+
55
+ record_audit({ command: 'host add', action: 'add', entity_type: 'host',
56
+ entity_names: names }, result: result, dry_run: options[:dry_run])
38
57
  render_add_hosts_events(result.events)
39
- puts 'Succeeded'
58
+ print_warning_summary(result, success_message: 'Succeeded', warning_message: 'Succeeded')
40
59
  end
41
60
 
42
61
  private
@@ -46,31 +65,52 @@ module Moose
46
65
  events.each { |event| render_add_hosts_event(event) }
47
66
  end
48
67
 
49
- def render_add_hosts_event(event) # rubocop:disable Metrics/CyclomaticComplexity
50
- payload = event.payload
51
- case event.type
52
- when :host_started
53
- puts "Add host '#{payload[:name]}':"
54
- when :creating_host
55
- fmt.puts 2, "- Creating host '#{payload[:name]}'..."
56
- when :host_exists
57
- fmt.warn "The host '#{payload[:name]}' already exists, skipping creation.\n"
58
- when :ok
59
- fmt.puts payload[:indent], '- OK'
60
- when :adding_association
61
- fmt.puts 2, "- Adding association {host:#{payload[:host]} <-> group:#{payload[:group]}}..."
62
- when :group_missing_created
63
- fmt.warn "The group '#{payload[:name]}' doesn't exist, but will be created.\n"
64
- when :association_exists
65
- fmt.warn(
66
- "Association {host:#{payload[:host]} <-> group:#{payload[:group]}} " \
67
- "already exists, skipping creation.\n"
68
- )
69
- when :adding_automatic_group
70
- fmt.puts 2, "- Adding automatic association {host:#{payload[:host]} <-> group:#{payload[:group]}}..."
71
- when :host_complete
72
- fmt.puts 2, '- All OK'
73
- end
68
+ def render_add_hosts_event(event)
69
+ renderer = ADD_HOST_EVENT_RENDERERS[event.type]
70
+ send(renderer, event.payload) unless renderer.nil?
71
+ end
72
+
73
+ def render_add_host_started(payload)
74
+ puts "Add host '#{payload[:name]}':"
75
+ end
76
+
77
+ def render_add_host_creation(payload)
78
+ fmt.puts 2, "- Creating host '#{payload[:name]}'..."
79
+ end
80
+
81
+ def render_add_host_exists_warning(payload)
82
+ fmt.warn "The host '#{payload[:name]}' already exists, skipping creation.\n"
83
+ end
84
+
85
+ def render_add_host_ok(payload)
86
+ fmt.puts payload[:indent], '- OK'
87
+ end
88
+
89
+ def render_add_host_association(payload)
90
+ fmt.puts 2, "- Adding association {host:#{payload[:host]} <-> group:#{payload[:group]}}..."
91
+ end
92
+
93
+ def render_add_host_missing_group_warning(payload)
94
+ fmt.warn "The group '#{payload[:name]}' doesn't exist, but will be created.\n"
95
+ end
96
+
97
+ def render_add_host_association_exists_warning(payload)
98
+ fmt.warn(
99
+ "Association {host:#{payload[:host]} <-> group:#{payload[:group]}} " \
100
+ "already exists, skipping creation.\n"
101
+ )
102
+ end
103
+
104
+ def render_add_host_automatic_group(payload)
105
+ fmt.puts 2, "- Adding automatic association {host:#{payload[:host]} <-> group:#{payload[:group]}}..."
106
+ end
107
+
108
+ def render_add_host_complete(_payload)
109
+ fmt.puts 2, '- All OK'
110
+ end
111
+
112
+ def render_dry_run_summary(_payload)
113
+ puts 'Dry run complete. No changes applied.'
74
114
  end
75
115
  end
76
116
  end