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
@@ -1,15 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/BlockLength
1
4
  require 'spec_helper'
2
5
 
3
- # TODO: the usual respond_to? method doesn't seem to work on Thor objects.
4
6
  # Why not? For now, we'll check against instance_methods.
5
7
 
6
8
  RSpec.describe Moose::Inventory::Cli::Group do
7
9
  before(:all) do
8
10
  # Set up the configuration object
9
11
  @mockarg_parts = {
10
- config: File.join(spec_root, 'config/config.yml'),
11
- format: 'yaml',
12
- env: 'test',
12
+ config: File.join(spec_root, 'config/config.yml'),
13
+ format: 'yaml',
14
+ env: 'test'
13
15
  }
14
16
 
15
17
  @mockargs = []
@@ -39,14 +41,14 @@ RSpec.describe Moose::Inventory::Cli::Group do
39
41
  describe 'addchild' do
40
42
  #------------------------
41
43
  it 'Group.addchild() should be responsive' do
42
- result = @group.instance_methods(false).include?(:addchild)
44
+ result = @group.method_defined?(:addchild, false)
43
45
  expect(result).to eq(true)
44
46
  end
45
47
 
46
48
  #------------------------
47
49
  it '<missing args> ... should abort with an error' do
48
50
  actual = runner do
49
- @app.start(%w(group addchild))
51
+ @app.start(%w[group addchild])
50
52
  end
51
53
 
52
54
  # @console.out(actual, 'y')
@@ -63,7 +65,7 @@ RSpec.describe Moose::Inventory::Cli::Group do
63
65
  child_name = 'fake'
64
66
 
65
67
  actual = runner do
66
- @app.start(%W(group addchild #{parent_name} #{child_name}))
68
+ @app.start(%W[group addchild #{parent_name} #{child_name}])
67
69
  end
68
70
 
69
71
  # @console.out(actual, 'y')
@@ -79,7 +81,7 @@ RSpec.describe Moose::Inventory::Cli::Group do
79
81
  child_name = 'ungrouped'
80
82
 
81
83
  actual = runner do
82
- @app.start(%W(group addchild #{parent_name} #{child_name}))
84
+ @app.start(%W[group addchild #{parent_name} #{child_name}])
83
85
  end
84
86
 
85
87
  # @console.out(actual, 'y')
@@ -92,25 +94,28 @@ RSpec.describe Moose::Inventory::Cli::Group do
92
94
 
93
95
  #------------------------
94
96
  it 'GROUP CHILDGROUP ... should abort if GROUP does not exist' do
95
- # TODO: Why don't we just create GROUP? Likewise for all similar functions?
96
-
97
+ # Contract: addchild attaches children to an existing parent. Missing child
98
+ # groups may be created, but the parent must already exist so typos do not
99
+ # silently create a new tree root.
97
100
  pname = 'parent_group'
98
101
  cname = 'child group'
99
102
 
100
103
  actual = runner do
101
- @app.start(%W(group addchild #{pname} #{cname}))
104
+ @app.start(%W[group addchild #{pname} #{cname}])
102
105
  end
103
106
 
104
107
  # @console.out(actual, 'y')
105
108
  # Check output
106
109
  desired = { aborted: true }
107
110
  desired[:STDOUT] =
108
- "Associate parent group '#{pname}' with child group(s) '#{cname}':\n"\
109
- " - retrieve group '#{pname}'...\n"
111
+ "Associate parent group '#{pname}' with child group(s) '#{cname}':\n " \
112
+ "- retrieve group '#{pname}'...\n"
110
113
  desired[:STDERR] =
111
- "ERROR: The group '#{pname}' does not exist.\n"\
114
+ "ERROR: The group '#{pname}' does not exist.\n" \
112
115
  "An error occurred during a transaction, any changes have been rolled back.\n"
113
116
  expected(actual, desired)
117
+ expect(@db.models[:group].find(name: pname)).to be_nil
118
+ expect(@db.models[:group].find(name: cname)).to be_nil
114
119
  end
115
120
 
116
121
  #------------------------
@@ -118,20 +123,20 @@ RSpec.describe Moose::Inventory::Cli::Group do
118
123
  pname = 'parent_group'
119
124
  cname = 'child_group'
120
125
 
121
- runner { @app.start(%W(group add #{pname} #{cname})) }
126
+ runner { @app.start(%W[group add #{pname} #{cname}]) }
122
127
 
123
- actual = runner { @app.start(%W(group addchild #{pname} #{cname})) }
128
+ actual = runner { @app.start(%W[group addchild #{pname} #{cname}]) }
124
129
 
125
130
  # @console.out(actual, 'y')
126
131
 
127
132
  desired = { aborted: false }
128
133
  desired[:STDOUT] =
129
- "Associate parent group '#{pname}' with child group(s) '#{cname}':\n"\
130
- " - retrieve group '#{pname}'...\n"\
131
- " - OK\n"\
132
- " - add association {group:#{pname} <-> group:#{cname}}...\n"\
133
- " - OK\n"\
134
- " - all OK\n"\
134
+ "Associate parent group '#{pname}' with child group(s) '#{cname}':\n " \
135
+ "- retrieve group '#{pname}'...\n " \
136
+ "- OK\n " \
137
+ "- add association {group:#{pname} <-> group:#{cname}}...\n " \
138
+ "- OK\n " \
139
+ "- all OK\n" \
135
140
  "Succeeded.\n"
136
141
  expected(actual, desired)
137
142
 
@@ -143,28 +148,76 @@ RSpec.describe Moose::Inventory::Cli::Group do
143
148
  end
144
149
 
145
150
  #------------------------
146
- it 'GROUP CHILDGROUP... should associate GROUP with a CHILDGROUP '\
147
- 'creating it if necessary' do
148
- #
151
+ it 'GROUP CHILDGROUP --dry-run should not create the child or association' do
152
+ pname = 'parent_group'
153
+ cname = 'dry_child'
154
+
155
+ runner { @app.start(%W[group add #{pname}]) }
156
+
157
+ actual = runner { @app.start(%W[group addchild #{pname} #{cname} --dry-run]) }
158
+
159
+ expect(actual[:unexpected]).to eq(false)
160
+ expect(actual[:aborted]).to eq(false)
161
+ expect(actual[:STDOUT]).to include('Dry run complete. No changes applied.')
162
+ expect(@db.models[:group].find(name: cname)).to be_nil
163
+ pgroup = @db.models[:group].find(name: pname)
164
+ expect(pgroup.children_dataset[name: cname]).to be_nil
165
+ end
166
+
167
+ #------------------------
168
+
169
+ it 'GROUP CHILDGROUP... should warn and skip when the association already exists' do
170
+ pname = 'parent_group'
171
+ cname = 'child_group'
172
+
173
+ runner { @app.start(%W[group add #{pname} #{cname}]) }
174
+ runner { @app.start(%W[group addchild #{pname} #{cname}]) }
175
+
176
+ actual = runner { @app.start(%W[group addchild #{pname} #{cname}]) }
177
+
178
+ desired = { aborted: false }
179
+ desired[:STDOUT] = <<~OUTPUT
180
+ Associate parent group '#{pname}' with child group(s) '#{cname}':
181
+ - retrieve group '#{pname}'...
182
+ - OK
183
+ - add association {group:#{pname} <-> group:#{cname}}...
184
+ - already exists, skipping.
185
+ - OK
186
+ - all OK
187
+ Succeeded, with warnings.
188
+ OUTPUT
189
+ desired[:STDERR] = <<~ERROR
190
+ WARNING: Association {group:#{pname} <-> group:#{cname}} already exists, skipping.
191
+ ERROR
192
+ expected(actual, desired)
193
+
194
+ pgroup = @db.models[:group].find(name: pname)
195
+ cgroups = pgroup.children_dataset
196
+ expect(cgroups.count).to eq(1)
197
+ expect(cgroups[name: cname]).not_to be_nil
198
+ end
199
+
200
+ it 'GROUP CHILDGROUP... should associate GROUP with a CHILDGROUP ' \
201
+ 'creating it if necessary' do
149
202
  pname = 'parent_group'
150
203
  cname = 'child_group'
151
204
 
152
- runner { @app.start(%W(group add #{pname})) } # <- don't pre-create the child
205
+ runner { @app.start(%W[group add #{pname}]) } # <- don't pre-create the child
153
206
 
154
- actual = runner { @app.start(%W(group addchild #{pname} #{cname})) }
207
+ actual = runner { @app.start(%W[group addchild #{pname} #{cname}]) }
155
208
 
156
209
  # @console.out(actual, 'y')
157
210
 
158
211
  desired = { aborted: false }
159
212
  desired[:STDOUT] =
160
- "Associate parent group '#{pname}' with child group(s) '#{cname}':\n"\
161
- " - retrieve group '#{pname}'...\n"\
162
- " - OK\n"\
163
- " - add association {group:#{pname} <-> group:#{cname}}...\n"\
164
- " - child group does not exist, creating now...\n"\
165
- " - OK\n"\
166
- " - OK\n"\
167
- " - all OK\n"\
213
+ "Associate parent group '#{pname}' with child group(s) '#{cname}':\n " \
214
+ "- retrieve group '#{pname}'...\n " \
215
+ "- OK\n " \
216
+ "- add association {group:#{pname} <-> group:#{cname}}...\n " \
217
+ "- child group does not exist, creating now...\n " \
218
+ "- OK\n " \
219
+ "- OK\n " \
220
+ "- all OK\n" \
168
221
  "Succeeded, with warnings.\n"
169
222
  desired[:STDERR] = "WARNING: Group '#{cname}' does not exist and will be created.\n"
170
223
  expected(actual, desired)
@@ -177,3 +230,4 @@ RSpec.describe Moose::Inventory::Cli::Group do
177
230
  end
178
231
  end
179
232
  end
233
+ # rubocop:enable Metrics/BlockLength
@@ -1,52 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/BlockLength
1
4
  require 'spec_helper'
2
5
 
3
- # TODO: the usual respond_to? method doesn't seem to work on Thor objects.
4
6
  # Why not? For now, we'll check against instance_methods.
5
7
 
6
8
  RSpec.describe Moose::Inventory::Cli::Group do
7
9
  before(:all) do
8
- # Set up the configuration object
9
- @mockarg_parts = {
10
- config: File.join(spec_root, 'config/config.yml'),
11
- format: 'yaml',
12
- env: 'test',
13
- }
14
-
15
- @mockargs = []
16
- @mockarg_parts.each do |key, val|
17
- @mockargs << "--#{key}"
18
- @mockargs << val
19
- end
20
-
21
- @console = Moose::Inventory::Cli::Formatter
22
-
23
- @config = Moose::Inventory::Config
24
- @config.init(@mockargs)
25
-
26
- @db = Moose::Inventory::DB
27
- @db.init if @db.db.nil?
28
-
29
- @group = Moose::Inventory::Cli::Group
30
- @host = Moose::Inventory::Cli::Host
31
- @app = Moose::Inventory::Cli::Application
10
+ setup_cli_harness(
11
+ command_class: Moose::Inventory::Cli::Group,
12
+ command_ivar: :@group,
13
+ extra_commands: { :@host => Moose::Inventory::Cli::Host }
14
+ )
32
15
  end
33
16
 
34
17
  before(:each) do
35
- @db.reset
18
+ reset_cli_harness
36
19
  end
37
20
 
38
21
  #=======================
39
22
  describe 'addhost' do
40
23
  #------------------------
41
24
  it 'Group.addhost() should be responsive' do
42
- result = @group.instance_methods(false).include?(:addhost)
25
+ result = @group.method_defined?(:addhost, false)
43
26
  expect(result).to eq(true)
44
27
  end
45
28
 
46
29
  #------------------------
47
30
  it 'addhost <missing args> ... should abort with an error' do
48
31
  actual = runner do
49
- @app.start(%w(group addhost)) # <- no group given
32
+ @app.start(%w[group addhost]) # <- no group given
50
33
  end
51
34
 
52
35
  # @console.out(actual, 'y')
@@ -63,17 +46,17 @@ RSpec.describe Moose::Inventory::Cli::Group do
63
46
  group_name = 'not-a-group'
64
47
 
65
48
  actual = runner do
66
- @app.start(%W(group addhost #{group_name} #{host_name}))
49
+ @app.start(%W[group addhost #{group_name} #{host_name}])
67
50
  end
68
51
 
69
52
  # @console.out(actual, 'y')
70
53
  # Check output
71
54
  desired = { aborted: true }
72
55
  desired[:STDOUT] =
73
- "Associate group '#{group_name}' with host(s) '#{host_name}':\n"\
74
- " - retrieve group '#{group_name}'...\n"
56
+ "Associate group '#{group_name}' with host(s) '#{host_name}':\n " \
57
+ "- retrieve group '#{group_name}'...\n"
75
58
  desired[:STDERR] =
76
- "ERROR: The group '#{group_name}' does not exist.\n"\
59
+ "ERROR: The group '#{group_name}' does not exist.\n" \
77
60
  "An error occurred during a transaction, any changes have been rolled back.\n"
78
61
  expected(actual, desired)
79
62
  end
@@ -86,26 +69,23 @@ RSpec.describe Moose::Inventory::Cli::Group do
86
69
  host_name = 'test1'
87
70
  group_name = 'testgroup1'
88
71
 
89
- runner { @app.start(%W(host add #{host_name})) }
72
+ runner { @app.start(%W[host add #{host_name}]) }
90
73
  @db.models[:group].create(name: group_name)
91
74
 
92
- actual = runner { @app.start(%W(group addhost #{group_name} #{host_name})) }
75
+ actual = runner { @app.start(%W[group addhost #{group_name} #{host_name}]) }
93
76
 
94
- # rubocop:disable Metrics/LineLength
95
77
  desired = { aborted: false }
96
78
  desired[:STDOUT] =
97
- "Associate group '#{group_name}' with host(s) '#{host_name}':\n"\
98
- " - retrieve group '#{group_name}'...\n"\
99
- " - OK\n"\
100
- " - add association {group:#{group_name} <-> host:#{host_name}}...\n"\
101
- " - OK\n"\
102
- " - remove automatic association {group:ungrouped <-> host:#{host_name}}...\n"\
103
- " - OK\n"\
104
- " - all OK\n"\
79
+ "Associate group '#{group_name}' with host(s) '#{host_name}':\n " \
80
+ "- retrieve group '#{group_name}'...\n " \
81
+ "- OK\n " \
82
+ "- add association {group:#{group_name} <-> host:#{host_name}}...\n " \
83
+ "- OK\n " \
84
+ "- remove automatic association {group:ungrouped <-> host:#{host_name}}...\n " \
85
+ "- OK\n " \
86
+ "- all OK\n" \
105
87
  "Succeeded.\n"
106
88
  expected(actual, desired)
107
- # rubocop:enable Metrics/LineLength
108
-
109
89
  # We should have the correct group associations
110
90
  host = @db.models[:host].find(name: host_name)
111
91
  groups = host.groups_dataset
@@ -114,14 +94,30 @@ RSpec.describe Moose::Inventory::Cli::Group do
114
94
  expect(groups[name: 'ungrouped']).to be_nil # redundant, but for clarity!
115
95
  end
116
96
 
97
+ #------------------------
98
+ it 'group addhost GROUP HOST --dry-run should not create the host or association' do
99
+ host_name = 'dry-host'
100
+ group_name = 'testgroup1'
101
+ runner { @app.start(%W[group add #{group_name}]) }
102
+
103
+ actual = runner { @app.start(%W[group addhost #{group_name} #{host_name} --dry-run]) }
104
+
105
+ expect(actual[:unexpected]).to eq(false)
106
+ expect(actual[:aborted]).to eq(false)
107
+ expect(actual[:STDOUT]).to include('Dry run complete. No changes applied.')
108
+ expect(@db.models[:host].find(name: host_name)).to be_nil
109
+ group = @db.models[:group].find(name: group_name)
110
+ expect(group.hosts_dataset[name: host_name]).to be_nil
111
+ end
112
+
117
113
  #------------------------
118
114
  it '\'ungrouped\' HOST... should abort with an error' do
119
115
  host_name = 'test1'
120
116
  group_name = 'ungrouped'
121
117
 
122
- runner { @app.start(%W(host add #{host_name})) }
118
+ runner { @app.start(%W[host add #{host_name}]) }
123
119
 
124
- actual = runner { @app.start(%W(group addhost #{group_name} #{host_name})) }
120
+ actual = runner { @app.start(%W[group addhost #{group_name} #{host_name}]) }
125
121
 
126
122
  desired = { aborted: true }
127
123
  desired[:STDERR] =
@@ -134,23 +130,23 @@ RSpec.describe Moose::Inventory::Cli::Group do
134
130
  host_name = 'test1'
135
131
  group_name = 'testgroup1'
136
132
 
137
- runner { @app.start(%W(group add #{group_name})) }
133
+ runner { @app.start(%W[group add #{group_name}]) }
138
134
 
139
135
  # DON'T CREATE THE HOST! That's the point of the test. ;o)
140
136
 
141
- actual = runner { @app.start(%W(group addhost #{group_name} #{host_name})) }
137
+ actual = runner { @app.start(%W[group addhost #{group_name} #{host_name}]) }
142
138
 
143
139
  # Check output
144
140
  desired = {}
145
141
  desired[:STDOUT] =
146
- "Associate group '#{group_name}' with host(s) '#{host_name}':\n"\
147
- " - retrieve group '#{group_name}'...\n"\
148
- " - OK\n"\
149
- " - add association {group:#{group_name} <-> host:#{host_name}}...\n"\
150
- " - host does not exist, creating now...\n"\
151
- " - OK\n"\
152
- " - OK\n"\
153
- " - all OK\n"\
142
+ "Associate group '#{group_name}' with host(s) '#{host_name}':\n " \
143
+ "- retrieve group '#{group_name}'...\n " \
144
+ "- OK\n " \
145
+ "- add association {group:#{group_name} <-> host:#{host_name}}...\n " \
146
+ "- host does not exist, creating now...\n " \
147
+ "- OK\n " \
148
+ "- OK\n " \
149
+ "- all OK\n" \
154
150
  "Succeeded, with warnings.\n"
155
151
  desired[:STDERR] =
156
152
  "WARNING: Host '#{host_name}' does not exist and will be created.\n"
@@ -164,32 +160,32 @@ RSpec.describe Moose::Inventory::Cli::Group do
164
160
  end
165
161
 
166
162
  #------------------------
167
- it 'GROUP HOST... should skip associations that already '\
168
- ' exist, but raise a warning.' do
163
+ it 'GROUP HOST... should skip associations that already ' \
164
+ 'exist, but raise a warning.' do
169
165
  host_name = 'test1'
170
166
  group_name = 'testgroup1'
171
167
 
172
- runner { @app.start(%W(group add #{group_name})) }
173
- runner { @app.start(%W(host add #{host_name})) }
168
+ runner { @app.start(%W[group add #{group_name}]) }
169
+ runner { @app.start(%W[host add #{host_name}]) }
174
170
 
175
171
  # Run once to make the initial association
176
- runner { @app.start(%W(group addhost #{group_name} #{host_name})) }
172
+ runner { @app.start(%W[group addhost #{group_name} #{host_name}]) }
177
173
 
178
174
  # Run again, to prove expected result
179
- actual = runner { @app.start(%W(group addhost #{group_name} #{host_name})) }
175
+ actual = runner { @app.start(%W[group addhost #{group_name} #{host_name}]) }
180
176
 
181
177
  # @console.out(actual,'y')
182
178
 
183
179
  # Check output
184
180
  desired = {}
185
181
  desired[:STDOUT] =
186
- "Associate group '#{group_name}' with host(s) '#{host_name}':\n"\
187
- " - retrieve group \'#{group_name}\'...\n"\
188
- " - OK\n"\
189
- " - add association {group:#{group_name} <-> host:#{host_name}}...\n"\
190
- " - already exists, skipping.\n"\
191
- " - OK\n"\
192
- " - all OK\n"\
182
+ "Associate group '#{group_name}' with host(s) '#{host_name}':\n " \
183
+ "- retrieve group '#{group_name}'...\n " \
184
+ "- OK\n " \
185
+ "- add association {group:#{group_name} <-> host:#{host_name}}...\n " \
186
+ "- already exists, skipping.\n " \
187
+ "- OK\n " \
188
+ "- all OK\n" \
193
189
  "Succeeded, with warnings.\n"
194
190
  desired[:STDERR] =
195
191
  "WARNING: Association {group:#{group_name} <-> host:#{host_name}} already exists, skipping.\n"
@@ -203,38 +199,38 @@ RSpec.describe Moose::Inventory::Cli::Group do
203
199
  end
204
200
 
205
201
  #------------------------
206
- it 'GROUP HOST1 HOST2 ... should associate the group with '\
207
- ' multiple hosts at once' do
202
+ it 'GROUP HOST1 HOST2 ... should associate the group with ' \
203
+ 'multiple hosts at once' do
208
204
  group_name = 'test1'
209
- host_names = %w(host1 host2 host3)
205
+ host_names = %w[host1 host2 host3]
210
206
 
211
- runner { @app.start(%W(group add #{group_name})) }
207
+ runner { @app.start(%W[group add #{group_name}]) }
212
208
  host_names.each do |host|
213
- runner { @app.start(%W(host add #{host})) }
209
+ runner { @app.start(%W[host add #{host}]) }
214
210
  end
215
211
 
216
- actual = runner { @app.start(%W(group addhost #{group_name}) + host_names) }
212
+ actual = runner { @app.start(%W[group addhost #{group_name}] + host_names) }
217
213
 
218
214
  # @console.out(actual, 'y')
219
215
 
220
216
  # Check output
221
217
  desired = { aborted: false, STDERR: '' }
222
218
  desired[:STDOUT] =
223
- "Associate group '#{group_name}' with host(s) '#{host_names.join(',')}':\n"\
224
- " - retrieve group '#{group_name}'...\n"\
225
- " - OK\n"
219
+ "Associate group '#{group_name}' with host(s) '#{host_names.join(',')}':\n " \
220
+ "- retrieve group '#{group_name}'...\n " \
221
+ "- OK\n"
226
222
  host_names.each do |host|
227
223
  desired[:STDOUT] = desired[:STDOUT] +
228
- " - add association {group:#{group_name} <-> host:#{host}}...\n"\
229
- " - OK\n"\
230
- " - remove automatic association {group:ungrouped <-> host:#{host}}...\n"\
231
- " - OK\n"\
224
+ " - add association {group:#{group_name} <-> host:#{host}}...\n " \
225
+ "- OK\n " \
226
+ "- remove automatic association {group:ungrouped <-> host:#{host}}...\n " \
227
+ "- OK\n" \
232
228
 
233
229
  # desired[:STDERR] = desired[:STDERR] +
234
230
  # "WARNING: Host '#{host}' does not exist and will be created.\n"
235
231
  end
236
232
  desired[:STDOUT] = desired[:STDOUT] +
237
- " - all OK\n"\
233
+ " - all OK\n" \
238
234
  "Succeeded.\n"
239
235
  expected(actual, desired)
240
236
 
@@ -246,3 +242,4 @@ RSpec.describe Moose::Inventory::Cli::Group do
246
242
  end
247
243
  end
248
244
  end
245
+ # rubocop:enable Metrics/BlockLength