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
@@ -1,38 +1,41 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thor'
2
4
  require 'json'
3
5
 
4
- require_relative './formatter.rb'
5
- require_relative '../db/exceptions.rb'
6
+ require_relative '../inventory_context'
7
+ require_relative '../operations/query_inventory'
6
8
 
7
9
  module Moose
8
10
  module Inventory
9
11
  module Cli
10
- ##
11
12
  # Implementation of the "host list" method of the CLI
12
13
  class Host
13
14
  desc 'list', 'List the contents of the inventory by host'
14
- def list # rubocop:disable Metrics/AbcSize
15
- # Convenience
16
- db = Moose::Inventory::DB
17
- fmt = Moose::Inventory::Cli::Formatter
15
+ option :group, type: :string, desc: 'Only include hosts in all comma-separated groups'
16
+ option :tag, type: :string, desc: 'Only include hosts with all comma-separated tags'
17
+ option :var, type: :string, desc: 'Only include hosts with comma-separated key=value variables'
18
+ def list
19
+ fmt.dump(inventory_query.list_hosts(filters: host_list_filters), output_format)
20
+ end
18
21
 
19
- # Process
20
- results = {}
21
- db.models[:host].all.each do |host|
22
- groups = host.groups_dataset.map(:name)
23
- results[host[:name].to_sym] = {}
24
- results[host[:name].to_sym][:groups] = groups
22
+ private
23
+
24
+ def host_list_filters
25
+ {
26
+ groups: csv_option_names(options[:group]),
27
+ tags: csv_option_names(options[:tag]),
28
+ variables: variable_filter_options(options[:var])
29
+ }
30
+ end
25
31
 
26
- hostvars = {}
27
- host.hostvars_dataset.each do |hv|
28
- hostvars[hv[:name].to_sym] = hv[:value]
29
- end
32
+ def variable_filter_options(value)
33
+ csv_option_names(value).to_h do |entry|
34
+ key, variable_value = entry.split('=', 2)
35
+ abort("ERROR: Invalid variable filter '#{entry}'. Expected key=value.") if variable_value.nil?
30
36
 
31
- unless hostvars.empty?
32
- results[host[:name].to_sym][:hostvars] = hostvars
33
- end
37
+ [key, variable_value]
34
38
  end
35
- fmt.dump(results)
36
39
  end
37
40
  end
38
41
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thor'
2
4
  require 'json'
3
5
 
4
- require_relative './formatter.rb'
5
- require_relative '../db/exceptions.rb'
6
+ require_relative '../inventory_context'
7
+ require_relative '../operations/query_inventory'
6
8
 
7
9
  module Moose
8
10
  module Inventory
@@ -13,68 +15,13 @@ module Moose
13
15
  #==========================
14
16
  desc 'listvar', 'List all variables associated with the host'
15
17
  def listvars(*argv)
16
- # Convenience
17
- confopts = Moose::Inventory::Config._confopts
18
-
19
- # sanity
20
- if confopts[:ansible] == true
21
- if argv.length != 1
22
- abort('ERROR: Wrong number of arguments for Ansible mode, '\
23
- "#{args.length} for 1.")
24
- end
25
- else
26
- if argv.empty?
27
- abort('ERROR: Wrong number of arguments, '\
28
- "#{args.length} for 1 or more.")
29
- end
30
- end
31
-
32
- # Convenience
33
- db = Moose::Inventory::DB
34
- fmt = Moose::Inventory::Cli::Formatter
35
-
36
- # Arguments
37
- names = argv.uniq.map(&:downcase)
38
-
39
- # process
40
- results = {}
41
-
42
- if confopts[:ansible] == true
43
- # This is the implementation per Ansible specs
44
- name = names.first
45
- host = db.models[:host].find(name: name)
46
- if host.nil?
47
- fmt.warn "The host #{name} does not exist.\n"
48
- else
49
- host.hostvars_dataset.each do |hv|
50
- results[hv[:name].to_sym] = hv[:value]
51
- end
52
- end
18
+ validate_listvars_args(argv)
53
19
 
54
- # Add the Ansible 1.3 '_meta' tag
55
- # see http://docs.ansible.com/ansible/developing_inventory.html#tuning-the-external-inventory-script
56
- results['_meta'.to_sym] = {}
57
- results['_meta'.to_sym]['hostvars'.to_sym] = {}
58
- db.models[:host].each do |host|
59
- results['_meta'.to_sym]['hostvars'.to_sym][host.name.to_sym] = {}
60
- host.hostvars_dataset.each do |hv|
61
- results['_meta'.to_sym]['hostvars'.to_sym][host.name.to_sym][hv[:name].to_sym] = hv[:value]
62
- end
63
- end
20
+ names = normalize_names(argv)
21
+ results = inventory_query.list_host_vars(names: names, ansible: ansible_mode?)
22
+ warn_if_missing_ansible_listvars_entity(:host, names.first)
64
23
 
65
- else
66
- # This our more flexible implementation, which is not compatible
67
- # with the Ansible specs
68
- names.each do |name|
69
- host = db.models[:host].find(name: name)
70
- next if host.nil?
71
- results[name.to_sym] = {}
72
- host.hostvars_dataset.each do |hv|
73
- results[name.to_sym][hv[:name].to_sym] = hv[:value]
74
- end
75
- end
76
- end
77
- fmt.dump(results)
24
+ fmt.dump(results, output_format)
78
25
  end
79
26
  end
80
27
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thor'
2
4
  require 'json'
3
5
 
4
- require_relative './formatter.rb'
5
- require_relative '../db/exceptions.rb'
6
+ require_relative '../inventory_context'
7
+ require_relative '../operations/remove_hosts'
6
8
 
7
9
  module Moose
8
10
  module Inventory
@@ -13,49 +15,65 @@ module Moose
13
15
  #==========================
14
16
  desc 'rm HOSTNAME_1 [HOSTNAME_2 ...]',
15
17
  'Remove hosts HOSTNAME_n from the inventory'
16
- def rm(*argv) # rubocop:disable Metrics/AbcSize
17
- #
18
- # Sanity
19
- if argv.empty?
20
- abort('ERROR: Wrong number of arguments, '\
21
- "#{argv.length} for 1 or more.")
22
- end
18
+ option :dry_run, type: :boolean
19
+ option :yes, type: :boolean, desc: 'Confirm destructive removal without prompting'
20
+ option :plan_format, type: :string, desc: 'Emit dry-run plan events as yaml|json|pjson'
21
+ def rm(*argv)
22
+ abort_if_missing_args(argv, 1, '1 or more')
23
+ validate_machine_plan_request!
24
+
25
+ names = normalize_names(argv)
26
+ confirm_destructive_action!("host rm #{names.join(',')}")
27
+ result = remove_hosts_operation.call(names: names, dry_run: options[:dry_run])
28
+ return if machine_plan_output_rendered?(result, command: 'host rm')
29
+
30
+ record_audit({ command: 'host rm', action: 'remove', entity_type: 'host',
31
+ entity_names: names }, result: result, dry_run: options[:dry_run])
32
+ render_remove_hosts_events(result.events)
33
+ print_warning_summary(result)
34
+ end
35
+
36
+ private
37
+
38
+ def remove_hosts_operation
39
+ build_operation(Moose::Inventory::Operations::RemoveHosts)
40
+ end
41
+
42
+ def render_remove_hosts_events(events)
43
+ events.each { |event| render_remove_hosts_event(event) }
44
+ end
23
45
 
24
- # Convenience
25
- db = Moose::Inventory::DB
26
- fmt = Moose::Inventory::Cli::Formatter
27
-
28
- # Arguments
29
- names = argv.uniq.map(&:downcase)
30
-
31
- # Transaction
32
- warn_count = 0
33
- db.transaction do # Transaction start
34
- names.each do |name|
35
- puts "Remove host '#{name}':"
36
- fmt.puts 2, "- Retrieve host '#{name}'..."
37
- host = db.models[:host].find(name: name)
38
- if host.nil?
39
- warn_count += 1
40
- fmt.warn "Host '#{name}' does not exist, skipping.\n"
41
- fmt.puts 4, '- No such host, skipping.'
42
- end
43
- fmt.puts 4, '- OK'
44
- unless host.nil?
45
- fmt.puts 2, "- Destroy host '#{name}'..."
46
- host.remove_all_groups
47
- host.destroy
48
- fmt.puts 4, '- OK'
49
- end
50
- fmt.puts 2, '- All OK'
51
- end
52
- end # Transaction end
53
- if warn_count == 0
54
- puts 'Succeeded.'
55
- else
56
- puts 'Succeeded, with warnings.'
46
+ def render_remove_hosts_event(event)
47
+ payload = event.payload
48
+
49
+ return render_host_rm_progress(event.type, payload) if host_rm_progress_event?(event.type)
50
+ return render_host_rm_warning(payload) if event.type == :host_missing
51
+ return fmt.puts(payload[:indent], '- No such host, skipping.') if event.type == :missing_skipping
52
+ return fmt.puts(payload[:indent], '- OK') if event.type == :ok
53
+
54
+ return fmt.puts 2, '- All OK' if event.type == :host_complete
55
+
56
+ puts 'Dry run complete. No changes applied.' if event.type == :dry_run_summary
57
+ end
58
+
59
+ def host_rm_progress_event?(type)
60
+ %i[host_started retrieving_host destroying_host].include?(type)
61
+ end
62
+
63
+ def render_host_rm_progress(type, payload)
64
+ case type
65
+ when :host_started
66
+ puts "Remove host '#{payload[:name]}':"
67
+ when :retrieving_host
68
+ fmt.puts 2, "- Retrieve host '#{payload[:name]}'..."
69
+ when :destroying_host
70
+ fmt.puts 2, "- Destroy host '#{payload[:name]}'..."
57
71
  end
58
72
  end
73
+
74
+ def render_host_rm_warning(payload)
75
+ fmt.warn "Host '#{payload[:name]}' does not exist, skipping.\n"
76
+ end
59
77
  end
60
78
  end
61
79
  end
@@ -17,65 +17,46 @@ module Moose
17
17
  #==========================
18
18
  desc 'rmgroup HOSTNAME GROUPNAME [GROUPNAME ...]',
19
19
  'dissociation the host from a group'
20
+ option :dry_run, type: :boolean
21
+ option :yes, type: :boolean, desc: 'Confirm destructive dissociation without prompting'
22
+ option :plan_format, type: :string, desc: 'Emit dry-run plan events as yaml|json|pjson'
20
23
  def rmgroup(*args)
21
24
  abort_if_missing_args(args, 2, '2 or more')
25
+ validate_machine_plan_request!
22
26
 
23
27
  name = args[0].downcase
24
28
  groups = normalize_names(args.slice(1, args.length - 1))
25
29
 
26
30
  abort_if_automatic_group(groups)
27
-
28
- context = Moose::Inventory::InventoryContext.new(db: db)
29
- operation = Moose::Inventory::Operations::RemoveAssociations.new(context: context)
30
-
31
- db.transaction do
32
- puts "Dissociate host '#{name}' from groups '#{groups.join(',')}':"
33
- host = fetch_existing_host_for_rmgroup(context, name)
34
- render_host_rmgroup_events(
35
- operation.host_from_groups(host: host, host_name: name, group_names: groups).events
36
- )
37
- fmt.puts 2, '- All OK'
31
+ confirm_destructive_action!("host rmgroup #{name} #{groups.join(',')}")
32
+
33
+ result = remove_groups_from_host(name, groups)
34
+ unless machine_plan_output_rendered?(
35
+ result, command: 'host rmgroup'
36
+ )
37
+ record_audit({ command: 'host rmgroup', action: 'dissociate', entity_type: 'host',
38
+ entity_names: name }, result: result, dry_run: options[:dry_run])
39
+ print_warning_summary(result, success_message: 'Succeeded',
40
+ warning_message: 'Succeeded')
38
41
  end
39
- puts 'Succeeded'
40
42
  end
41
43
 
42
44
  private
43
45
 
44
- def fetch_existing_host_for_rmgroup(context, name)
45
- fmt.puts 2, "- Retrieve host '#{name}'..."
46
- host = context.find_host(name)
47
- raise db.exceptions[:moose], "The host '#{name}' was not found in the database." if host.nil?
48
-
49
- fmt.puts 4, '- OK'
50
- host
51
- end
52
-
53
- def render_host_rmgroup_events(events)
54
- events.each { |event| render_host_rmgroup_event(event) }
55
- end
56
-
57
- def render_host_rmgroup_event(event)
58
- payload = event.payload
59
-
60
- return render_host_rmgroup_warning(payload) if event.type == :host_group_association_missing
61
- return render_host_rmgroup_missing(payload) if event.type == :missing_skipping
62
-
63
- case event.type
64
- when :removing_host_group_association
65
- fmt.puts 2, "- Remove association {host:#{payload[:host]} <-> group:#{payload[:group]}}..."
66
- when :adding_automatic_group
67
- fmt.puts 2, "- Add automatic association {host:#{payload[:host]} <-> group:ungrouped}..."
68
- when :ok
69
- fmt.puts payload[:indent], '- OK'
46
+ def remove_groups_from_host(name, groups)
47
+ operation = build_operation(Moose::Inventory::Operations::RemoveAssociations)
48
+ run_host_relation_transaction(heading: "Dissociate host '#{name}' from groups '#{groups.join(',')}':") do
49
+ host = fetch_existing_host_or_raise(name)
50
+ result = operation.host_from_groups(host: host, host_name: name, group_names: groups,
51
+ dry_run: options[:dry_run])
52
+ render_host_rmgroup_events(result.events) unless machine_plan_output_requested?
53
+ result
70
54
  end
71
55
  end
72
56
 
73
- def render_host_rmgroup_warning(payload)
74
- fmt.warn "Association {host:#{payload[:host]} <-> group:#{payload[:group]}} doesn't exist, skipping.\n"
75
- end
76
-
77
- def render_host_rmgroup_missing(payload)
78
- fmt.puts payload[:indent], "- Doesn't exist, skipping."
57
+ def render_host_rmgroup_events(events)
58
+ emitter = host_group_association_removal_emitter(perspective: :host)
59
+ events.each { |event| emitter.call(event) }
79
60
  end
80
61
  end
81
62
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thor'
2
4
  require 'json'
3
5
 
4
- require_relative './formatter.rb'
5
- require_relative '../db/exceptions.rb'
6
+ require_relative '../inventory_context'
7
+ require_relative '../operations/remove_variables'
6
8
 
7
9
  module Moose
8
10
  module Inventory
@@ -12,56 +14,40 @@ module Moose
12
14
  class Host
13
15
  #==========================
14
16
  desc 'rmvar', 'Remove a variable from the host'
15
- # rubocop:disable Metrics/LineLength
16
- def rmvar(*args) # rubocop:disable Metrics/AbcSize
17
- # rubocop:enableMetrics/LineLength
18
- if args.length < 2
19
- abort('ERROR: Wrong number of arguments, ' \
20
- "#{args.length} for 2 or more.")
21
- end
22
-
23
- # Convenience
24
- db = Moose::Inventory::DB
25
- fmt = Moose::Inventory::Cli::Formatter
17
+ option :dry_run, type: :boolean
18
+ option :yes, type: :boolean, desc: 'Confirm destructive removal without prompting'
19
+ option :plan_format, type: :string, desc: 'Emit dry-run plan events as yaml|json|pjson'
20
+ def rmvar(*args)
21
+ abort_if_missing_args(args, 2, '2 or more')
22
+ validate_machine_plan_request!
26
23
 
27
- # Arguments
28
24
  name = args[0].downcase
29
25
  vars = args.slice(1, args.length - 1).uniq
26
+ confirm_destructive_action!("host rmvar #{name} #{vars.join(',')}")
27
+ operation = build_operation(Moose::Inventory::Operations::RemoveVariables,
28
+ entity_type: :host,
29
+ emitter: machine_plan_emitter(host_rmvar_emitter(name, vars)))
30
30
 
31
- # Transaction
32
- db.transaction do # Transaction start
33
- puts "Remove variable(s) '#{vars.join(',')}' from host '#{name}':"
31
+ result = db.transaction do
32
+ operation.call(name: name, vars: vars, dry_run: options[:dry_run])
33
+ end
34
+
35
+ return if machine_plan_output_rendered?(result, command: 'host rmvar')
34
36
 
35
- fmt.puts 2, "- retrieve host '#{name}'..."
36
- host = db.models[:host].find(name: name)
37
- if host.nil?
38
- fail db.exceptions[:moose],
39
- "The host '#{name}' does not exist."
40
- end
41
- fmt.puts 4, '- OK'
37
+ record_audit({ command: 'host rmvar', action: 'remove_variable', entity_type: 'host',
38
+ entity_names: name }, result: result, dry_run: options[:dry_run])
39
+ print_success_summary
40
+ end
42
41
 
43
- hostvars_ds = host.hostvars_dataset
44
- vars.each do |v|
45
- fmt.puts 2, "- remove variable '#{v}'..."
46
- vararray = v.split('=')
47
- if v.start_with?('=') || v.scan('=').count > 1
48
- fail db.exceptions[:moose],
49
- "Incorrect format in {#{v}}. " \
50
- 'Expected \'key\' or \'key=value\'.'
51
- end
42
+ private
52
43
 
53
- # Check against existing associations
54
- hostvar = hostvars_ds[name: vararray[0]]
55
- unless hostvar.nil?
56
- # remove the association
57
- host.remove_hostvar(hostvar)
58
- hostvar.destroy
59
- end
60
- fmt.puts 4, '- OK'
61
- end
62
- fmt.puts 2, '- all OK'
63
- end # Transaction end
64
- puts 'Succeeded.'
44
+ def host_rmvar_emitter(name, vars)
45
+ variable_operation_emitter(
46
+ action: :remove,
47
+ entity_label: 'host',
48
+ entity_name: name,
49
+ variables_label: vars.join(',')
50
+ )
65
51
  end
66
52
  end
67
53
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moose
4
+ module Inventory
5
+ module Cli
6
+ class Host
7
+ desc 'addtag HOST TAG_1 [TAG_2 ...]', 'Add metadata tags to a host'
8
+ def addtag(*args)
9
+ abort_if_missing_args(args, 2, '2 or more')
10
+
11
+ add_tags('host', args[0].downcase, args.slice(1, args.length - 1))
12
+ end
13
+
14
+ desc 'rmtag HOST TAG_1 [TAG_2 ...]', 'Remove metadata tags from a host'
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!("host rmtag #{args[0].downcase} #{args.slice(1, args.length - 1).join(',')}")
19
+
20
+ remove_tags('host', args[0].downcase, args.slice(1, args.length - 1))
21
+ end
22
+
23
+ desc 'listtags HOST', 'List metadata tags for a host'
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('host', args[0].downcase)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moose
4
+ module Inventory
5
+ module Cli
6
+ # Shared argument and warning helpers for host/group listvars commands.
7
+ module ListvarsSupport
8
+ private
9
+
10
+ def validate_listvars_args(args)
11
+ if ansible_mode?
12
+ abort_if_wrong_ansible_listvars_arg_count(args, 1)
13
+ else
14
+ abort_if_missing_args(args, 1, '1 or more')
15
+ end
16
+ end
17
+
18
+ def warn_if_missing_ansible_listvars_entity(entity_type, name)
19
+ return unless ansible_mode?
20
+ return unless missing_listvars_entity?(entity_type, name)
21
+
22
+ fmt.warn missing_ansible_listvars_warning(entity_type, name)
23
+ end
24
+
25
+ def abort_if_wrong_ansible_listvars_arg_count(args, expected)
26
+ return if args.length == expected
27
+
28
+ abort("ERROR: Wrong number of arguments for Ansible mode, #{args.length} for #{expected}.")
29
+ end
30
+
31
+ def missing_listvars_entity?(entity_type, name)
32
+ case entity_type
33
+ when :host
34
+ inventory_context.find_host(name).nil?
35
+ when :group
36
+ inventory_context.find_group(name).nil?
37
+ else
38
+ raise ArgumentError, "Unsupported listvars entity type: #{entity_type.inspect}"
39
+ end
40
+ end
41
+
42
+ def missing_ansible_listvars_warning(entity_type, name)
43
+ case entity_type
44
+ when :host
45
+ "The host #{name} does not exist.\n"
46
+ when :group
47
+ "The Group #{name} does not exist."
48
+ else
49
+ raise ArgumentError, "Unsupported listvars entity type: #{entity_type.inspect}"
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moose
4
+ module Inventory
5
+ module Cli
6
+ # Shared machine-readable dry-run plan rendering helpers.
7
+ module PlanRendering
8
+ private
9
+
10
+ def machine_plan_output_requested?
11
+ respond_to?(:options) && !options[:plan_format].nil?
12
+ end
13
+
14
+ def validate_machine_plan_request!
15
+ abort('ERROR: --plan-format requires --dry-run.') if machine_plan_output_requested? && !options[:dry_run]
16
+ end
17
+
18
+ def machine_plan_emitter(emitter)
19
+ machine_plan_output_requested? ? nil : emitter
20
+ end
21
+
22
+ def machine_plan_output_rendered?(result, command:)
23
+ return false unless machine_plan_output_requested?
24
+
25
+ fmt.dump(
26
+ {
27
+ command: command,
28
+ dry_run: true,
29
+ changes_applied: false,
30
+ events: result.events.map { |event| serialize_plan_event(event) }
31
+ },
32
+ options[:plan_format].downcase
33
+ )
34
+ true
35
+ end
36
+
37
+ def serialize_plan_event(event)
38
+ {
39
+ type: event.type.to_s,
40
+ payload: stringify_plan_payload(event.payload)
41
+ }
42
+ end
43
+
44
+ def stringify_plan_payload(payload)
45
+ payload.to_h.transform_keys(&:to_s)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moose
4
+ module Inventory
5
+ module Cli
6
+ # Shared transaction/fetch helpers for host/group relation commands.
7
+ module RelationTransactionSupport
8
+ private
9
+
10
+ def fetch_existing_group_or_abort(name)
11
+ fmt.puts 2, "- retrieve group '#{name}'..." unless machine_plan_output_requested?
12
+ group = inventory_context.find_group(name)
13
+ abort("ERROR: The group '#{name}' does not exist.") if group.nil?
14
+
15
+ fmt.puts 4, '- OK' unless machine_plan_output_requested?
16
+ group
17
+ end
18
+
19
+ def fetch_existing_host_or_raise(name)
20
+ fmt.puts 2, "- Retrieve host '#{name}'..." unless machine_plan_output_requested?
21
+ host = inventory_context.find_host(name)
22
+ raise db.exceptions[:moose], "The host '#{name}' was not found in the database." if host.nil?
23
+
24
+ fmt.puts 4, '- OK' unless machine_plan_output_requested?
25
+ host
26
+ end
27
+
28
+ def run_group_relation_transaction(heading:, on_error: nil, &)
29
+ run_relation_transaction(heading: heading, all_ok_message: '- all OK', on_error: on_error, &)
30
+ end
31
+
32
+ def run_host_relation_transaction(heading:, on_error: nil, &)
33
+ run_relation_transaction(heading: heading, all_ok_message: '- All OK', on_error: on_error, &)
34
+ end
35
+
36
+ def run_relation_transaction(heading:, all_ok_message:, on_error: nil)
37
+ result = nil
38
+ db.transaction do
39
+ puts heading unless machine_plan_output_requested?
40
+ result = yield
41
+ fmt.puts 2, all_ok_message unless machine_plan_output_requested?
42
+ end
43
+ result
44
+ rescue db.exceptions[:moose] => e
45
+ message = on_error ? on_error.call(e) : e.message
46
+ abort("ERROR: #{message}")
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end