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,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moose
4
+ module Inventory
5
+ module DB
6
+ # Schema definitions, ordered migrations, and schema-artifact helpers for Moose Inventory DB.
7
+ # rubocop:disable Metrics/ModuleLength
8
+ module SchemaMigrations
9
+ SCHEMA_VERSION = 4
10
+
11
+ TABLE_DEFINITIONS = {
12
+ hosts: lambda do |db|
13
+ db.create_table(:hosts) do
14
+ primary_key :id
15
+ column :name, :text, unique: true
16
+ end
17
+ end,
18
+ hostvars: lambda do |db|
19
+ db.create_table(:hostvars) do
20
+ primary_key :id
21
+ foreign_key :host_id
22
+ column :name, :text
23
+ column :value, :text
24
+ end
25
+ end,
26
+ groups: lambda do |db|
27
+ db.create_table(:groups) do
28
+ primary_key :id
29
+ column :name, :text, unique: true
30
+ end
31
+ end,
32
+ groups_groups: lambda do |db|
33
+ db.create_table(:groups_groups) do
34
+ primary_key :id
35
+ foreign_key :parent_id, :groups
36
+ foreign_key :child_id, :groups
37
+ end
38
+ end,
39
+ groupvars: lambda do |db|
40
+ db.create_table(:groupvars) do
41
+ primary_key :id
42
+ foreign_key :group_id
43
+ column :name, :text
44
+ column :value, :text
45
+ end
46
+ end,
47
+ groups_hosts: lambda do |db|
48
+ db.create_table(:groups_hosts) do
49
+ primary_key :id
50
+ foreign_key :host_id, :hosts
51
+ foreign_key :group_id, :groups
52
+ end
53
+ end,
54
+ schema_info: lambda do |db|
55
+ db.create_table(:schema_info) do
56
+ primary_key :id
57
+ column :version, :integer, null: false
58
+ end
59
+ end,
60
+ audit_events: lambda do |db|
61
+ db.create_table(:audit_events) do
62
+ primary_key :id
63
+ column :created_at, :text, null: false
64
+ column :actor, :text
65
+ column :command, :text, null: false
66
+ column :action, :text, null: false
67
+ column :entity_type, :text
68
+ column :entity_name, :text
69
+ column :details, :text
70
+ end
71
+ end,
72
+ tags: lambda do |db|
73
+ db.create_table(:tags) do
74
+ primary_key :id
75
+ column :name, :text, unique: true, null: false
76
+ end
77
+ end,
78
+ hosts_tags: lambda do |db|
79
+ db.create_table(:hosts_tags) do
80
+ primary_key :id
81
+ foreign_key :host_id, :hosts
82
+ foreign_key :tag_id, :tags
83
+ end
84
+ end,
85
+ groups_tags: lambda do |db|
86
+ db.create_table(:groups_tags) do
87
+ primary_key :id
88
+ foreign_key :group_id, :groups
89
+ foreign_key :tag_id, :tags
90
+ end
91
+ end
92
+ }.freeze
93
+
94
+ SCHEMA_MIGRATIONS = {
95
+ 1 => %i[hosts hostvars groups groups_groups groupvars groups_hosts schema_info],
96
+ 2 => %i[audit_events],
97
+ 3 => %i[tags hosts_tags groups_tags],
98
+ 4 => []
99
+ }.freeze
100
+
101
+ INDEX_DEFINITIONS = [
102
+ { table: :hostvars, columns: %i[host_id name], unique: true, name: :idx_hostvars_host_id_name },
103
+ { table: :groupvars, columns: %i[group_id name], unique: true, name: :idx_groupvars_group_id_name },
104
+ { table: :groups_hosts, columns: %i[host_id group_id], unique: true,
105
+ name: :idx_groups_hosts_host_id_group_id },
106
+ { table: :groups_groups, columns: %i[parent_id child_id], unique: true,
107
+ name: :idx_groups_groups_parent_id_child_id },
108
+ { table: :hosts_tags, columns: %i[host_id tag_id], unique: true, name: :idx_hosts_tags_host_id_tag_id },
109
+ { table: :groups_tags, columns: %i[group_id tag_id], unique: true, name: :idx_groups_tags_group_id_tag_id },
110
+ { table: :groups_hosts, columns: %i[group_id host_id], unique: false,
111
+ name: :idx_groups_hosts_group_id_host_id },
112
+ { table: :groups_groups, columns: %i[child_id parent_id], unique: false,
113
+ name: :idx_groups_groups_child_id_parent_id },
114
+ { table: :hosts_tags, columns: %i[tag_id host_id], unique: false, name: :idx_hosts_tags_tag_id_host_id },
115
+ { table: :groups_tags, columns: %i[tag_id group_id], unique: false, name: :idx_groups_tags_tag_id_group_id }
116
+ ].freeze
117
+
118
+ def migration_versions
119
+ SCHEMA_MIGRATIONS.keys.sort
120
+ end
121
+
122
+ def schema_version
123
+ return nil unless @db.table_exists?(:schema_info)
124
+
125
+ @db[:schema_info].order(:id).last&.fetch(:version)
126
+ end
127
+
128
+ def migrate_schema!
129
+ reject_future_schema!
130
+ migration_versions.each do |version|
131
+ next if schema_version.to_i >= version && !schema_migration_artifacts_missing?(version)
132
+
133
+ apply_schema_migration!(version)
134
+ end
135
+ end
136
+
137
+ def schema_migration_artifacts_missing?(version)
138
+ schema_migration_tables_missing?(version) || (version == 4 && schema_indexes_missing?)
139
+ end
140
+
141
+ def schema_migration_tables_missing?(version)
142
+ SCHEMA_MIGRATIONS.fetch(version).any? { |table_name| !@db.table_exists?(table_name) }
143
+ end
144
+
145
+ def schema_indexes_missing?
146
+ INDEX_DEFINITIONS.any? do |definition|
147
+ !@db.table_exists?(definition.fetch(:table)) || !index_exists?(definition.fetch(:table),
148
+ definition.fetch(:name))
149
+ end
150
+ end
151
+
152
+ def apply_schema_migration!(version)
153
+ tables = SCHEMA_MIGRATIONS.fetch(version)
154
+ tables.each { |table_name| create_table(table_name) }
155
+ apply_schema_indexes! if version == 4
156
+ record_schema_version!(version)
157
+ end
158
+
159
+ def apply_schema_indexes!
160
+ clean_duplicate_index_rows!
161
+ INDEX_DEFINITIONS.each { |definition| add_index(definition) }
162
+ end
163
+
164
+ def clean_duplicate_index_rows!
165
+ dedupe_duplicate_rows!(:hostvars, %i[host_id name], value_columns: [:value])
166
+ dedupe_duplicate_rows!(:groupvars, %i[group_id name], value_columns: [:value])
167
+ dedupe_duplicate_rows!(:groups_hosts, %i[host_id group_id])
168
+ dedupe_duplicate_rows!(:groups_groups, %i[parent_id child_id])
169
+ dedupe_duplicate_rows!(:hosts_tags, %i[host_id tag_id])
170
+ dedupe_duplicate_rows!(:groups_tags, %i[group_id tag_id])
171
+ end
172
+
173
+ def dedupe_duplicate_rows!(table_name, columns, value_columns: [])
174
+ duplicate_keys(table_name, columns).each do |key|
175
+ rows = @db[table_name].where(key).order(:id).all
176
+ reject_conflicting_duplicates!(table_name, key, rows, value_columns)
177
+ @db[table_name].where(id: rows.drop(1).map { |row| row.fetch(:id) }).delete
178
+ end
179
+ end
180
+
181
+ def duplicate_keys(table_name, columns)
182
+ @db[table_name]
183
+ .select(*columns)
184
+ .group(*columns)
185
+ .having { count(id) > 1 }
186
+ .all
187
+ end
188
+
189
+ def reject_conflicting_duplicates!(table_name, key, rows, value_columns)
190
+ conflicts = value_columns.any? do |column|
191
+ rows.map { |row| row[column] }.uniq.length > 1
192
+ end
193
+ return unless conflicts
194
+
195
+ raise @exceptions[:moose], "Cannot add unique indexes because #{table_name} has conflicting duplicates " \
196
+ "for #{key}. Resolve duplicate values before migrating."
197
+ end
198
+
199
+ def add_index(definition)
200
+ return if index_exists?(definition.fetch(:table), definition.fetch(:name))
201
+
202
+ @db.add_index(definition.fetch(:table), definition.fetch(:columns), unique: definition.fetch(:unique),
203
+ name: definition.fetch(:name))
204
+ end
205
+
206
+ def index_exists?(table_name, index_name)
207
+ @db.indexes(table_name).key?(index_name)
208
+ end
209
+
210
+ def record_schema_version!(version)
211
+ unless @db.table_exists?(:schema_info)
212
+ raise @exceptions[:moose],
213
+ 'Cannot record schema version before schema_info exists.'
214
+ end
215
+
216
+ if @db[:schema_info].empty?
217
+ @db[:schema_info].insert(version: version)
218
+ else
219
+ @db[:schema_info].update(version: version)
220
+ end
221
+ end
222
+
223
+ def reject_future_schema!
224
+ return unless @db.table_exists?(:schema_info)
225
+
226
+ current_version = schema_version
227
+ return if current_version.nil? || current_version <= SCHEMA_VERSION
228
+
229
+ raise @exceptions[:moose], "Database schema version #{current_version} is newer than supported version " \
230
+ "#{SCHEMA_VERSION}. Upgrade moose-inventory before using this database."
231
+ end
232
+
233
+ def create_tables
234
+ TABLE_DEFINITIONS.each do |table_name, definition|
235
+ create_table(table_name, definition)
236
+ end
237
+ end
238
+
239
+ def create_table(table_name, definition = TABLE_DEFINITIONS.fetch(table_name))
240
+ return if @db.table_exists?(table_name)
241
+
242
+ definition.call(@db)
243
+ end
244
+ end
245
+ # rubocop:enable Metrics/ModuleLength
246
+ end
247
+ end
248
+ end
@@ -1,16 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'time'
4
+
3
5
  module Moose
4
6
  module Inventory
5
7
  ##
6
- # Thin facade over the current DB singleton.
8
+ # Thin facade over an explicitly supplied DB implementation.
7
9
  #
8
10
  # This gives new operation/service objects a small inventory-facing seam
9
11
  # without forcing the legacy CLI to stop using the DB singleton all at once.
10
12
  class InventoryContext
11
13
  AUTOMATIC_GROUP = 'ungrouped'
12
14
 
13
- def initialize(db: Moose::Inventory::DB)
15
+ def initialize(db:)
14
16
  @db = db
15
17
  end
16
18
 
@@ -38,12 +40,76 @@ module Moose
38
40
  db.models[:group].find_or_create(name: name)
39
41
  end
40
42
 
43
+ def find_tag(name)
44
+ db.models[:tag].find(name: normalize_tag_name(name))
45
+ end
46
+
47
+ def find_or_create_tag(name)
48
+ db.models[:tag].find_or_create(name: normalize_tag_name(name))
49
+ end
50
+
51
+ def normalize_tag_name(name)
52
+ name.to_s.downcase.strip
53
+ end
54
+
55
+ def normalize_tag_names(names)
56
+ names.map { |name| normalize_tag_name(name) }.reject(&:empty?).uniq
57
+ end
58
+
59
+ def hosts_dataset
60
+ db.models[:host].dataset
61
+ end
62
+
63
+ def db_dataset(table_name)
64
+ db.db[table_name]
65
+ end
66
+
41
67
  def automatic_group
42
68
  find_or_create_group(AUTOMATIC_GROUP)
43
69
  end
44
70
 
71
+ def find_variable(entity_type, id)
72
+ db.models[variable_model_key(entity_type)].find(id: id)
73
+ end
74
+
75
+ def create_variable(entity_type, name:, value:)
76
+ db.models[variable_model_key(entity_type)].create(name: name, value: value)
77
+ end
78
+
79
+ def all_hosts
80
+ db.models[:host].all
81
+ end
82
+
83
+ def all_groups
84
+ db.models[:group].all
85
+ end
86
+
87
+ def record_audit_event(attributes)
88
+ db.models[:audit_event].create(
89
+ created_at: Time.now.utc.iso8601,
90
+ actor: attributes[:actor],
91
+ command: attributes.fetch(:command),
92
+ action: attributes.fetch(:action),
93
+ entity_type: attributes[:entity_type],
94
+ entity_name: attributes[:entity_name],
95
+ details: attributes[:details]
96
+ )
97
+ end
98
+
99
+ def audit_events(limit: 20)
100
+ db.models[:audit_event].reverse_order(:id).limit(limit).all
101
+ end
102
+
103
+ def moose_exception_class
104
+ db.exceptions[:moose]
105
+ end
106
+
45
107
  private
46
108
 
109
+ def variable_model_key(entity_type)
110
+ :"#{entity_type}var"
111
+ end
112
+
47
113
  attr_reader :db
48
114
  end
49
115
  end
@@ -1,21 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'operation_event_support'
4
+
3
5
  module Moose
4
6
  module Inventory
5
7
  module Operations
6
8
  # Adds host/group associations for existing primary entities.
7
9
  class AddAssociations
8
10
  AUTOMATIC_GROUP = 'ungrouped'
9
- Event = Struct.new(:type, :payload, keyword_init: true)
10
- Result = Struct.new(:events, :warning_count, keyword_init: true)
11
+ include OperationEventSupport
11
12
 
12
13
  def initialize(context:)
13
14
  @context = context
14
15
  end
15
16
 
16
- def host_to_groups(host:, host_name:, group_names:)
17
+ def host_to_groups(host:, host_name:, group_names:, dry_run: false)
17
18
  events = []
18
19
  warning_count = 0
20
+ @dry_run = dry_run
19
21
 
20
22
  group_names.each do |group_name|
21
23
  next if group_name.nil? || group_name.empty?
@@ -24,13 +26,15 @@ module Moose
24
26
  end
25
27
 
26
28
  remove_automatic_group_from_host(host, host_name, events)
29
+ emit(events, :dry_run_summary) if dry_run
27
30
 
28
- Result.new(events: events, warning_count: warning_count)
31
+ operation_result(events: events, warning_count: warning_count)
29
32
  end
30
33
 
31
- def group_to_hosts(group:, group_name:, host_names:)
34
+ def group_to_hosts(group:, group_name:, host_names:, dry_run: false)
32
35
  events = []
33
36
  warning_count = 0
37
+ @dry_run = dry_run
34
38
  hosts_dataset = group.hosts_dataset
35
39
 
36
40
  host_names.each do |host_name|
@@ -45,12 +49,14 @@ module Moose
45
49
  )
46
50
  end
47
51
 
48
- Result.new(events: events, warning_count: warning_count)
52
+ emit(events, :dry_run_summary) if dry_run
53
+
54
+ operation_result(events: events, warning_count: warning_count)
49
55
  end
50
56
 
51
57
  private
52
58
 
53
- attr_reader :context
59
+ attr_reader :context, :dry_run
54
60
 
55
61
  def add_group_to_host(host, host_name, group_name, events)
56
62
  warning_count = 0
@@ -69,12 +75,12 @@ module Moose
69
75
  if group.nil?
70
76
  emit(events, :group_missing_created, name: group_name)
71
77
  emit(events, :group_creating_now, name: group_name)
72
- group = context.create_group(group_name)
78
+ group = context.create_group(group_name) unless dry_run
73
79
  emit(events, :ok, indent: 6)
74
80
  warning_count += 1
75
81
  end
76
82
 
77
- host.add_group(group)
83
+ host.add_group(group) unless dry_run
78
84
  emit(events, :ok, indent: 4)
79
85
  warning_count
80
86
  end
@@ -94,33 +100,31 @@ module Moose
94
100
  if host.nil?
95
101
  emit(events, :host_missing_created, name: host_name)
96
102
  emit(events, :host_creating_now, name: host_name)
97
- host = context.create_host(host_name)
103
+ host = context.create_host(host_name) unless dry_run
98
104
  emit(events, :ok, indent: 6)
99
105
  warning_count += 1
100
106
  end
101
107
 
102
- group.add_host(host)
108
+ group.add_host(host) unless dry_run
103
109
  emit(events, :ok, indent: 4)
104
110
  remove_automatic_group_from_host(host, host_name, events)
105
111
  warning_count
106
112
  end
107
113
 
108
114
  def remove_automatic_group_from_host(host, host_name, events)
115
+ return if host.nil?
116
+
109
117
  ungrouped = host.groups_dataset[name: AUTOMATIC_GROUP]
110
118
  return if ungrouped.nil?
111
119
 
112
120
  emit(events, :removing_automatic_group, host: host_name)
113
- host.remove_group(ungrouped)
121
+ host.remove_group(ungrouped) unless dry_run
114
122
  emit(events, :ok, indent: 4)
115
123
  end
116
124
 
117
125
  def association_exists?(dataset, name)
118
126
  !dataset.nil? && !dataset[name: name].nil?
119
127
  end
120
-
121
- def emit(events, type, payload = {})
122
- events << Event.new(type: type, payload: payload)
123
- end
124
128
  end
125
129
  end
126
130
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'operation_event_support'
4
+
3
5
  module Moose
4
6
  module Inventory
5
7
  module Operations
@@ -7,16 +9,24 @@ module Moose
7
9
  # Adds groups and their optional host associations.
8
10
  class AddGroups
9
11
  AUTOMATIC_GROUP = 'ungrouped'
10
- Event = Struct.new(:type, :payload, keyword_init: true)
11
- Result = Struct.new(:events, :warning_count, keyword_init: true)
12
+ include OperationEventSupport
12
13
 
13
14
  def initialize(context:)
14
15
  @context = context
15
16
  end
16
17
 
17
- def call(names:, hosts:)
18
+ def call(names:, hosts:, dry_run: false)
18
19
  events = []
19
20
  warning_count = 0
21
+ @dry_run = dry_run
22
+
23
+ if dry_run
24
+ names.each do |name|
25
+ warning_count += add_group(name, hosts, events)
26
+ end
27
+ emit(events, :dry_run_summary)
28
+ return operation_result(events: events, warning_count: warning_count)
29
+ end
20
30
 
21
31
  context.transaction do
22
32
  names.each do |name|
@@ -24,12 +34,12 @@ module Moose
24
34
  end
25
35
  end
26
36
 
27
- Result.new(events: events, warning_count: warning_count)
37
+ operation_result(events: events, warning_count: warning_count)
28
38
  end
29
39
 
30
40
  private
31
41
 
32
- attr_reader :context
42
+ attr_reader :context, :dry_run
33
43
 
34
44
  def add_group(name, hosts, events)
35
45
  warning_count = 0
@@ -52,7 +62,7 @@ module Moose
52
62
  group = context.find_group(name)
53
63
 
54
64
  if group.nil?
55
- group = context.create_group(name)
65
+ group = context.create_group(name) unless dry_run
56
66
  emit(events, :ok, indent: 4)
57
67
  [group, nil, true]
58
68
  else
@@ -73,7 +83,7 @@ module Moose
73
83
  emit(events, :association_exists, group: group_name, host: host_name)
74
84
  emit(events, :already_exists_skipping, indent: 4)
75
85
  warning_count += 1
76
- else
86
+ elsif !dry_run
77
87
  group.add_host(host)
78
88
  end
79
89
  emit(events, :ok, indent: 4)
@@ -88,27 +98,25 @@ module Moose
88
98
 
89
99
  emit(events, :host_missing_created, name: name)
90
100
  emit(events, :host_creating_now, name: name)
91
- host = context.create_host(name)
101
+ host = context.create_host(name) unless dry_run
92
102
  emit(events, :ok, indent: 6)
93
103
  [host, :warned_create]
94
104
  end
95
105
 
96
106
  def remove_automatic_group_from_host(host, host_name, events)
107
+ return if host.nil?
108
+
97
109
  ungrouped = host.groups_dataset[name: AUTOMATIC_GROUP]
98
110
  return if ungrouped.nil?
99
111
 
100
112
  emit(events, :removing_automatic_group, host: host_name)
101
- host.remove_group(ungrouped)
113
+ host.remove_group(ungrouped) unless dry_run
102
114
  emit(events, :ok, indent: 4)
103
115
  end
104
116
 
105
117
  def association_exists?(dataset, name)
106
118
  !dataset.nil? && !dataset[name: name].nil?
107
119
  end
108
-
109
- def emit(events, type, payload = {})
110
- events << Event.new(type: type, payload: payload)
111
- end
112
120
  end
113
121
  end
114
122
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'operation_event_support'
4
+
3
5
  module Moose
4
6
  module Inventory
5
7
  module Operations
@@ -12,26 +14,35 @@ module Moose
12
14
  # to progress text.
13
15
  class AddHosts
14
16
  AUTOMATIC_GROUP = 'ungrouped'
15
- Event = Struct.new(:type, :payload, keyword_init: true)
16
- Result = Struct.new(:events, keyword_init: true)
17
+ include OperationEventSupport
17
18
 
18
19
  def initialize(context:)
19
20
  @context = context
20
21
  end
21
22
 
22
- def call(names:, groups:)
23
+ def call(names:, groups:, dry_run: false)
23
24
  events = []
25
+ @dry_run = dry_run
26
+
27
+ if dry_run
28
+ names.each do |name|
29
+ add_host(name, groups, events)
30
+ end
31
+ emit(events, :dry_run_summary)
32
+ return operation_result(events: events)
33
+ end
34
+
24
35
  context.transaction do
25
36
  names.each do |name|
26
37
  add_host(name, groups, events)
27
38
  end
28
39
  end
29
- Result.new(events: events)
40
+ operation_result(events: events)
30
41
  end
31
42
 
32
43
  private
33
44
 
34
- attr_reader :context
45
+ attr_reader :context, :dry_run
35
46
 
36
47
  def add_host(name, groups, events)
37
48
  emit(events, :host_started, name: name)
@@ -41,7 +52,7 @@ module Moose
41
52
  add_group_association(host, name, group_name, groups_dataset, events)
42
53
  end
43
54
 
44
- add_automatic_group_if_needed(host, name, events)
55
+ add_automatic_group_if_needed(host, name, groups, groups_dataset, events)
45
56
  emit(events, :host_complete)
46
57
  end
47
58
 
@@ -51,7 +62,7 @@ module Moose
51
62
  groups_dataset = nil
52
63
 
53
64
  if host.nil?
54
- host = context.create_host(name)
65
+ host = context.create_host(name) unless dry_run
55
66
  else
56
67
  emit(events, :host_exists, name: name)
57
68
  groups_dataset = host.groups_dataset
@@ -69,7 +80,7 @@ module Moose
69
80
 
70
81
  if association_exists?(groups_dataset, group_name)
71
82
  emit(events, :association_exists, host: host_name, group: group_name)
72
- else
83
+ elsif !dry_run
73
84
  host.add_group(group)
74
85
  end
75
86
 
@@ -81,18 +92,24 @@ module Moose
81
92
  return group unless group.nil?
82
93
 
83
94
  emit(events, :group_missing_created, name: name)
84
- context.create_group(name)
95
+ context.create_group(name) unless dry_run
85
96
  end
86
97
 
87
- def add_automatic_group_if_needed(host, host_name, events)
88
- groups_dataset = host.groups_dataset
89
- return if groups_dataset.nil? || groups_dataset.any?
98
+ def add_automatic_group_if_needed(host, host_name, requested_groups, groups_dataset, events)
99
+ return unless automatic_group_needed?(host, requested_groups, groups_dataset)
90
100
 
91
101
  emit(events, :adding_automatic_group, host: host_name, group: AUTOMATIC_GROUP)
92
- host.add_group(automatic_group)
102
+ host.add_group(automatic_group) unless dry_run
93
103
  emit(events, :ok, indent: 4)
94
104
  end
95
105
 
106
+ def automatic_group_needed?(host, requested_groups, groups_dataset)
107
+ return requested_groups.empty? && (host.nil? || groups_dataset.nil? || groups_dataset.none?) if dry_run
108
+
109
+ groups_dataset = host.groups_dataset
110
+ !groups_dataset.nil? && groups_dataset.none?
111
+ end
112
+
96
113
  def automatic_group
97
114
  context.automatic_group
98
115
  end
@@ -100,10 +117,6 @@ module Moose
100
117
  def association_exists?(dataset, name)
101
118
  !dataset.nil? && !dataset[name: name].nil?
102
119
  end
103
-
104
- def emit(events, type, payload = {})
105
- events << Event.new(type: type, payload: payload)
106
- end
107
120
  end
108
121
  end
109
122
  end