moose-inventory 1.0.9 → 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 (176) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +15 -1
  3. data/.github/workflows/release.yml +60 -0
  4. data/.gitignore +2 -1
  5. data/.gitleaks.toml +9 -0
  6. data/.rubocop.yml +49 -0
  7. data/BACKLOG.md +752 -24
  8. data/Gemfile +2 -0
  9. data/Gemfile.lock +36 -1
  10. data/README.md +340 -44
  11. data/Rakefile +2 -0
  12. data/bin/moose-inventory +2 -1
  13. data/docs/architecture/architecture-and-trust-boundaries.md +444 -0
  14. data/docs/compatibility/cli-output-compatibility.md +76 -0
  15. data/docs/governance/approval-register.md +37 -0
  16. data/docs/maintenance/database-backup-restore-guidance.md +162 -0
  17. data/docs/maintenance/package-maintenance-and-agent-boundaries.md +260 -0
  18. data/docs/process/conformance-gap-analysis-2026-05-28.md +192 -0
  19. data/docs/product/product-brief.md +161 -0
  20. data/docs/product/requirements-baseline.md +477 -0
  21. data/docs/qa/qa-documentation-and-release-gates.md +283 -0
  22. data/docs/release/package-provenance-hardening.md +126 -0
  23. data/docs/release/publishing.md +54 -50
  24. data/docs/release/release-environment-protection.md +70 -0
  25. data/docs/release/release-readiness.md +37 -4
  26. data/docs/security/accepted-risk-register.md +84 -0
  27. data/docs/security/security-privacy-process.md +287 -0
  28. data/docs/security-audit-2026-05-26-rerun.md +75 -0
  29. data/docs/security-audit-2026-05-26.md +63 -0
  30. data/docs/ux/cli-workflow-notes.md +287 -0
  31. data/examples/ansible/ansible.cfg +3 -0
  32. data/examples/ansible/inventory/moose_inventory.yml +5 -0
  33. data/examples/ansible/inventory_plugins/moose_inventory.py +100 -0
  34. data/examples/ci/README.md +16 -0
  35. data/examples/ci/github-actions/inventory-review.yml +38 -0
  36. data/examples/ci/inventory/example-snapshot.yml +19 -0
  37. data/examples/ci/scripts/validate-inventory-snapshot.sh +30 -0
  38. data/lib/moose_inventory/cli/application.rb +133 -5
  39. data/lib/moose_inventory/cli/association_rendering.rb +74 -0
  40. data/lib/moose_inventory/cli/association_rendering_support.rb +89 -0
  41. data/lib/moose_inventory/cli/audit.rb +62 -0
  42. data/lib/moose_inventory/cli/audit_recording.rb +40 -0
  43. data/lib/moose_inventory/cli/child_relation_rendering.rb +110 -0
  44. data/lib/moose_inventory/cli/console.rb +135 -0
  45. data/lib/moose_inventory/cli/db.rb +64 -0
  46. data/lib/moose_inventory/cli/factory.rb +28 -0
  47. data/lib/moose_inventory/cli/formatter.rb +8 -12
  48. data/lib/moose_inventory/cli/group.rb +7 -1
  49. data/lib/moose_inventory/cli/group_add.rb +91 -73
  50. data/lib/moose_inventory/cli/group_addchild.rb +41 -66
  51. data/lib/moose_inventory/cli/group_addhost.rb +33 -71
  52. data/lib/moose_inventory/cli/group_addvar.rb +27 -47
  53. data/lib/moose_inventory/cli/group_get.rb +8 -42
  54. data/lib/moose_inventory/cli/group_list.rb +7 -40
  55. data/lib/moose_inventory/cli/group_listvars.rb +9 -55
  56. data/lib/moose_inventory/cli/group_rm.rb +105 -73
  57. data/lib/moose_inventory/cli/group_rmchild.rb +47 -57
  58. data/lib/moose_inventory/cli/group_rmhost.rb +34 -61
  59. data/lib/moose_inventory/cli/group_rmvar.rb +30 -41
  60. data/lib/moose_inventory/cli/group_tags.rb +33 -0
  61. data/lib/moose_inventory/cli/helpers.rb +143 -0
  62. data/lib/moose_inventory/cli/host.rb +8 -2
  63. data/lib/moose_inventory/cli/host_add.rb +91 -66
  64. data/lib/moose_inventory/cli/host_addgroup.rb +39 -66
  65. data/lib/moose_inventory/cli/host_addvar.rb +28 -52
  66. data/lib/moose_inventory/cli/host_get.rb +9 -37
  67. data/lib/moose_inventory/cli/host_list.rb +24 -21
  68. data/lib/moose_inventory/cli/host_listvars.rb +9 -62
  69. data/lib/moose_inventory/cli/host_rm.rb +60 -42
  70. data/lib/moose_inventory/cli/host_rmgroup.rb +39 -55
  71. data/lib/moose_inventory/cli/host_rmvar.rb +31 -45
  72. data/lib/moose_inventory/cli/host_tags.rb +33 -0
  73. data/lib/moose_inventory/cli/listvars_support.rb +55 -0
  74. data/lib/moose_inventory/cli/plan_rendering.rb +50 -0
  75. data/lib/moose_inventory/cli/relation_transaction_support.rb +51 -0
  76. data/lib/moose_inventory/cli/tag_support.rb +97 -0
  77. data/lib/moose_inventory/cli/variable_rendering.rb +67 -0
  78. data/lib/moose_inventory/config/config.rb +185 -108
  79. data/lib/moose_inventory/db/db.rb +188 -193
  80. data/lib/moose_inventory/db/exceptions.rb +6 -3
  81. data/lib/moose_inventory/db/models.rb +16 -0
  82. data/lib/moose_inventory/db/schema_migrations.rb +248 -0
  83. data/lib/moose_inventory/inventory_context.rb +116 -0
  84. data/lib/moose_inventory/operations/add_associations.rb +131 -0
  85. data/lib/moose_inventory/operations/add_groups.rb +123 -0
  86. data/lib/moose_inventory/operations/add_hosts.rb +123 -0
  87. data/lib/moose_inventory/operations/add_variables.rb +77 -0
  88. data/lib/moose_inventory/operations/entity_variable_operation_support.rb +46 -0
  89. data/lib/moose_inventory/operations/group_child_relations.rb +125 -0
  90. data/lib/moose_inventory/operations/group_cleanup.rb +70 -0
  91. data/lib/moose_inventory/operations/import_inventory_snapshot.rb +41 -0
  92. data/lib/moose_inventory/operations/inventory_doctor.rb +172 -0
  93. data/lib/moose_inventory/operations/inventory_snapshot.rb +60 -0
  94. data/lib/moose_inventory/operations/inventory_snapshot_applier.rb +112 -0
  95. data/lib/moose_inventory/operations/inventory_snapshot_preview.rb +174 -0
  96. data/lib/moose_inventory/operations/inventory_snapshot_validator.rb +134 -0
  97. data/lib/moose_inventory/operations/operation_event_support.rb +27 -0
  98. data/lib/moose_inventory/operations/query_inventory/base_query.rb +24 -0
  99. data/lib/moose_inventory/operations/query_inventory/group_queries.rb +86 -0
  100. data/lib/moose_inventory/operations/query_inventory/host_queries.rb +106 -0
  101. data/lib/moose_inventory/operations/query_inventory.rb +47 -0
  102. data/lib/moose_inventory/operations/remove_associations.rb +113 -0
  103. data/lib/moose_inventory/operations/remove_groups.rb +79 -0
  104. data/lib/moose_inventory/operations/remove_hosts.rb +68 -0
  105. data/lib/moose_inventory/operations/remove_variables.rb +67 -0
  106. data/lib/moose_inventory/runtime_options.rb +31 -0
  107. data/lib/moose_inventory/version.rb +3 -1
  108. data/lib/moose_inventory.rb +10 -7
  109. data/moose-inventory.gemspec +22 -35
  110. data/scripts/check.sh +3 -0
  111. data/scripts/ci/check_generated_artifacts.sh +41 -0
  112. data/scripts/ci/check_permissions.sh +5 -0
  113. data/scripts/ci/check_rubocop.sh +33 -0
  114. data/scripts/ci/check_secrets.sh +26 -0
  115. data/scripts/ci/check_security.sh +18 -0
  116. data/scripts/ci/install_security_tools.sh +47 -0
  117. data/scripts/files.rb +5 -4
  118. data/scripts/install_dependencies.sh +2 -0
  119. data/spec/examples/ci_examples_spec.rb +37 -0
  120. data/spec/lib/moose_inventory/ansible_plugin_examples_spec.rb +29 -0
  121. data/spec/lib/moose_inventory/cli/application_doctor_spec.rb +50 -0
  122. data/spec/lib/moose_inventory/cli/application_import_export_spec.rb +100 -0
  123. data/spec/lib/moose_inventory/cli/application_spec.rb +25 -15
  124. data/spec/lib/moose_inventory/cli/audit_spec.rb +56 -0
  125. data/spec/lib/moose_inventory/cli/cli_spec.rb +15 -19
  126. data/spec/lib/moose_inventory/cli/console_spec.rb +98 -0
  127. data/spec/lib/moose_inventory/cli/factory_spec.rb +27 -0
  128. data/spec/lib/moose_inventory/cli/formatter_spec.rb +95 -3
  129. data/spec/lib/moose_inventory/cli/group_add_spec.rb +140 -116
  130. data/spec/lib/moose_inventory/cli/group_addchild_spec.rb +89 -35
  131. data/spec/lib/moose_inventory/cli/group_addhost_spec.rb +81 -84
  132. data/spec/lib/moose_inventory/cli/group_addvar_spec.rb +65 -68
  133. data/spec/lib/moose_inventory/cli/group_get_spec.rb +17 -33
  134. data/spec/lib/moose_inventory/cli/group_list_spec.rb +16 -38
  135. data/spec/lib/moose_inventory/cli/group_listvar_spec.rb +33 -40
  136. data/spec/lib/moose_inventory/cli/group_rm_spec.rb +165 -85
  137. data/spec/lib/moose_inventory/cli/group_rmchild_spec.rb +100 -30
  138. data/spec/lib/moose_inventory/cli/group_rmhost_spec.rb +76 -78
  139. data/spec/lib/moose_inventory/cli/group_rmvar_spec.rb +57 -63
  140. data/spec/lib/moose_inventory/cli/group_spec.rb +2 -0
  141. data/spec/lib/moose_inventory/cli/helpers_spec.rb +146 -0
  142. data/spec/lib/moose_inventory/cli/host_add_spec.rb +170 -116
  143. data/spec/lib/moose_inventory/cli/host_addgroup_spec.rb +100 -83
  144. data/spec/lib/moose_inventory/cli/host_addvar_spec.rb +92 -74
  145. data/spec/lib/moose_inventory/cli/host_get_spec.rb +14 -33
  146. data/spec/lib/moose_inventory/cli/host_list_spec.rb +41 -33
  147. data/spec/lib/moose_inventory/cli/host_listvar_spec.rb +45 -53
  148. data/spec/lib/moose_inventory/cli/host_rm_spec.rb +66 -48
  149. data/spec/lib/moose_inventory/cli/host_rmgroup_spec.rb +73 -83
  150. data/spec/lib/moose_inventory/cli/host_rmvar_spec.rb +56 -63
  151. data/spec/lib/moose_inventory/cli/host_spec.rb +2 -0
  152. data/spec/lib/moose_inventory/cli/tags_spec.rb +81 -0
  153. data/spec/lib/moose_inventory/config/config_spec.rb +41 -3
  154. data/spec/lib/moose_inventory/db/db_spec.rb +551 -29
  155. data/spec/lib/moose_inventory/db/exceptions_spec.rb +18 -0
  156. data/spec/lib/moose_inventory/db/models_spec.rb +7 -3
  157. data/spec/lib/moose_inventory/db_lifecycle_spec.rb +73 -0
  158. data/spec/lib/moose_inventory/inventory_context_spec.rb +10 -0
  159. data/spec/lib/moose_inventory/operations/add_associations_spec.rb +111 -0
  160. data/spec/lib/moose_inventory/operations/add_groups_spec.rb +80 -0
  161. data/spec/lib/moose_inventory/operations/add_hosts_spec.rb +82 -0
  162. data/spec/lib/moose_inventory/operations/add_variables_spec.rb +103 -0
  163. data/spec/lib/moose_inventory/operations/group_child_relations_spec.rb +122 -0
  164. data/spec/lib/moose_inventory/operations/import_inventory_snapshot_spec.rb +226 -0
  165. data/spec/lib/moose_inventory/operations/inventory_doctor_spec.rb +77 -0
  166. data/spec/lib/moose_inventory/operations/inventory_snapshot_spec.rb +50 -0
  167. data/spec/lib/moose_inventory/operations/operation_event_support_spec.rb +78 -0
  168. data/spec/lib/moose_inventory/operations/query_inventory_spec.rb +146 -0
  169. data/spec/lib/moose_inventory/operations/remove_associations_spec.rb +113 -0
  170. data/spec/lib/moose_inventory/operations/remove_groups_spec.rb +78 -0
  171. data/spec/lib/moose_inventory/operations/remove_hosts_spec.rb +55 -0
  172. data/spec/lib/moose_inventory/operations/remove_variables_spec.rb +83 -0
  173. data/spec/shared/shared_config_setup.rb +4 -3
  174. data/spec/spec_helper.rb +50 -40
  175. data/spec/support/cli_harness.rb +33 -0
  176. metadata +163 -35
@@ -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
@@ -1,8 +1,12 @@
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 'formatter'
7
+ require_relative '../db/exceptions'
8
+ require_relative '../inventory_context'
9
+ require_relative '../operations/remove_associations'
6
10
 
7
11
  module Moose
8
12
  module Inventory
@@ -13,66 +17,46 @@ module Moose
13
17
  #==========================
14
18
  desc 'rmgroup HOSTNAME GROUPNAME [GROUPNAME ...]',
15
19
  'dissociation the host from a group'
16
- # rubocop:disable Metrics/LineLength
17
- def rmgroup(*args) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
18
- # rubocop:enable Metrics/LineLength
19
- if args.length < 2
20
- abort('ERROR: Wrong number of arguments, '\
21
- "#{args.length} for 2 or more.")
22
- end
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'
23
+ def rmgroup(*args)
24
+ abort_if_missing_args(args, 2, '2 or more')
25
+ validate_machine_plan_request!
23
26
 
24
- # Convenience
25
- db = Moose::Inventory::DB
26
- fmt = Moose::Inventory::Cli::Formatter
27
+ name = args[0].downcase
28
+ groups = normalize_names(args.slice(1, args.length - 1))
27
29
 
28
- # arguments
29
- name = args[0].downcase
30
- groups = args.slice(1, args.length - 1).uniq.map(&:downcase)
30
+ abort_if_automatic_group(groups)
31
+ confirm_destructive_action!("host rmgroup #{name} #{groups.join(',')}")
31
32
 
32
- # Sanity
33
- if groups.include?('ungrouped')
34
- abort 'ERROR: Cannot manually manipulate the automatic '\
35
- 'group \'ungrouped\'.'
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')
36
41
  end
42
+ end
37
43
 
38
- # Transaction
39
- db.transaction do # Transaction start
40
- puts "Dissociate host '#{name}' from groups '#{groups.join(',')}':"
41
- fmt.puts 2, "- Retrieve host '#{name}'..."
42
- host = db.models[:host].find(name: name)
43
- if host.nil?
44
- fail db.exceptions[:moose],
45
- "The host '#{name}' was not found in the database."
46
- end
47
- fmt.puts 4, '- OK'
48
-
49
- # dissociate host from the groups
50
- groups_ds = host.groups_dataset
51
- groups.each do |g|
52
- fmt.puts 2, "- Remove association {host:#{name} <-> group:#{g}}..."
44
+ private
53
45
 
54
- # Check against existing associations
55
- if groups_ds[name: g].nil?
56
- fmt.warn "Association {host:#{name} <-> group:#{g}} doesn't exist, skipping.\n"
57
- fmt.puts 4, "- Doesn't exist, skipping."
58
- else
59
- group = db.models[:group].find(name: g)
60
- host.remove_group(group) unless group.nil?
61
- end
62
- fmt.puts 4, '- OK'
63
- end
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
54
+ end
55
+ end
64
56
 
65
- # Handle 'ungrouped' group automation
66
- if host.groups_dataset.count == 0
67
- fmt.puts 2, '- Add automatic association '\
68
- "{host:#{name} <-> group:ungrouped}..."
69
- ungrouped = db.models[:group].find_or_create(name: 'ungrouped')
70
- host.add_group(ungrouped) unless ungrouped.nil?
71
- fmt.puts 4, '- OK'
72
- end
73
- fmt.puts 2, '- All OK'
74
- end # End transaction
75
- puts 'Succeeded'
57
+ def render_host_rmgroup_events(events)
58
+ emitter = host_group_association_removal_emitter(perspective: :host)
59
+ events.each { |event| emitter.call(event) }
76
60
  end
77
61
  end
78
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