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
@@ -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
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ module Moose
6
+ module Inventory
7
+ ##
8
+ # Thin facade over an explicitly supplied DB implementation.
9
+ #
10
+ # This gives new operation/service objects a small inventory-facing seam
11
+ # without forcing the legacy CLI to stop using the DB singleton all at once.
12
+ class InventoryContext
13
+ AUTOMATIC_GROUP = 'ungrouped'
14
+
15
+ def initialize(db:)
16
+ @db = db
17
+ end
18
+
19
+ def transaction(&)
20
+ db.transaction(&)
21
+ end
22
+
23
+ def find_host(name)
24
+ db.models[:host].find(name: name)
25
+ end
26
+
27
+ def create_host(name)
28
+ db.models[:host].create(name: name)
29
+ end
30
+
31
+ def find_group(name)
32
+ db.models[:group].find(name: name)
33
+ end
34
+
35
+ def create_group(name)
36
+ db.models[:group].create(name: name)
37
+ end
38
+
39
+ def find_or_create_group(name)
40
+ db.models[:group].find_or_create(name: name)
41
+ end
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
+
67
+ def automatic_group
68
+ find_or_create_group(AUTOMATIC_GROUP)
69
+ end
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
+
107
+ private
108
+
109
+ def variable_model_key(entity_type)
110
+ :"#{entity_type}var"
111
+ end
112
+
113
+ attr_reader :db
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'operation_event_support'
4
+
5
+ module Moose
6
+ module Inventory
7
+ module Operations
8
+ # Adds host/group associations for existing primary entities.
9
+ class AddAssociations
10
+ AUTOMATIC_GROUP = 'ungrouped'
11
+ include OperationEventSupport
12
+
13
+ def initialize(context:)
14
+ @context = context
15
+ end
16
+
17
+ def host_to_groups(host:, host_name:, group_names:, dry_run: false)
18
+ events = []
19
+ warning_count = 0
20
+ @dry_run = dry_run
21
+
22
+ group_names.each do |group_name|
23
+ next if group_name.nil? || group_name.empty?
24
+
25
+ warning_count += add_group_to_host(host, host_name, group_name, events)
26
+ end
27
+
28
+ remove_automatic_group_from_host(host, host_name, events)
29
+ emit(events, :dry_run_summary) if dry_run
30
+
31
+ operation_result(events: events, warning_count: warning_count)
32
+ end
33
+
34
+ def group_to_hosts(group:, group_name:, host_names:, dry_run: false)
35
+ events = []
36
+ warning_count = 0
37
+ @dry_run = dry_run
38
+ hosts_dataset = group.hosts_dataset
39
+
40
+ host_names.each do |host_name|
41
+ next if host_name.nil? || host_name.empty?
42
+
43
+ warning_count += add_host_to_group(
44
+ group,
45
+ group_name,
46
+ host_name,
47
+ hosts_dataset,
48
+ events
49
+ )
50
+ end
51
+
52
+ emit(events, :dry_run_summary) if dry_run
53
+
54
+ operation_result(events: events, warning_count: warning_count)
55
+ end
56
+
57
+ private
58
+
59
+ attr_reader :context, :dry_run
60
+
61
+ def add_group_to_host(host, host_name, group_name, events)
62
+ warning_count = 0
63
+ groups_dataset = host.groups_dataset
64
+
65
+ emit(events, :adding_host_group_association, host: host_name, group: group_name)
66
+
67
+ if association_exists?(groups_dataset, group_name)
68
+ emit(events, :host_group_association_exists, host: host_name, group: group_name)
69
+ emit(events, :already_exists_skipping, indent: 4)
70
+ emit(events, :ok, indent: 4)
71
+ return warning_count + 1
72
+ end
73
+
74
+ group = context.find_group(group_name)
75
+ if group.nil?
76
+ emit(events, :group_missing_created, name: group_name)
77
+ emit(events, :group_creating_now, name: group_name)
78
+ group = context.create_group(group_name) unless dry_run
79
+ emit(events, :ok, indent: 6)
80
+ warning_count += 1
81
+ end
82
+
83
+ host.add_group(group) unless dry_run
84
+ emit(events, :ok, indent: 4)
85
+ warning_count
86
+ end
87
+
88
+ def add_host_to_group(group, group_name, host_name, hosts_dataset, events)
89
+ warning_count = 0
90
+ emit(events, :adding_group_host_association, group: group_name, host: host_name)
91
+
92
+ if association_exists?(hosts_dataset, host_name)
93
+ emit(events, :group_host_association_exists, group: group_name, host: host_name)
94
+ emit(events, :already_exists_skipping, indent: 4)
95
+ emit(events, :ok, indent: 4)
96
+ return warning_count + 1
97
+ end
98
+
99
+ host = context.find_host(host_name)
100
+ if host.nil?
101
+ emit(events, :host_missing_created, name: host_name)
102
+ emit(events, :host_creating_now, name: host_name)
103
+ host = context.create_host(host_name) unless dry_run
104
+ emit(events, :ok, indent: 6)
105
+ warning_count += 1
106
+ end
107
+
108
+ group.add_host(host) unless dry_run
109
+ emit(events, :ok, indent: 4)
110
+ remove_automatic_group_from_host(host, host_name, events)
111
+ warning_count
112
+ end
113
+
114
+ def remove_automatic_group_from_host(host, host_name, events)
115
+ return if host.nil?
116
+
117
+ ungrouped = host.groups_dataset[name: AUTOMATIC_GROUP]
118
+ return if ungrouped.nil?
119
+
120
+ emit(events, :removing_automatic_group, host: host_name)
121
+ host.remove_group(ungrouped) unless dry_run
122
+ emit(events, :ok, indent: 4)
123
+ end
124
+
125
+ def association_exists?(dataset, name)
126
+ !dataset.nil? && !dataset[name: name].nil?
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'operation_event_support'
4
+
5
+ module Moose
6
+ module Inventory
7
+ module Operations
8
+ ##
9
+ # Adds groups and their optional host associations.
10
+ class AddGroups
11
+ AUTOMATIC_GROUP = 'ungrouped'
12
+ include OperationEventSupport
13
+
14
+ def initialize(context:)
15
+ @context = context
16
+ end
17
+
18
+ def call(names:, hosts:, dry_run: false)
19
+ events = []
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
30
+
31
+ context.transaction do
32
+ names.each do |name|
33
+ warning_count += add_group(name, hosts, events)
34
+ end
35
+ end
36
+
37
+ operation_result(events: events, warning_count: warning_count)
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :context, :dry_run
43
+
44
+ def add_group(name, hosts, events)
45
+ warning_count = 0
46
+ emit(events, :group_started, name: name)
47
+ group, hosts_dataset, created = create_or_find_group(name, events)
48
+ warning_count += 1 unless created
49
+
50
+ hosts.each do |host_name|
51
+ next if host_name.nil? || host_name.empty?
52
+
53
+ warning_count += add_host_association(group, name, host_name, hosts_dataset, events)
54
+ end
55
+
56
+ emit(events, :group_complete)
57
+ warning_count
58
+ end
59
+
60
+ def create_or_find_group(name, events)
61
+ emit(events, :creating_group)
62
+ group = context.find_group(name)
63
+
64
+ if group.nil?
65
+ group = context.create_group(name) unless dry_run
66
+ emit(events, :ok, indent: 4)
67
+ [group, nil, true]
68
+ else
69
+ emit(events, :group_exists, name: name)
70
+ emit(events, :already_exists_skipping, indent: 4)
71
+ emit(events, :ok, indent: 4)
72
+ [group, group.hosts_dataset, false]
73
+ end
74
+ end
75
+
76
+ def add_host_association(group, group_name, host_name, hosts_dataset, events)
77
+ warning_count = 0
78
+ emit(events, :adding_association, group: group_name, host: host_name)
79
+ host, created = find_or_create_host(host_name, events)
80
+ warning_count += 1 if created == :warned_create
81
+
82
+ if association_exists?(hosts_dataset, host_name)
83
+ emit(events, :association_exists, group: group_name, host: host_name)
84
+ emit(events, :already_exists_skipping, indent: 4)
85
+ warning_count += 1
86
+ elsif !dry_run
87
+ group.add_host(host)
88
+ end
89
+ emit(events, :ok, indent: 4)
90
+
91
+ remove_automatic_group_from_host(host, host_name, events)
92
+ warning_count
93
+ end
94
+
95
+ def find_or_create_host(name, events)
96
+ host = context.find_host(name)
97
+ return [host, :existing] unless host.nil?
98
+
99
+ emit(events, :host_missing_created, name: name)
100
+ emit(events, :host_creating_now, name: name)
101
+ host = context.create_host(name) unless dry_run
102
+ emit(events, :ok, indent: 6)
103
+ [host, :warned_create]
104
+ end
105
+
106
+ def remove_automatic_group_from_host(host, host_name, events)
107
+ return if host.nil?
108
+
109
+ ungrouped = host.groups_dataset[name: AUTOMATIC_GROUP]
110
+ return if ungrouped.nil?
111
+
112
+ emit(events, :removing_automatic_group, host: host_name)
113
+ host.remove_group(ungrouped) unless dry_run
114
+ emit(events, :ok, indent: 4)
115
+ end
116
+
117
+ def association_exists?(dataset, name)
118
+ !dataset.nil? && !dataset[name: name].nil?
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end