moose-inventory 2.0 → 2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +2 -0
  3. data/.gitignore +2 -1
  4. data/.rubocop.yml +21 -0
  5. data/BACKLOG.md +630 -8
  6. data/Gemfile +2 -0
  7. data/Gemfile.lock +1 -1
  8. data/README.md +315 -39
  9. data/Rakefile +2 -0
  10. data/bin/moose-inventory +2 -1
  11. data/docs/architecture/architecture-and-trust-boundaries.md +444 -0
  12. data/docs/compatibility/cli-output-compatibility.md +76 -0
  13. data/docs/governance/approval-register.md +37 -0
  14. data/docs/maintenance/database-backup-restore-guidance.md +162 -0
  15. data/docs/maintenance/package-maintenance-and-agent-boundaries.md +260 -0
  16. data/docs/process/conformance-gap-analysis-2026-05-28.md +192 -0
  17. data/docs/product/product-brief.md +161 -0
  18. data/docs/product/requirements-baseline.md +477 -0
  19. data/docs/qa/qa-documentation-and-release-gates.md +283 -0
  20. data/docs/release/package-provenance-hardening.md +126 -0
  21. data/docs/release/publishing.md +11 -3
  22. data/docs/release/release-environment-protection.md +70 -0
  23. data/docs/release/release-readiness.md +23 -4
  24. data/docs/security/accepted-risk-register.md +84 -0
  25. data/docs/security/security-privacy-process.md +287 -0
  26. data/docs/security-audit-2026-05-26-rerun.md +2 -2
  27. data/docs/ux/cli-workflow-notes.md +287 -0
  28. data/examples/ansible/ansible.cfg +3 -0
  29. data/examples/ansible/inventory/moose_inventory.yml +5 -0
  30. data/examples/ansible/inventory_plugins/moose_inventory.py +100 -0
  31. data/examples/ci/README.md +16 -0
  32. data/examples/ci/github-actions/inventory-review.yml +38 -0
  33. data/examples/ci/inventory/example-snapshot.yml +19 -0
  34. data/examples/ci/scripts/validate-inventory-snapshot.sh +30 -0
  35. data/lib/moose_inventory/cli/application.rb +133 -5
  36. data/lib/moose_inventory/cli/association_rendering.rb +74 -0
  37. data/lib/moose_inventory/cli/association_rendering_support.rb +89 -0
  38. data/lib/moose_inventory/cli/audit.rb +62 -0
  39. data/lib/moose_inventory/cli/audit_recording.rb +40 -0
  40. data/lib/moose_inventory/cli/child_relation_rendering.rb +110 -0
  41. data/lib/moose_inventory/cli/console.rb +135 -0
  42. data/lib/moose_inventory/cli/db.rb +64 -0
  43. data/lib/moose_inventory/cli/factory.rb +28 -0
  44. data/lib/moose_inventory/cli/formatter.rb +8 -12
  45. data/lib/moose_inventory/cli/group.rb +5 -2
  46. data/lib/moose_inventory/cli/group_add.rb +11 -9
  47. data/lib/moose_inventory/cli/group_addchild.rb +23 -65
  48. data/lib/moose_inventory/cli/group_addhost.rb +16 -67
  49. data/lib/moose_inventory/cli/group_addvar.rb +27 -47
  50. data/lib/moose_inventory/cli/group_get.rb +8 -42
  51. data/lib/moose_inventory/cli/group_list.rb +7 -40
  52. data/lib/moose_inventory/cli/group_listvars.rb +9 -55
  53. data/lib/moose_inventory/cli/group_rm.rb +12 -10
  54. data/lib/moose_inventory/cli/group_rmchild.rb +26 -82
  55. data/lib/moose_inventory/cli/group_rmhost.rb +18 -53
  56. data/lib/moose_inventory/cli/group_rmvar.rb +30 -41
  57. data/lib/moose_inventory/cli/group_tags.rb +33 -0
  58. data/lib/moose_inventory/cli/helpers.rb +68 -1
  59. data/lib/moose_inventory/cli/host.rb +6 -3
  60. data/lib/moose_inventory/cli/host_add.rb +69 -29
  61. data/lib/moose_inventory/cli/host_addgroup.rb +22 -58
  62. data/lib/moose_inventory/cli/host_addvar.rb +28 -52
  63. data/lib/moose_inventory/cli/host_get.rb +9 -37
  64. data/lib/moose_inventory/cli/host_list.rb +24 -21
  65. data/lib/moose_inventory/cli/host_listvars.rb +9 -62
  66. data/lib/moose_inventory/cli/host_rm.rb +60 -42
  67. data/lib/moose_inventory/cli/host_rmgroup.rb +25 -44
  68. data/lib/moose_inventory/cli/host_rmvar.rb +31 -45
  69. data/lib/moose_inventory/cli/host_tags.rb +33 -0
  70. data/lib/moose_inventory/cli/listvars_support.rb +55 -0
  71. data/lib/moose_inventory/cli/plan_rendering.rb +50 -0
  72. data/lib/moose_inventory/cli/relation_transaction_support.rb +51 -0
  73. data/lib/moose_inventory/cli/tag_support.rb +97 -0
  74. data/lib/moose_inventory/cli/variable_rendering.rb +67 -0
  75. data/lib/moose_inventory/config/config.rb +185 -108
  76. data/lib/moose_inventory/db/db.rb +170 -195
  77. data/lib/moose_inventory/db/exceptions.rb +6 -3
  78. data/lib/moose_inventory/db/models.rb +16 -0
  79. data/lib/moose_inventory/db/schema_migrations.rb +248 -0
  80. data/lib/moose_inventory/inventory_context.rb +68 -2
  81. data/lib/moose_inventory/operations/add_associations.rb +20 -16
  82. data/lib/moose_inventory/operations/add_groups.rb +21 -13
  83. data/lib/moose_inventory/operations/add_hosts.rb +30 -17
  84. data/lib/moose_inventory/operations/add_variables.rb +77 -0
  85. data/lib/moose_inventory/operations/entity_variable_operation_support.rb +46 -0
  86. data/lib/moose_inventory/operations/group_child_relations.rb +23 -16
  87. data/lib/moose_inventory/operations/group_cleanup.rb +23 -8
  88. data/lib/moose_inventory/operations/import_inventory_snapshot.rb +41 -0
  89. data/lib/moose_inventory/operations/inventory_doctor.rb +172 -0
  90. data/lib/moose_inventory/operations/inventory_snapshot.rb +60 -0
  91. data/lib/moose_inventory/operations/inventory_snapshot_applier.rb +112 -0
  92. data/lib/moose_inventory/operations/inventory_snapshot_preview.rb +174 -0
  93. data/lib/moose_inventory/operations/inventory_snapshot_validator.rb +134 -0
  94. data/lib/moose_inventory/operations/operation_event_support.rb +27 -0
  95. data/lib/moose_inventory/operations/query_inventory/base_query.rb +24 -0
  96. data/lib/moose_inventory/operations/query_inventory/group_queries.rb +86 -0
  97. data/lib/moose_inventory/operations/query_inventory/host_queries.rb +106 -0
  98. data/lib/moose_inventory/operations/query_inventory.rb +47 -0
  99. data/lib/moose_inventory/operations/remove_associations.rb +30 -18
  100. data/lib/moose_inventory/operations/remove_groups.rb +12 -12
  101. data/lib/moose_inventory/operations/remove_hosts.rb +68 -0
  102. data/lib/moose_inventory/operations/remove_variables.rb +67 -0
  103. data/lib/moose_inventory/runtime_options.rb +31 -0
  104. data/lib/moose_inventory/version.rb +3 -1
  105. data/lib/moose_inventory.rb +10 -7
  106. data/moose-inventory.gemspec +19 -35
  107. data/scripts/check.sh +1 -0
  108. data/scripts/ci/check_generated_artifacts.sh +41 -0
  109. data/scripts/ci/check_permissions.sh +2 -0
  110. data/scripts/ci/check_rubocop.sh +30 -25
  111. data/scripts/files.rb +5 -4
  112. data/spec/examples/ci_examples_spec.rb +37 -0
  113. data/spec/lib/moose_inventory/ansible_plugin_examples_spec.rb +29 -0
  114. data/spec/lib/moose_inventory/cli/application_doctor_spec.rb +50 -0
  115. data/spec/lib/moose_inventory/cli/application_import_export_spec.rb +100 -0
  116. data/spec/lib/moose_inventory/cli/application_spec.rb +25 -15
  117. data/spec/lib/moose_inventory/cli/audit_spec.rb +56 -0
  118. data/spec/lib/moose_inventory/cli/cli_spec.rb +15 -19
  119. data/spec/lib/moose_inventory/cli/console_spec.rb +98 -0
  120. data/spec/lib/moose_inventory/cli/factory_spec.rb +27 -0
  121. data/spec/lib/moose_inventory/cli/formatter_spec.rb +95 -3
  122. data/spec/lib/moose_inventory/cli/group_add_spec.rb +140 -116
  123. data/spec/lib/moose_inventory/cli/group_addchild_spec.rb +89 -35
  124. data/spec/lib/moose_inventory/cli/group_addhost_spec.rb +81 -84
  125. data/spec/lib/moose_inventory/cli/group_addvar_spec.rb +65 -68
  126. data/spec/lib/moose_inventory/cli/group_get_spec.rb +17 -33
  127. data/spec/lib/moose_inventory/cli/group_list_spec.rb +16 -38
  128. data/spec/lib/moose_inventory/cli/group_listvar_spec.rb +33 -40
  129. data/spec/lib/moose_inventory/cli/group_rm_spec.rb +136 -96
  130. data/spec/lib/moose_inventory/cli/group_rmchild_spec.rb +66 -41
  131. data/spec/lib/moose_inventory/cli/group_rmhost_spec.rb +76 -78
  132. data/spec/lib/moose_inventory/cli/group_rmvar_spec.rb +57 -63
  133. data/spec/lib/moose_inventory/cli/group_spec.rb +2 -0
  134. data/spec/lib/moose_inventory/cli/helpers_spec.rb +146 -0
  135. data/spec/lib/moose_inventory/cli/host_add_spec.rb +170 -116
  136. data/spec/lib/moose_inventory/cli/host_addgroup_spec.rb +100 -83
  137. data/spec/lib/moose_inventory/cli/host_addvar_spec.rb +92 -74
  138. data/spec/lib/moose_inventory/cli/host_get_spec.rb +14 -33
  139. data/spec/lib/moose_inventory/cli/host_list_spec.rb +41 -33
  140. data/spec/lib/moose_inventory/cli/host_listvar_spec.rb +45 -53
  141. data/spec/lib/moose_inventory/cli/host_rm_spec.rb +66 -48
  142. data/spec/lib/moose_inventory/cli/host_rmgroup_spec.rb +73 -83
  143. data/spec/lib/moose_inventory/cli/host_rmvar_spec.rb +56 -63
  144. data/spec/lib/moose_inventory/cli/host_spec.rb +2 -0
  145. data/spec/lib/moose_inventory/cli/tags_spec.rb +81 -0
  146. data/spec/lib/moose_inventory/config/config_spec.rb +41 -3
  147. data/spec/lib/moose_inventory/db/db_spec.rb +396 -36
  148. data/spec/lib/moose_inventory/db/exceptions_spec.rb +18 -0
  149. data/spec/lib/moose_inventory/db/models_spec.rb +7 -3
  150. data/spec/lib/moose_inventory/db_lifecycle_spec.rb +73 -0
  151. data/spec/lib/moose_inventory/inventory_context_spec.rb +10 -0
  152. data/spec/lib/moose_inventory/operations/add_associations_spec.rb +34 -0
  153. data/spec/lib/moose_inventory/operations/add_groups_spec.rb +15 -0
  154. data/spec/lib/moose_inventory/operations/add_hosts_spec.rb +13 -0
  155. data/spec/lib/moose_inventory/operations/add_variables_spec.rb +103 -0
  156. data/spec/lib/moose_inventory/operations/group_child_relations_spec.rb +46 -0
  157. data/spec/lib/moose_inventory/operations/import_inventory_snapshot_spec.rb +226 -0
  158. data/spec/lib/moose_inventory/operations/inventory_doctor_spec.rb +77 -0
  159. data/spec/lib/moose_inventory/operations/inventory_snapshot_spec.rb +50 -0
  160. data/spec/lib/moose_inventory/operations/operation_event_support_spec.rb +78 -0
  161. data/spec/lib/moose_inventory/operations/query_inventory_spec.rb +146 -0
  162. data/spec/lib/moose_inventory/operations/remove_associations_spec.rb +35 -0
  163. data/spec/lib/moose_inventory/operations/remove_groups_spec.rb +21 -0
  164. data/spec/lib/moose_inventory/operations/remove_hosts_spec.rb +55 -0
  165. data/spec/lib/moose_inventory/operations/remove_variables_spec.rb +83 -0
  166. data/spec/shared/shared_config_setup.rb +4 -3
  167. data/spec/spec_helper.rb +50 -40
  168. data/spec/support/cli_harness.rb +33 -0
  169. metadata +80 -41
@@ -0,0 +1,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
@@ -1,7 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Configuration
2
4
 
3
5
  require 'yaml'
4
6
 
7
+ require_relative '../runtime_options'
8
+
5
9
  module Moose
6
10
  module Inventory
7
11
  ##
@@ -11,16 +15,16 @@ module Moose
11
15
  extend self
12
16
  # rubocop:enable Style/ModuleFunction
13
17
 
14
- @_argv = []
18
+ @_argv = []
15
19
  @_confopts = {}
16
20
  @_settings = {}
21
+ @runtime_options = nil
17
22
 
18
- attr_reader :_argv
19
- attr_reader :_confopts
20
- attr_reader :_settings
23
+ attr_reader :_argv, :_confopts, :_settings
21
24
 
22
25
  #----------------------
23
26
  def self.init(args)
27
+ reset_runtime_state
24
28
  @_argv = args.dup
25
29
 
26
30
  top_level_help
@@ -28,6 +32,46 @@ module Moose
28
32
  ansible_args
29
33
  resolve_config_file
30
34
  load
35
+ refresh_runtime_options
36
+ end
37
+
38
+ def self.reset_runtime_state
39
+ @_argv = []
40
+ @_confopts = default_confopts
41
+ @_settings = {}
42
+ @runtime_options = nil
43
+ end
44
+
45
+ def self.default_confopts
46
+ { env: '', format: 'json', ansible: false, trace: false }
47
+ end
48
+
49
+ def self.runtime_options
50
+ @runtime_options ||= build_runtime_options
51
+ end
52
+
53
+ def self.application_args
54
+ runtime_options.argv
55
+ end
56
+
57
+ def self.output_format
58
+ runtime_options.output_format
59
+ end
60
+
61
+ def self.ansible?
62
+ runtime_options.ansible? || @_confopts[:ansible] == true
63
+ end
64
+
65
+ def self.trace_enabled?
66
+ runtime_options.trace? || @_confopts[:trace] == true
67
+ end
68
+
69
+ def self.db_settings
70
+ @_settings[:config][:db]
71
+ end
72
+
73
+ def self._runtime_options
74
+ @runtime_options
31
75
  end
32
76
 
33
77
  #----------------------
@@ -51,50 +95,26 @@ module Moose
51
95
  # -- trace => Enable more complete exceptions for db transactions
52
96
  # Default is not to trace.
53
97
 
54
- @_confopts = { env: '', format: 'json', ansible: false, trace: false }
55
-
56
- # Check for two-part flags
57
- %w(config env format).each do |var|
58
- @_argv.each_with_index do |val, index|
59
- next if val != "--#{var}"
60
- @_confopts[var.to_sym] = @_argv[index + 1]
61
- 1.downto(0) { |offset| @_argv.delete_at(index + offset) }
62
- break
63
- end
64
- end
65
-
66
- # Check for one-part flags
67
- %w(ansible trace).each do |var|
68
- @_argv.each_with_index do |val, index|
69
- next if val != "--#{var}"
70
- @_confopts[var.to_sym] = true
71
- @_argv.delete_at(index)
72
- break
73
- end
74
- end
75
-
76
- # Sanity
77
- # - Ansible output format must be json - pjson is permitted, but yaml is not.
78
- if @_confopts[:ansible] == true
79
- unless @_confopts[:format] =~ /p|pjson|j|json/
80
- @_confopts[:format] = 'json'
81
- end
82
- end
98
+ extract_value_flags(%w[config env format])
99
+ extract_boolean_flags(%w[ansible trace])
100
+ normalize_ansible_format!
83
101
  end
84
102
 
85
103
  #----------------------
86
104
  def self.top_level_help
87
- if @_argv[0] == 'help'
88
- puts 'Global flags:'
89
- printf ' %-31s %-10s', '--ansible', "# Force Ansible mode (automatically set when using ansible flags)\n"
90
- printf ' %-31s %-10s', '--config FILE', "# Specifies a configuration file to use\n"
91
- printf ' %-31s %-10s', '--env ENV', "# Specifies the environment section of the config to use\n"
92
- printf ' %-31s %-10s', '--format yaml|json|pjson', "# Format for the output of 'get', 'list', and 'listvars' subcommands\n"
93
- printf ' %-31s %-10s', '--trace', "# Enable more complete exception dumps for database transactions\n"
94
- puts "\nAnsible flags:"
95
- printf ' %-31s %-10s', '--host HOSTNAME', "# Retrieves host variables for the specified host (alias for 'host listvars HOSTNAME')\n"
96
- printf ' %-31s %-10s', '--list', "# Retrieves the list of groups (alias for 'group list')\n\n"
97
- end
105
+ return unless @_argv[0] == 'help'
106
+
107
+ puts 'Global flags:'
108
+ printf ' %-31s %-10s', '--ansible', "# Force Ansible mode (automatically set when using ansible flags)\n"
109
+ printf ' %-31s %-10s', '--config FILE', "# Specifies a configuration file to use\n"
110
+ printf ' %-31s %-10s', '--env ENV', "# Specifies the environment section of the config to use\n"
111
+ printf ' %-31s %-10s', '--format yaml|json|pjson',
112
+ "# Format for the output of 'get', 'list', and 'listvars' subcommands\n"
113
+ printf ' %-31s %-10s', '--trace', "# Enable more complete exception dumps for database transactions\n"
114
+ puts "\nAnsible flags:"
115
+ printf ' %-31s %-10s', '--host HOSTNAME',
116
+ "# Retrieves host variables for the specified host (alias for 'host listvars HOSTNAME')\n"
117
+ printf ' %-31s %-10s', '--list', "# Retrieves the list of groups (alias for 'group list')\n\n"
98
118
  end
99
119
 
100
120
  #----------------------
@@ -108,95 +128,152 @@ module Moose
108
128
 
109
129
  case @_argv[0]
110
130
  when '--list'
111
- @_confopts[:ansible] = true
112
- @_confopts[:format] = 'json' unless @_confopts[:format] =~ /p|pjson|j|json/
113
- @_argv.clear
114
- @_argv.concat(%w(group list)).flatten
131
+ apply_ansible_alias!(%w[group list])
115
132
  when '--host'
116
- @_confopts[:ansible] = true
117
- @_confopts[:format] = 'json' unless @_confopts[:format] =~ /p|pjson|j|json/
118
133
  host = @_argv[1]
119
- @_argv.clear
120
- @_argv.concat(['host', 'listvars', host.to_s]).flatten
134
+ apply_ansible_alias!(['host', 'listvars', host.to_s])
121
135
  end
122
136
  end
123
137
 
124
138
  #----------------------
125
139
  def self.resolve_config_file
126
- if !@_confopts[:config].nil?
127
- path = File.expand_path(@_confopts[:config])
128
- if File.exist?(path)
129
- @_confopts[:config] = path
130
- else
131
- fail("The configuration file #{path} does not exist")
132
- end
133
- else
134
- possibles = ['./.moose-tools/inventory/config',
135
- '~/.moose-tools/inventory/config',
136
- '~/local/etc/moose-tools/inventory/config',
137
- '/etc/moose-tools/inventory/config']
138
- possibles.each do |f|
139
- file = File.expand_path(f)
140
- @_confopts[:config] = file if File.exist?(file)
141
- end
142
- end
140
+ explicit_path = @_confopts[:config]
141
+ @_confopts[:config] = if explicit_path.nil?
142
+ find_default_config_file
143
+ else
144
+ validated_config_path(explicit_path)
145
+ end
143
146
 
144
- if @_confopts[:config].nil?
145
- fail('No configuration either given or found in standard locations.')
146
- end
147
+ raise('No configuration either given or found in standard locations.') if @_confopts[:config].nil?
147
148
  end
148
149
 
149
150
  #----------------------
150
151
  def self.symbolize_keys(hash)
151
- # rubocop:disable Style/EachWithObject
152
- hash.inject({}) do |result, (key, value)|
153
- new_key = case key
154
- when String then key.to_sym
155
- else key
156
- end
157
- new_value = case value
158
- when Hash then symbolize_keys(value)
159
- else value
160
- end
161
- result[new_key] = new_value
162
- result
152
+ hash.each_with_object({}) do |(key, value), result|
153
+ result[symbolize_key(key)] = value.is_a?(Hash) ? symbolize_keys(value) : value
163
154
  end
164
- # rubocop:enable Style/EachWithObject
165
155
  end
166
156
 
167
157
  #----------------------
168
- # rubocop:disable PerceivedComplexity
169
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize
170
158
  def self.load
171
- newsets = symbolize_keys(YAML.safe_load_file(
172
- @_confopts[:config],
173
- aliases: false,
174
- permitted_classes: [],
175
- permitted_symbols: []
176
- ))
177
-
159
+ newsets = load_config_file(@_confopts[:config])
178
160
  path = @_confopts[:config]
179
161
 
180
- # Get the "general" section
181
- @_settings[:general] = newsets[:general]
182
- @_settings[:general].nil? && fail("Missing 'general' root in #{path}")
162
+ @_settings[:general] = fetch_general_settings(newsets, path)
163
+
164
+ env, settings = resolve_environment_settings(newsets, path)
165
+ @_settings[:config] = settings
166
+ @_settings[:config].nil? && raise("Missing '#{env}' root in #{path}")
167
+ end
168
+
169
+ def self.load_config_file(path)
170
+ symbolize_keys(YAML.safe_load_file(
171
+ path,
172
+ aliases: false,
173
+ permitted_classes: [],
174
+ permitted_symbols: []
175
+ ))
176
+ end
183
177
 
184
- # Get the config for the correct environment
178
+ def self.fetch_general_settings(newsets, path)
179
+ general = newsets[:general]
180
+ general.nil? && raise("Missing 'general' root in #{path}")
181
+ general
182
+ end
183
+
184
+ def self.resolve_environment_settings(newsets, path)
185
+ env = selected_environment(newsets, path)
186
+ [env, newsets[env.to_sym]]
187
+ end
185
188
 
186
- if @_confopts[:env] && !@_confopts[:env].empty?
187
- env = @_confopts[:env]
188
- @_settings[:config] = newsets[@_confopts[:env].to_sym]
189
- else
190
- env = @_settings[:general][:defaultenv]
191
- (env.nil? || env.empty?) && fail("No defaultenv set in #{path}")
192
- @_settings[:config] = newsets[env.to_sym]
189
+ def self.selected_environment(newsets, path)
190
+ return @_confopts[:env] if @_confopts[:env] && !@_confopts[:env].empty?
191
+
192
+ env = newsets.dig(:general, :defaultenv)
193
+ (env.nil? || env.empty?) && raise("No defaultenv set in #{path}")
194
+ env
195
+ end
196
+
197
+ def self.standard_config_paths
198
+ ['./.moose-tools/inventory/config',
199
+ '~/.moose-tools/inventory/config',
200
+ '~/local/etc/moose-tools/inventory/config',
201
+ '/etc/moose-tools/inventory/config']
202
+ end
203
+
204
+ def self.find_default_config_file
205
+ standard_config_paths.map { |path| File.expand_path(path) }.find do |path|
206
+ File.exist?(path)
193
207
  end
208
+ end
209
+
210
+ def self.validated_config_path(path)
211
+ expanded = File.expand_path(path)
212
+ raise("The configuration file #{expanded} does not exist") unless File.exist?(expanded)
213
+
214
+ expanded
215
+ end
216
+
217
+ def self.extract_value_flags(flags)
218
+ flags.each { |flag| extract_value_flag(flag) }
219
+ end
220
+
221
+ def self.extract_value_flag(flag)
222
+ index = @_argv.index("--#{flag}")
223
+ return if index.nil?
194
224
 
195
- @_settings[:config].nil? && fail("Missing '#{env}' root in #{path}")
225
+ value = @_argv[index + 1]
226
+ raise("Expected a value after --#{flag}") if value.nil? || value.start_with?('--')
227
+
228
+ @_confopts[flag.to_sym] = value
229
+ @_argv.slice!(index, 2)
230
+ end
231
+
232
+ def self.extract_boolean_flags(flags)
233
+ flags.each { |flag| extract_boolean_flag(flag) }
234
+ end
235
+
236
+ def self.extract_boolean_flag(flag)
237
+ index = @_argv.index("--#{flag}")
238
+ return if index.nil?
239
+
240
+ @_confopts[flag.to_sym] = true
241
+ @_argv.delete_at(index)
242
+ end
243
+
244
+ def self.normalize_ansible_format!
245
+ return unless @_confopts[:ansible] == true
246
+ return if @_confopts[:format] =~ /p|pjson|j|json/
247
+
248
+ @_confopts[:format] = 'json'
249
+ end
250
+
251
+ def self.apply_ansible_alias!(argv)
252
+ @_confopts[:ansible] = true
253
+ normalize_ansible_format!
254
+ @_argv = argv
255
+ end
256
+
257
+ def self.refresh_runtime_options
258
+ @runtime_options = build_runtime_options
259
+ end
260
+
261
+ def self.build_runtime_options
262
+ Moose::Inventory::RuntimeOptions.new(
263
+ argv: @_argv.dup,
264
+ config: @_confopts[:config],
265
+ env: @_confopts[:env],
266
+ format: @_confopts[:format],
267
+ flags: {
268
+ ansible: @_confopts[:ansible],
269
+ trace: @_confopts[:trace]
270
+ }
271
+ )
272
+ end
196
273
 
197
- # And now we should have a valid config stuffed into @_options[:config]
274
+ def self.symbolize_key(key)
275
+ key.is_a?(String) ? key.to_sym : key
198
276
  end
199
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/AbcSize
200
277
  end
201
278
  end
202
279
  end