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
@@ -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
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moose
4
+ module Inventory
5
+ module Cli
6
+ # Shared host/group metadata tag commands.
7
+ module TagSupport
8
+ private
9
+
10
+ def add_tags(entity_type, entity_name, tag_names)
11
+ entity = fetch_tag_entity(entity_type, entity_name)
12
+ normalized = normalize_tags(tag_names)
13
+ changed = []
14
+
15
+ db.transaction do
16
+ normalized.each do |tag_name|
17
+ tag = inventory_context.find_or_create_tag(tag_name)
18
+ next unless entity.tags_dataset[name: tag_name].nil?
19
+
20
+ entity.add_tag(tag)
21
+ changed << tag_name
22
+ end
23
+ end
24
+
25
+ result = tag_result(events: changed.map { |tag| tag_event(:tag_added, entity_type, entity_name, tag) })
26
+ record_tag_audit(command: 'addtag', action: 'add_tag', entity_type: entity_type,
27
+ entity_name: entity_name, result: result, changed: changed)
28
+ puts "Added #{entity_type} tag(s) to '#{entity_name}': #{changed.join(', ')}."
29
+ end
30
+
31
+ def remove_tags(entity_type, entity_name, tag_names)
32
+ entity = fetch_tag_entity(entity_type, entity_name)
33
+ normalized = normalize_tags(tag_names)
34
+ changed = []
35
+
36
+ db.transaction do
37
+ normalized.each do |tag_name|
38
+ tag = entity.tags_dataset[name: tag_name]
39
+ next if tag.nil?
40
+
41
+ entity.remove_tag(tag)
42
+ changed << tag_name
43
+ end
44
+ end
45
+
46
+ result = tag_result(events: changed.map { |tag| tag_event(:tag_removed, entity_type, entity_name, tag) })
47
+ record_tag_audit(command: 'rmtag', action: 'remove_tag', entity_type: entity_type,
48
+ entity_name: entity_name, result: result, changed: changed)
49
+ puts "Removed #{entity_type} tag(s) from '#{entity_name}': #{changed.join(', ')}."
50
+ end
51
+
52
+ def list_tags(entity_type, entity_name)
53
+ entity = fetch_tag_entity(entity_type, entity_name)
54
+ tags = entity.tags_dataset.order(:name).map(:name)
55
+
56
+ if options[:format]
57
+ fmt.dump({ entity_type => entity_name, tags: tags }, options[:format].downcase)
58
+ elsif tags.empty?
59
+ puts "#{entity_type.capitalize} '#{entity_name}' has no tags."
60
+ else
61
+ puts "#{entity_type.capitalize} '#{entity_name}' tags: #{tags.join(', ')}"
62
+ end
63
+ end
64
+
65
+ def fetch_tag_entity(entity_type, entity_name)
66
+ entity = inventory_context.public_send("find_#{entity_type}", entity_name)
67
+ abort("ERROR: The #{entity_type} '#{entity_name}' does not exist.") if entity.nil?
68
+
69
+ entity
70
+ end
71
+
72
+ def normalize_tags(values)
73
+ inventory_context.normalize_tag_names(values)
74
+ end
75
+
76
+ def tag_result(events:)
77
+ Moose::Inventory::Operations::OperationEventSupport::Result.new(events: events, warning_count: 0)
78
+ end
79
+
80
+ def tag_event(type, entity_type, entity_name, tag_name)
81
+ Moose::Inventory::Operations::OperationEventSupport::Event.new(
82
+ type: type,
83
+ payload: { entity_type: entity_type, entity_name: entity_name, tag: tag_name }
84
+ )
85
+ end
86
+
87
+ def record_tag_audit(metadata)
88
+ return if metadata.fetch(:changed).empty?
89
+
90
+ record_audit({ command: "#{metadata.fetch(:entity_type)} #{metadata.fetch(:command)}",
91
+ action: metadata.fetch(:action), entity_type: metadata.fetch(:entity_type),
92
+ entity_names: metadata.fetch(:entity_name) }, result: metadata.fetch(:result))
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moose
4
+ module Inventory
5
+ module Cli
6
+ # Shared rendering helpers for variable add/remove commands.
7
+ module VariableRendering
8
+ private
9
+
10
+ def variable_operation_emitter(action:, entity_label:, entity_name:, variables_label:)
11
+ lambda do |event|
12
+ render_variable_event(
13
+ event,
14
+ action: action,
15
+ entity_label: entity_label,
16
+ entity_name: entity_name,
17
+ variables_label: variables_label
18
+ )
19
+ end
20
+ end
21
+
22
+ def render_variable_event(event, action:, entity_label:, entity_name:, variables_label:)
23
+ if event.type == :entity_started
24
+ return puts(variable_operation_heading(action:, entity_label:, entity_name:,
25
+ variables_label:))
26
+ end
27
+ return render_variable_change(event, entity_label) if variable_change_event?(event.type)
28
+ return puts 'Dry run complete. No changes applied.' if event.type == :dry_run_summary
29
+
30
+ render_variable_status(event)
31
+ end
32
+
33
+ def variable_operation_heading(action:, entity_label:, entity_name:, variables_label:)
34
+ return "Add variables '#{variables_label}' to #{entity_label} '#{entity_name}':" if action == :add
35
+
36
+ "Remove variable(s) '#{variables_label}' from #{entity_label} '#{entity_name}':"
37
+ end
38
+
39
+ def variable_change_event?(type)
40
+ %i[retrieving_entity adding_variable removing_variable updating_existing_variable].include?(type)
41
+ end
42
+
43
+ def render_variable_change(event, entity_label)
44
+ case event.type
45
+ when :retrieving_entity
46
+ fmt.puts 2, "- retrieve #{entity_label} '#{event.payload[:name]}'..."
47
+ when :adding_variable
48
+ fmt.puts 2, "- add variable '#{event.payload[:variable]}'..."
49
+ when :removing_variable
50
+ fmt.puts 2, "- remove variable '#{event.payload[:variable]}'..."
51
+ when :updating_existing_variable
52
+ fmt.puts 4, '- already exists, applying as an update...'
53
+ end
54
+ end
55
+
56
+ def render_variable_status(event)
57
+ case event.type
58
+ when :entity_complete
59
+ fmt.puts 2, '- all OK'
60
+ when :ok
61
+ fmt.puts event.payload[:indent], '- OK'
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end