moose-inventory 1.0.9 → 2.0

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +15 -1
  3. data/.github/workflows/release.yml +58 -0
  4. data/.gitleaks.toml +9 -0
  5. data/.rubocop.yml +28 -0
  6. data/BACKLOG.md +130 -24
  7. data/Gemfile.lock +36 -1
  8. data/README.md +26 -6
  9. data/Rakefile +1 -1
  10. data/docs/release/publishing.md +44 -48
  11. data/docs/release/release-readiness.md +14 -0
  12. data/docs/security-audit-2026-05-26-rerun.md +75 -0
  13. data/docs/security-audit-2026-05-26.md +63 -0
  14. data/lib/moose_inventory/cli/group.rb +3 -0
  15. data/lib/moose_inventory/cli/group_add.rb +89 -73
  16. data/lib/moose_inventory/cli/group_addchild.rb +77 -60
  17. data/lib/moose_inventory/cli/group_addhost.rb +78 -65
  18. data/lib/moose_inventory/cli/group_rm.rb +101 -71
  19. data/lib/moose_inventory/cli/group_rmchild.rb +99 -53
  20. data/lib/moose_inventory/cli/group_rmhost.rb +64 -56
  21. data/lib/moose_inventory/cli/helpers.rb +76 -0
  22. data/lib/moose_inventory/cli/host.rb +3 -0
  23. data/lib/moose_inventory/cli/host_add.rb +47 -62
  24. data/lib/moose_inventory/cli/host_addgroup.rb +73 -64
  25. data/lib/moose_inventory/cli/host_rmgroup.rb +58 -55
  26. data/lib/moose_inventory/db/db.rb +27 -7
  27. data/lib/moose_inventory/inventory_context.rb +50 -0
  28. data/lib/moose_inventory/operations/add_associations.rb +127 -0
  29. data/lib/moose_inventory/operations/add_groups.rb +115 -0
  30. data/lib/moose_inventory/operations/add_hosts.rb +110 -0
  31. data/lib/moose_inventory/operations/group_child_relations.rb +118 -0
  32. data/lib/moose_inventory/operations/group_cleanup.rb +55 -0
  33. data/lib/moose_inventory/operations/remove_associations.rb +101 -0
  34. data/lib/moose_inventory/operations/remove_groups.rb +79 -0
  35. data/lib/moose_inventory/version.rb +1 -1
  36. data/moose-inventory.gemspec +3 -0
  37. data/scripts/check.sh +2 -0
  38. data/scripts/ci/check_permissions.sh +3 -0
  39. data/scripts/ci/check_rubocop.sh +28 -0
  40. data/scripts/ci/check_secrets.sh +26 -0
  41. data/scripts/ci/check_security.sh +18 -0
  42. data/scripts/ci/install_security_tools.sh +47 -0
  43. data/scripts/install_dependencies.sh +2 -0
  44. data/spec/lib/moose_inventory/cli/group_rm_spec.rb +40 -0
  45. data/spec/lib/moose_inventory/cli/group_rmchild_spec.rb +45 -0
  46. data/spec/lib/moose_inventory/db/db_spec.rb +162 -0
  47. data/spec/lib/moose_inventory/operations/add_associations_spec.rb +77 -0
  48. data/spec/lib/moose_inventory/operations/add_groups_spec.rb +65 -0
  49. data/spec/lib/moose_inventory/operations/add_hosts_spec.rb +69 -0
  50. data/spec/lib/moose_inventory/operations/group_child_relations_spec.rb +76 -0
  51. data/spec/lib/moose_inventory/operations/remove_associations_spec.rb +78 -0
  52. data/spec/lib/moose_inventory/operations/remove_groups_spec.rb +57 -0
  53. metadata +90 -1
@@ -2,6 +2,6 @@ module Moose
2
2
  ##
3
3
  # The Moose-Tools dynamic inventory management library
4
4
  module Inventory
5
- VERSION = '1.0.9'.freeze
5
+ VERSION = '2.0'.freeze
6
6
  end
7
7
  end
@@ -53,8 +53,11 @@ Gem::Specification.new do |spec|
53
53
  spec.add_runtime_dependency 'thor', '>= 1.3', '< 2'
54
54
 
55
55
  spec.add_development_dependency 'bundler', '>= 2.2.33', '< 3'
56
+ spec.add_development_dependency 'bundler-audit', '>= 0.9', '< 1'
57
+ spec.add_development_dependency 'parallel', '>= 1.10', '< 2.0'
56
58
  spec.add_development_dependency 'rake', '>= 13.0', '< 14'
57
59
  spec.add_development_dependency 'rspec', '~> 3'
60
+ spec.add_development_dependency 'rubocop', '>= 1.72', '< 2'
58
61
  spec.add_development_dependency 'simplecov', '~> 0'
59
62
 
60
63
  end
data/scripts/check.sh CHANGED
@@ -2,7 +2,9 @@
2
2
  set -euo pipefail
3
3
 
4
4
  bundle exec rspec --format progress
5
+ scripts/ci/check_rubocop.sh
5
6
  git diff --check
6
7
  scripts/ci/check_permissions.sh
7
8
  scripts/ci/check_security.sh
9
+ scripts/ci/check_secrets.sh
8
10
  scripts/ci/package_sanity.sh
@@ -5,7 +5,10 @@ allowed_executables=(
5
5
  "bin/moose-inventory"
6
6
  "scripts/check.sh"
7
7
  "scripts/ci/check_permissions.sh"
8
+ "scripts/ci/check_rubocop.sh"
9
+ "scripts/ci/check_secrets.sh"
8
10
  "scripts/ci/check_security.sh"
11
+ "scripts/ci/install_security_tools.sh"
9
12
  "scripts/ci/package_sanity.sh"
10
13
  "scripts/files.rb"
11
14
  "scripts/install_dependencies.sh"
@@ -0,0 +1,28 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ bundle exec rubocop \
5
+ lib/moose_inventory/inventory_context.rb \
6
+ lib/moose_inventory/operations/add_hosts.rb \
7
+ lib/moose_inventory/operations/add_groups.rb \
8
+ lib/moose_inventory/operations/add_associations.rb \
9
+ lib/moose_inventory/operations/remove_associations.rb \
10
+ lib/moose_inventory/operations/group_cleanup.rb \
11
+ lib/moose_inventory/operations/group_child_relations.rb \
12
+ lib/moose_inventory/operations/remove_groups.rb \
13
+ lib/moose_inventory/cli/helpers.rb \
14
+ lib/moose_inventory/cli/host_add.rb \
15
+ lib/moose_inventory/cli/group_add.rb \
16
+ lib/moose_inventory/cli/host_addgroup.rb \
17
+ lib/moose_inventory/cli/group_addhost.rb \
18
+ lib/moose_inventory/cli/host_rmgroup.rb \
19
+ lib/moose_inventory/cli/group_rmhost.rb \
20
+ lib/moose_inventory/cli/group_addchild.rb \
21
+ lib/moose_inventory/cli/group_rmchild.rb \
22
+ lib/moose_inventory/cli/group_rm.rb \
23
+ spec/lib/moose_inventory/operations/add_hosts_spec.rb \
24
+ spec/lib/moose_inventory/operations/add_groups_spec.rb \
25
+ spec/lib/moose_inventory/operations/add_associations_spec.rb \
26
+ spec/lib/moose_inventory/operations/remove_associations_spec.rb \
27
+ spec/lib/moose_inventory/operations/group_child_relations_spec.rb \
28
+ spec/lib/moose_inventory/operations/remove_groups_spec.rb
@@ -0,0 +1,26 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ BIN_DIR="${MOOSE_INVENTORY_SECURITY_TOOLS_BIN:-$PWD/tmp/security-tools/bin}"
5
+ if command -v gitleaks >/dev/null 2>&1; then
6
+ GITLEAKS=(gitleaks)
7
+ elif [ -x "$BIN_DIR/gitleaks" ]; then
8
+ GITLEAKS=("$BIN_DIR/gitleaks")
9
+ else
10
+ if [ "${MOOSE_INVENTORY_REQUIRE_SECURITY_TOOLS:-0}" = "1" ]; then
11
+ echo "gitleaks is required but was not found. Run scripts/ci/install_security_tools.sh first." >&2
12
+ exit 2
13
+ fi
14
+ echo "gitleaks not found; skipping dedicated secret scan."
15
+ exit 0
16
+ fi
17
+
18
+ "${GITLEAKS[@]}" detect \
19
+ --no-git \
20
+ --source . \
21
+ --config .gitleaks.toml \
22
+ --redact \
23
+ --no-banner \
24
+ --log-level warn
25
+
26
+ echo "Gitleaks secret scan passed."
@@ -48,3 +48,21 @@ if findings:
48
48
  print(f'- {name} {version}: {vuln_id} {summary}', file=sys.stderr)
49
49
  sys.exit(1)
50
50
  PY
51
+
52
+ bundle exec bundle-audit check --update
53
+
54
+ BIN_DIR="${MOOSE_INVENTORY_SECURITY_TOOLS_BIN:-$PWD/tmp/security-tools/bin}"
55
+ if command -v osv-scanner >/dev/null 2>&1; then
56
+ OSV_SCANNER=(osv-scanner)
57
+ elif [ -x "$BIN_DIR/osv-scanner" ]; then
58
+ OSV_SCANNER=("$BIN_DIR/osv-scanner")
59
+ else
60
+ if [ "${MOOSE_INVENTORY_REQUIRE_SECURITY_TOOLS:-0}" = "1" ]; then
61
+ echo "osv-scanner is required but was not found. Run scripts/ci/install_security_tools.sh first." >&2
62
+ exit 2
63
+ fi
64
+ echo "osv-scanner not found; skipping osv-scanner lockfile scan."
65
+ exit 0
66
+ fi
67
+
68
+ "${OSV_SCANNER[@]}" scan source --lockfile Gemfile.lock .
@@ -0,0 +1,47 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # Installs optional security audit CLIs used by CI. They are kept out of the
5
+ # gem runtime/development bundle because they are Go command-line tools, not
6
+ # Ruby dependencies.
7
+
8
+ BIN_DIR="${MOOSE_INVENTORY_SECURITY_TOOLS_BIN:-$PWD/tmp/security-tools/bin}"
9
+ GITLEAKS_VERSION="${GITLEAKS_VERSION:-v8.30.0}"
10
+ OSV_SCANNER_VERSION="${OSV_SCANNER_VERSION:-v2.2.3}"
11
+
12
+ mkdir -p "$BIN_DIR"
13
+
14
+ if ! command -v go >/dev/null 2>&1; then
15
+ echo "Go is required to install gitleaks/osv-scanner. Install Go or use a prebuilt package." >&2
16
+ exit 2
17
+ fi
18
+
19
+ install_go_tool() {
20
+ local name="$1"
21
+ local module="$2"
22
+ local version="$3"
23
+
24
+ if command -v "$name" >/dev/null 2>&1; then
25
+ echo "$name already available at $(command -v "$name")"
26
+ return
27
+ fi
28
+
29
+ if [ -x "$BIN_DIR/$name" ]; then
30
+ echo "$name already installed at $BIN_DIR/$name"
31
+ return
32
+ fi
33
+
34
+ echo "Installing $name $version into $BIN_DIR"
35
+ GOBIN="$BIN_DIR" go install "$module@$version"
36
+ }
37
+
38
+ install_go_tool gitleaks github.com/zricethezav/gitleaks/v8 "$GITLEAKS_VERSION"
39
+ install_go_tool osv-scanner github.com/google/osv-scanner/v2/cmd/osv-scanner "$OSV_SCANNER_VERSION"
40
+
41
+ if [ -n "${GITHUB_PATH:-}" ]; then
42
+ echo "$BIN_DIR" >> "$GITHUB_PATH"
43
+ fi
44
+
45
+ export PATH="$BIN_DIR:$PATH"
46
+ gitleaks version || true
47
+ osv-scanner --version
@@ -4,6 +4,8 @@ set -euo pipefail
4
4
  sudo dnf groupinstall -y "C Development Tools and Libraries" "Development Tools"
5
5
  sudo dnf install -y \
6
6
  ansible \
7
+ gitleaks \
8
+ golang \
7
9
  ruby \
8
10
  ruby-devel \
9
11
  rubygem-bundler \
@@ -241,5 +241,45 @@ RSpec.describe Moose::Inventory::Cli::Group do
241
241
  groups = @db.models[:group].all
242
242
  expect(groups.count).to eq(1)
243
243
  end
244
+
245
+ #---------------
246
+ it 'GROUP --recursive ... should remove orphaned child groups recursively' do
247
+ runner { @app.start(%w(group add parent)) }
248
+ runner { @app.start(%w(group add child --hosts child-host)) }
249
+ runner { @app.start(%w(group add grandchild)) }
250
+ runner { @app.start(%w(group addchild parent child)) }
251
+ runner { @app.start(%w(group addchild child grandchild)) }
252
+
253
+ actual = runner { @app.start(%w(group rm --recursive parent)) }
254
+
255
+ expect(actual[:unexpected]).to eq(false)
256
+ expect(actual[:aborted]).to eq(false)
257
+ expect(actual[:STDOUT]).to include("- Recursively delete orphaned group 'child'...\n")
258
+ expect(actual[:STDOUT]).to include("- Recursively delete orphaned group 'grandchild'...\n")
259
+
260
+ %w(parent child grandchild).each do |name|
261
+ expect(@db.models[:group].find(name: name)).to be_nil
262
+ end
263
+
264
+ host = @db.models[:host].find(name: 'child-host')
265
+ expect(host.groups_dataset[name: 'ungrouped']).not_to be_nil
266
+ end
267
+
268
+ #---------------
269
+ it 'GROUP --recursive ... should not remove child groups with another parent' do
270
+ runner { @app.start(%w(group add parent other-parent)) }
271
+ runner { @app.start(%w(group addchild parent child)) }
272
+ runner { @app.start(%w(group addchild other-parent child)) }
273
+
274
+ actual = runner { @app.start(%w(group rm --recursive parent)) }
275
+
276
+ expect(actual[:unexpected]).to eq(false)
277
+ expect(actual[:aborted]).to eq(false)
278
+ expect(@db.models[:group].find(name: 'parent')).to be_nil
279
+
280
+ child = @db.models[:group].find(name: 'child')
281
+ expect(child).not_to be_nil
282
+ expect(child.parents_dataset[name: 'other-parent']).not_to be_nil
283
+ end
244
284
  end
245
285
  end
@@ -169,5 +169,50 @@ RSpec.describe Moose::Inventory::Cli::Group do
169
169
 
170
170
  expected(actual, desired)
171
171
  end
172
+
173
+ #------------------------
174
+ it 'GROUP CHILDGROUP --delete-orphans ... should delete orphaned child groups recursively' do
175
+ runner { @app.start(%w(group add parent)) }
176
+ runner { @app.start(%w(group add child --hosts child-host)) }
177
+ runner { @app.start(%w(group add grandchild)) }
178
+ runner { @app.start(%w(group addchild parent child)) }
179
+ runner { @app.start(%w(group addchild child grandchild)) }
180
+
181
+ actual = runner do
182
+ @app.start(%w(group rmchild --delete-orphans parent child))
183
+ end
184
+
185
+ expect(actual[:unexpected]).to eq(false)
186
+ expect(actual[:aborted]).to eq(false)
187
+ expect(actual[:STDOUT]).to include("- Recursively delete orphaned group 'child'...\n")
188
+ expect(actual[:STDOUT]).to include("- Recursively delete orphaned group 'grandchild'...\n")
189
+
190
+ expect(@db.models[:group].find(name: 'parent')).not_to be_nil
191
+ %w(child grandchild).each do |name|
192
+ expect(@db.models[:group].find(name: name)).to be_nil
193
+ end
194
+
195
+ host = @db.models[:host].find(name: 'child-host')
196
+ expect(host.groups_dataset[name: 'ungrouped']).not_to be_nil
197
+ end
198
+
199
+ #------------------------
200
+ it 'GROUP CHILDGROUP --delete-orphans ... should preserve child groups with another parent' do
201
+ runner { @app.start(%w(group add parent other-parent)) }
202
+ runner { @app.start(%w(group addchild parent child)) }
203
+ runner { @app.start(%w(group addchild other-parent child)) }
204
+
205
+ actual = runner do
206
+ @app.start(%w(group rmchild --delete-orphans parent child))
207
+ end
208
+
209
+ expect(actual[:unexpected]).to eq(false)
210
+ expect(actual[:aborted]).to eq(false)
211
+
212
+ child = @db.models[:group].find(name: 'child')
213
+ expect(child).not_to be_nil
214
+ expect(child.parents_dataset[name: 'parent']).to be_nil
215
+ expect(child.parents_dataset[name: 'other-parent']).not_to be_nil
216
+ end
172
217
  end
173
218
  end
@@ -190,6 +190,59 @@ RSpec.describe 'Moose::Inventory::DB' do
190
190
  end
191
191
  end
192
192
 
193
+ it 'raises a Moose DB exception when password and password_env are missing' do
194
+ with_db_config(
195
+ adapter: 'mysql',
196
+ host: 'localhost',
197
+ database: 'moose_inventory_test',
198
+ user: 'moose'
199
+ ) do
200
+ expect { @db.init_mysql }.to raise_error(
201
+ Moose::Inventory::DB::MooseDBException,
202
+ /Expected key password or password_env missing in mysql configuration/
203
+ )
204
+ end
205
+ end
206
+
207
+ it 'uses a mysql password from the configured environment variable' do
208
+ saved_db = @db.instance_variable_get(:@db)
209
+ saved_settings = @config._settings.dup
210
+ saved_password = ENV['MOOSE_INVENTORY_MYSQL_PASSWORD']
211
+ mysql_config = {
212
+ adapter: 'mysql',
213
+ host: 'localhost',
214
+ database: 'moose_inventory_test',
215
+ user: 'moose',
216
+ password_env: 'MOOSE_INVENTORY_MYSQL_PASSWORD',
217
+ }
218
+
219
+ begin
220
+ ENV['MOOSE_INVENTORY_MYSQL_PASSWORD'] = 'env-secret'
221
+ @db.instance_variable_set(:@db, nil)
222
+ @config._settings.clear
223
+ @config._settings[:config] = { db: mysql_config }
224
+
225
+ expect(Sequel).to receive(:mysql2).with(
226
+ user: 'moose',
227
+ password: 'env-secret',
228
+ host: 'localhost',
229
+ database: 'moose_inventory_test'
230
+ ).and_return(:mysql2_connection)
231
+
232
+ @db.init_mysql
233
+ expect(@db.db).to eq(:mysql2_connection)
234
+ ensure
235
+ if saved_password.nil?
236
+ ENV.delete('MOOSE_INVENTORY_MYSQL_PASSWORD')
237
+ else
238
+ ENV['MOOSE_INVENTORY_MYSQL_PASSWORD'] = saved_password
239
+ end
240
+ @db.instance_variable_set(:@db, saved_db)
241
+ @config._settings.clear
242
+ @config._settings.merge!(saved_settings)
243
+ end
244
+ end
245
+
193
246
  it 'uses the mysql2 Sequel adapter with configured connection settings' do
194
247
  saved_db = @db.instance_variable_get(:@db)
195
248
  saved_settings = @config._settings.dup
@@ -238,6 +291,67 @@ RSpec.describe 'Moose::Inventory::DB' do
238
291
  end
239
292
  end
240
293
 
294
+ it 'raises a Moose DB exception when password_env points to an unset variable' do
295
+ saved_password = ENV['MOOSE_INVENTORY_POSTGRES_PASSWORD']
296
+ ENV.delete('MOOSE_INVENTORY_POSTGRES_PASSWORD')
297
+
298
+ begin
299
+ with_db_config(
300
+ adapter: 'postgresql',
301
+ host: 'localhost',
302
+ database: 'moose_inventory_test',
303
+ user: 'moose',
304
+ password_env: 'MOOSE_INVENTORY_POSTGRES_PASSWORD'
305
+ ) do
306
+ expect { @db.init_postgresql }.to raise_error(
307
+ Moose::Inventory::DB::MooseDBException,
308
+ /Environment variable MOOSE_INVENTORY_POSTGRES_PASSWORD is not set for postgresql password/
309
+ )
310
+ end
311
+ ensure
312
+ ENV['MOOSE_INVENTORY_POSTGRES_PASSWORD'] = saved_password unless saved_password.nil?
313
+ end
314
+ end
315
+
316
+ it 'uses a postgresql password from the configured environment variable' do
317
+ saved_db = @db.instance_variable_get(:@db)
318
+ saved_settings = @config._settings.dup
319
+ saved_password = ENV['MOOSE_INVENTORY_POSTGRES_PASSWORD']
320
+ postgresql_config = {
321
+ adapter: 'postgresql',
322
+ host: 'localhost',
323
+ database: 'moose_inventory_test',
324
+ user: 'moose',
325
+ password_env: 'MOOSE_INVENTORY_POSTGRES_PASSWORD',
326
+ }
327
+
328
+ begin
329
+ ENV['MOOSE_INVENTORY_POSTGRES_PASSWORD'] = 'env-secret'
330
+ @db.instance_variable_set(:@db, nil)
331
+ @config._settings.clear
332
+ @config._settings[:config] = { db: postgresql_config }
333
+
334
+ expect(Sequel).to receive(:postgres).with(
335
+ user: 'moose',
336
+ password: 'env-secret',
337
+ host: 'localhost',
338
+ database: 'moose_inventory_test'
339
+ ).and_return(:postgresql_connection)
340
+
341
+ @db.init_postgresql
342
+ expect(@db.db).to eq(:postgresql_connection)
343
+ ensure
344
+ if saved_password.nil?
345
+ ENV.delete('MOOSE_INVENTORY_POSTGRES_PASSWORD')
346
+ else
347
+ ENV['MOOSE_INVENTORY_POSTGRES_PASSWORD'] = saved_password
348
+ end
349
+ @db.instance_variable_set(:@db, saved_db)
350
+ @config._settings.clear
351
+ @config._settings.merge!(saved_settings)
352
+ end
353
+ end
354
+
241
355
  it 'uses the postgres Sequel adapter with configured connection settings' do
242
356
  saved_db = @db.instance_variable_get(:@db)
243
357
  saved_settings = @config._settings.dup
@@ -402,5 +516,53 @@ RSpec.describe 'Moose::Inventory::DB' do
402
516
 
403
517
  expect(count[:final]).to eq(count[:initial])
404
518
  end
519
+
520
+ it 'prints concise Moose DB transaction errors by default' do
521
+ saved_trace = @config._confopts[:trace]
522
+ @config._confopts[:trace] = false
523
+
524
+ begin
525
+ actual = runner do
526
+ @db.transaction do
527
+ fail @db.exceptions[:moose], 'Trace regression target'
528
+ end
529
+ end
530
+ ensure
531
+ @config._confopts[:trace] = saved_trace
532
+ end
533
+
534
+ expect(actual[:unexpected]).to eq(false)
535
+ expect(actual[:aborted]).to eq(true)
536
+ expect(actual[:STDERR]).to eq(
537
+ "An error occurred during a transaction, any changes have been rolled back.\n" \
538
+ "ERROR: Trace regression target\n"
539
+ )
540
+ end
541
+
542
+ it 'prints the Moose DB exception backtrace when trace is enabled' do
543
+ saved_trace = @config._confopts[:trace]
544
+ @config._confopts[:trace] = true
545
+
546
+ begin
547
+ actual = runner do
548
+ @db.transaction do
549
+ fail @db.exceptions[:moose], 'Trace regression target'
550
+ end
551
+ end
552
+ ensure
553
+ @config._confopts[:trace] = saved_trace
554
+ end
555
+
556
+ expect(actual[:unexpected]).to eq(false)
557
+ expect(actual[:aborted]).to eq(true)
558
+ expect(actual[:STDERR]).to include(
559
+ "An error occurred during a transaction, any changes have been rolled back.\n"
560
+ )
561
+ expect(actual[:STDERR]).to include('Moose::Inventory::DB::MooseDBException')
562
+ expect(actual[:STDERR]).to include('Trace regression target')
563
+ expect(actual[:STDERR]).to include('spec/lib/moose_inventory/db/db_spec.rb')
564
+ expect(actual[:STDERR]).to include("ERROR: Trace regression target\n")
565
+ expect(actual[:STDERR]).not_to include('NoMethodError')
566
+ end
405
567
  end
406
568
  end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'inventory_context'
5
+ require 'operations/add_associations'
6
+
7
+ RSpec.describe Moose::Inventory::Operations::AddAssociations do
8
+ before(:all) do
9
+ @mockargs = [
10
+ '--config', File.join(spec_root, 'config/config.yml'),
11
+ '--format', 'yaml',
12
+ '--env', 'test'
13
+ ]
14
+
15
+ Moose::Inventory::Config.init(@mockargs)
16
+ @db = Moose::Inventory::DB
17
+ @db.init if @db.db.nil?
18
+ end
19
+
20
+ before(:each) do
21
+ @db.reset
22
+ end
23
+
24
+ def operation
25
+ described_class.new(
26
+ context: Moose::Inventory::InventoryContext.new(db: @db)
27
+ )
28
+ end
29
+
30
+ it 'adds groups to an existing host and reports creation/duplicate events' do
31
+ host = @db.models[:host].create(name: 'host1')
32
+ ungrouped = @db.models[:group].find_or_create(name: 'ungrouped')
33
+ host.add_group(ungrouped)
34
+ existing_group = @db.models[:group].create(name: 'existing')
35
+ host.add_group(existing_group)
36
+
37
+ result = operation.host_to_groups(
38
+ host: host,
39
+ host_name: 'host1',
40
+ group_names: %w[existing created]
41
+ )
42
+
43
+ expect(result.warning_count).to eq(2)
44
+ expect(result.events.map(&:type)).to include(
45
+ :host_group_association_exists,
46
+ :group_missing_created,
47
+ :removing_automatic_group
48
+ )
49
+ expect(host.groups_dataset[name: 'created']).not_to be_nil
50
+ expect(host.groups_dataset[name: 'ungrouped']).to be_nil
51
+ end
52
+
53
+ it 'adds hosts to an existing group and reports creation/duplicate events' do
54
+ group = @db.models[:group].create(name: 'group1')
55
+ duplicate_host = @db.models[:host].create(name: 'host1')
56
+ existing_host = @db.models[:host].create(name: 'host3')
57
+ ungrouped = @db.models[:group].find_or_create(name: 'ungrouped')
58
+ duplicate_host.add_group(ungrouped)
59
+ existing_host.add_group(ungrouped)
60
+ group.add_host(duplicate_host)
61
+
62
+ result = operation.group_to_hosts(
63
+ group: group,
64
+ group_name: 'group1',
65
+ host_names: %w[host1 host2 host3]
66
+ )
67
+
68
+ expect(result.warning_count).to eq(2)
69
+ expect(result.events.map(&:type)).to include(
70
+ :group_host_association_exists,
71
+ :host_missing_created,
72
+ :removing_automatic_group
73
+ )
74
+ expect(group.hosts_dataset[name: 'host2']).not_to be_nil
75
+ expect(existing_host.groups_dataset[name: 'ungrouped']).to be_nil
76
+ end
77
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'inventory_context'
5
+ require 'operations/add_groups'
6
+
7
+ RSpec.describe Moose::Inventory::Operations::AddGroups do
8
+ before(:all) do
9
+ @mockargs = [
10
+ '--config', File.join(spec_root, 'config/config.yml'),
11
+ '--format', 'yaml',
12
+ '--env', 'test'
13
+ ]
14
+
15
+ Moose::Inventory::Config.init(@mockargs)
16
+ @db = Moose::Inventory::DB
17
+ @db.init if @db.db.nil?
18
+ end
19
+
20
+ before(:each) do
21
+ @db.reset
22
+ end
23
+
24
+ def operation
25
+ described_class.new(
26
+ context: Moose::Inventory::InventoryContext.new(db: @db)
27
+ )
28
+ end
29
+
30
+ it 'adds a group and returns structured events without rendering output' do
31
+ actual = runner do
32
+ @result = operation.call(names: ['testgroup'], hosts: [])
33
+ end
34
+
35
+ expected(actual, STDOUT: '', STDERR: '')
36
+ expect(@result.warning_count).to eq(0)
37
+ expect(@result.events.map(&:type)).to eq(%i[group_started creating_group ok group_complete])
38
+
39
+ group = @db.models[:group].find(name: 'testgroup')
40
+ expect(group).not_to be_nil
41
+ end
42
+
43
+ it 'reports existing groups, created hosts, duplicate associations, and ungrouped removal as events' do
44
+ host = @db.models[:host].create(name: 'testhost')
45
+ ungrouped = @db.models[:group].find_or_create(name: 'ungrouped')
46
+ host.add_group(ungrouped)
47
+ group = @db.models[:group].create(name: 'testgroup')
48
+ group.add_host(host)
49
+
50
+ @result = operation.call(
51
+ names: ['testgroup'],
52
+ hosts: %w[testhost newhost]
53
+ )
54
+
55
+ expect(@result.warning_count).to eq(3)
56
+ expect(@result.events.map(&:type)).to include(
57
+ :group_exists,
58
+ :association_exists,
59
+ :host_missing_created,
60
+ :removing_automatic_group
61
+ )
62
+ expect(@db.models[:host].find(name: 'newhost')).not_to be_nil
63
+ expect(host.groups_dataset[name: 'ungrouped']).to be_nil
64
+ end
65
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Moose::Inventory::Operations::AddHosts do
6
+ before(:all) do
7
+ @mockargs = [
8
+ '--config', File.join(spec_root, 'config/config.yml'),
9
+ '--format', 'yaml',
10
+ '--env', 'test'
11
+ ]
12
+
13
+ Moose::Inventory::Config.init(@mockargs)
14
+ @db = Moose::Inventory::DB
15
+ @db.init if @db.db.nil?
16
+ end
17
+
18
+ before(:each) do
19
+ @db.reset
20
+ end
21
+
22
+ def operation
23
+ described_class.new(
24
+ context: Moose::Inventory::InventoryContext.new(db: @db)
25
+ )
26
+ end
27
+
28
+ describe '#call' do
29
+ it 'adds a host and returns structured events without rendering output' do
30
+ actual = runner do
31
+ @result = operation.call(names: ['testhost'], groups: [])
32
+ end
33
+
34
+ expected(actual, STDOUT: '', STDERR: '')
35
+ expect(@result.events.map(&:type)).to eq(
36
+ %i[host_started creating_host ok adding_automatic_group ok host_complete]
37
+ )
38
+ expect(@result.events[0].payload).to eq(name: 'testhost')
39
+ expect(@result.events[3].payload).to eq(host: 'testhost', group: 'ungrouped')
40
+
41
+ host = @db.models[:host].find(name: 'testhost')
42
+ expect(host).not_to be_nil
43
+ expect(host.groups_dataset[name: 'ungrouped']).not_to be_nil
44
+ end
45
+
46
+ it 'reports existing hosts, missing groups, and duplicate associations as events' do
47
+ host = @db.models[:host].create(name: 'testhost')
48
+ group = @db.models[:group].create(name: 'existinggroup')
49
+ host.add_group(group)
50
+
51
+ @result = operation.call(
52
+ names: ['testhost'],
53
+ groups: %w[existinggroup missinggroup]
54
+ )
55
+
56
+ expect(@result.events.map(&:type)).to include(
57
+ :host_exists,
58
+ :association_exists,
59
+ :group_missing_created
60
+ )
61
+ expect(@result.events.find { |event| event.type == :host_exists }.payload).to eq(name: 'testhost')
62
+ expect(@result.events.find { |event| event.type == :association_exists }.payload).to eq(
63
+ host: 'testhost',
64
+ group: 'existinggroup'
65
+ )
66
+ expect(@db.models[:group].find(name: 'missinggroup')).not_to be_nil
67
+ end
68
+ end
69
+ end