morpheus-cli 9.0.2 → 9.1.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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +1 -1
  3. data/lib/morpheus/api/affinity_groups_interface.rb +46 -0
  4. data/lib/morpheus/api/api_client.rb +13 -0
  5. data/lib/morpheus/api/clouds_interface.rb +42 -0
  6. data/lib/morpheus/api/clusters_interface.rb +7 -1
  7. data/lib/morpheus/api/distributed_workers_interface.rb +41 -0
  8. data/lib/morpheus/api/host_vm_groups_interface.rb +37 -0
  9. data/lib/morpheus/cli/commands/affinity_groups_command.rb +554 -0
  10. data/lib/morpheus/cli/commands/backups_command.rb +1 -1
  11. data/lib/morpheus/cli/commands/clouds.rb +421 -29
  12. data/lib/morpheus/cli/commands/clusters.rb +37 -39
  13. data/lib/morpheus/cli/commands/distributed_workers_command.rb +282 -0
  14. data/lib/morpheus/cli/commands/host_vm_groups_command.rb +423 -0
  15. data/lib/morpheus/cli/mixins/affinity_helper.rb +118 -0
  16. data/lib/morpheus/cli/mixins/infrastructure_helper.rb +137 -0
  17. data/lib/morpheus/cli/option_types.rb +36 -1
  18. data/lib/morpheus/cli/version.rb +1 -1
  19. data/test/api/affinity_groups_interface_test.rb +43 -0
  20. data/test/api/clouds_interface_test.rb +67 -0
  21. data/test/api/host_vm_groups_interface_test.rb +35 -0
  22. data/test/cli/affinity_groups_cloud_cluster_scope_test.rb +270 -0
  23. data/test/cli/affinity_groups_cluster_scope_test.rb +171 -0
  24. data/test/cli/affinity_groups_test.rb +57 -0
  25. data/test/cli/affinity_groups_violations_scope_test.rb +141 -0
  26. data/test/cli/affinity_helper_test.rb +367 -0
  27. data/test/cli/backups_test.rb +212 -0
  28. data/test/cli/clusters_uuid_test.rb +131 -0
  29. data/test/cli/distributed_workers_test.rb +68 -0
  30. data/test/cli/host_vm_groups_cluster_scope_test.rb +187 -0
  31. data/test/cli/host_vm_groups_test.rb +63 -0
  32. data/test/cli/infrastructure_helper_cluster_resolver_test.rb +250 -0
  33. metadata +37 -2
@@ -0,0 +1,250 @@
1
+ require 'test/unit'
2
+ require 'stringio'
3
+ require 'json'
4
+ require 'morpheus'
5
+
6
+ # Unit tests for the MORPH-17279 cloud-owned vSphere cluster resolver added to
7
+ # Morpheus::Cli::InfrastructureHelper:
8
+ # find_resource_pool_cluster_by_name_or_id / _by_id / _by_name
9
+ #
10
+ # These are pure unit tests against a fake CloudResourcePoolsInterface. They
11
+ # deliberately do NOT extend MorpheusTest::TestCase because they require no
12
+ # appliance, no remote and no authentication - they must always be runnable.
13
+ # See test/cli/affinity_helper_test.rb for the established pattern this
14
+ # follows.
15
+ #
16
+ # Bug context: --cloud CLOUD --cluster CLUSTER must resolve CLUSTER as a
17
+ # ComputeZonePool(type='Cluster') owned by CLOUD, distinct from the managed
18
+ # cluster (ComputeServerGroup) namespace resolved via ClustersInterface. A
19
+ # name lookup that traverses ambiguous, wrong-cloud or wrong-type pools must
20
+ # fail loudly instead of returning nil silently or the wrong pool.
21
+ class InfrastructureHelperClusterResolverTest < Test::Unit::TestCase
22
+
23
+ # Fake standing in for Morpheus::CloudResourcePoolsInterface. Records every
24
+ # call so tests can assert exactly what was requested, mirroring the
25
+ # dry-run request assertions used elsewhere in this suite.
26
+ class FakeResourcePoolsInterface
27
+ attr_reader :get_calls, :list_calls
28
+
29
+ def initialize(get_result: nil, get_error: nil, list_result: nil)
30
+ @get_result = get_result
31
+ @get_error = get_error
32
+ @list_result = list_result
33
+ @get_calls = []
34
+ @list_calls = []
35
+ end
36
+
37
+ def get(cloud_id, id, params = {})
38
+ @get_calls << [cloud_id, id, params]
39
+ raise @get_error if @get_error
40
+ @get_result
41
+ end
42
+
43
+ def list(cloud_id, params = {})
44
+ @list_calls << [cloud_id, params]
45
+ @list_result
46
+ end
47
+ end
48
+
49
+ class FakeClustersInterface
50
+ def initialize(get_result: nil, get_error: nil, list_result: nil)
51
+ @get_result = get_result
52
+ @get_error = get_error
53
+ @list_result = list_result || {'clusters' => []}
54
+ end
55
+
56
+ def get(id)
57
+ raise @get_error if @get_error
58
+ @get_result
59
+ end
60
+
61
+ def list(params = {})
62
+ @list_result
63
+ end
64
+ end
65
+
66
+ # Bare host for the mixin under test.
67
+ class HelperHost
68
+ include Morpheus::Cli::InfrastructureHelper
69
+
70
+ def initialize(fake_interface, clusters_interface=nil)
71
+ @cloud_resource_pools_interface = fake_interface
72
+ @clusters_interface = clusters_interface if clusters_interface
73
+ end
74
+ end
75
+
76
+ def fake_not_found_error
77
+ response = Object.new
78
+ def response.code; 404; end
79
+ RestClient::Exception.new(response)
80
+ end
81
+
82
+ def fake_server_error
83
+ response = Object.new
84
+ def response.code; 500; end
85
+ RestClient::Exception.new(response)
86
+ end
87
+
88
+ def cloud
89
+ {'id' => 59, 'name' => 'vCenter Cloud'}
90
+ end
91
+
92
+ # print_red_alert writes straight to $stderr (print_helper.rb) - capture it.
93
+ def capture_stderr
94
+ original = $stderr
95
+ $stderr = StringIO.new
96
+ yield
97
+ $stderr.string
98
+ ensure
99
+ $stderr = original
100
+ end
101
+
102
+ ## find_resource_pool_cluster_by_id
103
+
104
+ def test_find_by_id_returns_the_pool_when_it_is_a_cluster_in_the_cloud
105
+ fake = FakeResourcePoolsInterface.new(get_result: {
106
+ 'resourcePool' => {'id' => 501, 'name' => 'QA', 'type' => 'Cluster', 'zone' => {'id' => 59, 'name' => 'vCenter Cloud'}}
107
+ })
108
+ helper = HelperHost.new(fake)
109
+ pool = helper.find_resource_pool_cluster_by_id(cloud, 501)
110
+ assert_not_nil pool
111
+ assert_equal 501, pool['id']
112
+ assert_equal 'QA', pool['name']
113
+ # Scoped to the cloud -- the server itself enforces ownership.
114
+ assert_equal [[59, 501, {}]], fake.get_calls
115
+ end
116
+
117
+ # ZoneResourcePoolsController#show 404s when the pool's zone does not match
118
+ # the requested cloud -- this is the server-side cloud-ownership check.
119
+ def test_find_by_id_reports_not_found_on_404_without_raising
120
+ fake = FakeResourcePoolsInterface.new(get_error: fake_not_found_error)
121
+ helper = HelperHost.new(fake)
122
+ pool = nil
123
+ output = capture_stderr { pool = helper.find_resource_pool_cluster_by_id(cloud, 999) }
124
+ assert_nil pool
125
+ assert_match(/not found by id 999/i, output)
126
+ assert_match(/vCenter Cloud/, output)
127
+ end
128
+
129
+ # A non-404 failure (auth, server error, etc.) must not be swallowed as a
130
+ # not-found -- that would hide a real problem behind a wrong error message.
131
+ def test_find_by_id_reraises_non_404_errors
132
+ fake = FakeResourcePoolsInterface.new(get_error: fake_server_error)
133
+ helper = HelperHost.new(fake)
134
+ assert_raise(RestClient::Exception) { helper.find_resource_pool_cluster_by_id(cloud, 501) }
135
+ end
136
+
137
+ def test_find_by_id_rejects_a_non_cluster_resource_pool
138
+ fake = FakeResourcePoolsInterface.new(get_result: {
139
+ 'resourcePool' => {'id' => 501, 'name' => 'Folder A', 'type' => 'ResourcePool', 'zone' => {'id' => 59, 'name' => 'vCenter Cloud'}}
140
+ })
141
+ helper = HelperHost.new(fake)
142
+ pool = nil
143
+ output = capture_stderr { pool = helper.find_resource_pool_cluster_by_id(cloud, 501) }
144
+ assert_nil pool
145
+ assert_match(/not a Cluster/i, output)
146
+ assert_match(/ResourcePool/, output)
147
+ end
148
+
149
+ ## find_resource_pool_cluster_by_name
150
+
151
+ def test_find_by_name_returns_the_single_match
152
+ fake = FakeResourcePoolsInterface.new(list_result: {
153
+ 'resourcePools' => [
154
+ {'id' => 501, 'name' => 'QA', 'type' => 'Cluster'},
155
+ ]
156
+ })
157
+ helper = HelperHost.new(fake)
158
+ pool = helper.find_resource_pool_cluster_by_name(cloud, 'QA')
159
+ assert_not_nil pool
160
+ assert_equal 501, pool['id']
161
+ assert_equal [[59, {name: 'QA', type: 'Cluster'}]], fake.list_calls
162
+ end
163
+
164
+ def test_find_by_name_matches_on_display_name
165
+ fake = FakeResourcePoolsInterface.new(list_result: {
166
+ 'resourcePools' => [
167
+ {'id' => 501, 'name' => 'internal-qa-01', 'displayName' => 'QA', 'type' => 'Cluster'},
168
+ ]
169
+ })
170
+ helper = HelperHost.new(fake)
171
+ pool = helper.find_resource_pool_cluster_by_name(cloud, 'QA')
172
+ assert_not_nil pool
173
+ assert_equal 501, pool['id']
174
+ end
175
+
176
+ def test_find_by_name_reports_not_found_with_a_managed_cluster_hint
177
+ fake = FakeResourcePoolsInterface.new(list_result: {'resourcePools' => []})
178
+ helper = HelperHost.new(fake)
179
+ pool = nil
180
+ output = capture_stderr { pool = helper.find_resource_pool_cluster_by_name(cloud, 'QA') }
181
+ assert_nil pool
182
+ assert_match(/not found by name 'QA'/i, output)
183
+ assert_match(/vCenter Cloud/, output)
184
+ assert_match(/--cluster 'QA' without --cloud/, output)
185
+ end
186
+
187
+ # The cloud-scoped resource-pools endpoint returns a pool *tree*: when the
188
+ # matched pool has a parent folder, ancestor entries are backfilled into the
189
+ # result so the hierarchy renders, even though they do not match the
190
+ # name/type filter. The resolver must not misreport this as ambiguity.
191
+ def test_find_by_name_ignores_ancestor_tree_padding
192
+ fake = FakeResourcePoolsInterface.new(list_result: {
193
+ 'resourcePools' => [
194
+ {'id' => 10, 'name' => 'Datacenter Folder', 'type' => 'Folder', 'depth' => 0},
195
+ {'id' => 501, 'name' => 'QA', 'type' => 'Cluster', 'depth' => 1},
196
+ ]
197
+ })
198
+ helper = HelperHost.new(fake)
199
+ pool = helper.find_resource_pool_cluster_by_name(cloud, 'QA')
200
+ assert_not_nil pool, "the ancestor folder must not be counted as a second match"
201
+ assert_equal 501, pool['id']
202
+ end
203
+
204
+ def test_find_by_name_reports_ambiguity_with_candidates_and_requires_numeric_id
205
+ fake = FakeResourcePoolsInterface.new(list_result: {
206
+ 'resourcePools' => [
207
+ {'id' => 501, 'name' => 'QA', 'type' => 'Cluster'},
208
+ {'id' => 502, 'name' => 'QA', 'type' => 'Cluster'},
209
+ ]
210
+ })
211
+ helper = HelperHost.new(fake)
212
+ pool = nil
213
+ output = capture_stderr { pool = helper.find_resource_pool_cluster_by_name(cloud, 'QA') }
214
+ assert_nil pool
215
+ assert_match(/2 clusters found by name 'QA'/i, output)
216
+ assert_match(/numeric cluster id/i, output)
217
+ end
218
+
219
+ ## find_resource_pool_cluster_by_name_or_id dispatch
220
+
221
+ def test_find_by_name_or_id_dispatches_numeric_strings_to_by_id
222
+ fake = FakeResourcePoolsInterface.new(get_result: {
223
+ 'resourcePool' => {'id' => 501, 'name' => 'QA', 'type' => 'Cluster', 'zone' => {'id' => 59}}
224
+ })
225
+ helper = HelperHost.new(fake)
226
+ pool = helper.find_resource_pool_cluster_by_name_or_id(cloud, '501')
227
+ assert_not_nil pool
228
+ assert_equal 1, fake.get_calls.size
229
+ assert_equal 0, fake.list_calls.size
230
+ end
231
+
232
+ def test_find_by_name_or_id_dispatches_non_numeric_strings_to_by_name
233
+ fake = FakeResourcePoolsInterface.new(list_result: {
234
+ 'resourcePools' => [{'id' => 501, 'name' => 'QA', 'type' => 'Cluster'}]
235
+ })
236
+ helper = HelperHost.new(fake)
237
+ pool = helper.find_resource_pool_cluster_by_name_or_id(cloud, 'QA')
238
+ assert_not_nil pool
239
+ assert_equal 0, fake.get_calls.size
240
+ assert_equal 1, fake.list_calls.size
241
+ end
242
+
243
+ def test_managed_cluster_lookup_reraises_non_404_errors
244
+ pools = FakeResourcePoolsInterface.new
245
+ clusters = FakeClustersInterface.new(get_error: fake_server_error)
246
+ helper = HelperHost.new(pools, clusters)
247
+ assert_raise(RestClient::Exception) { helper.find_managed_cluster_by_name_or_id('8') }
248
+ end
249
+
250
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: morpheus-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 9.0.2
4
+ version: 9.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Estes
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2026-08-26 00:00:00.000000000 Z
14
+ date: 2026-10-12 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: tins
@@ -218,6 +218,7 @@ files:
218
218
  - lib/morpheus/api/account_users_interface.rb
219
219
  - lib/morpheus/api/accounts_interface.rb
220
220
  - lib/morpheus/api/activity_interface.rb
221
+ - lib/morpheus/api/affinity_groups_interface.rb
221
222
  - lib/morpheus/api/api_client.rb
222
223
  - lib/morpheus/api/appliance_settings_interface.rb
223
224
  - lib/morpheus/api/approvals_interface.rb
@@ -257,6 +258,7 @@ files:
257
258
  - lib/morpheus/api/datastores_interface.rb
258
259
  - lib/morpheus/api/deploy_interface.rb
259
260
  - lib/morpheus/api/deployments_interface.rb
261
+ - lib/morpheus/api/distributed_workers_interface.rb
260
262
  - lib/morpheus/api/email_templates_interface.rb
261
263
  - lib/morpheus/api/environments_interface.rb
262
264
  - lib/morpheus/api/execute_schedules_interface.rb
@@ -268,6 +270,7 @@ files:
268
270
  - lib/morpheus/api/guidance_interface.rb
269
271
  - lib/morpheus/api/guidance_settings_interface.rb
270
272
  - lib/morpheus/api/health_interface.rb
273
+ - lib/morpheus/api/host_vm_groups_interface.rb
271
274
  - lib/morpheus/api/hub_interface.rb
272
275
  - lib/morpheus/api/image_builder_boot_scripts_interface.rb
273
276
  - lib/morpheus/api/image_builder_image_builds_interface.rb
@@ -410,6 +413,7 @@ files:
410
413
  - lib/morpheus/cli/commands/access_token_command.rb
411
414
  - lib/morpheus/cli/commands/account_groups_command.rb
412
415
  - lib/morpheus/cli/commands/activity_command.rb
416
+ - lib/morpheus/cli/commands/affinity_groups_command.rb
413
417
  - lib/morpheus/cli/commands/alias_command.rb
414
418
  - lib/morpheus/cli/commands/appliance_settings_command.rb
415
419
  - lib/morpheus/cli/commands/approvals_command.rb
@@ -450,6 +454,7 @@ files:
450
454
  - lib/morpheus/cli/commands/deploy.rb
451
455
  - lib/morpheus/cli/commands/deployments.rb
452
456
  - lib/morpheus/cli/commands/deploys.rb
457
+ - lib/morpheus/cli/commands/distributed_workers_command.rb
453
458
  - lib/morpheus/cli/commands/echo_command.rb
454
459
  - lib/morpheus/cli/commands/edit_profile_command.rb
455
460
  - lib/morpheus/cli/commands/edit_rc_command.rb
@@ -466,6 +471,7 @@ files:
466
471
  - lib/morpheus/cli/commands/guidance_settings.rb
467
472
  - lib/morpheus/cli/commands/health_command.rb
468
473
  - lib/morpheus/cli/commands/history_command.rb
474
+ - lib/morpheus/cli/commands/host_vm_groups_command.rb
469
475
  - lib/morpheus/cli/commands/hosts.rb
470
476
  - lib/morpheus/cli/commands/hub.rb
471
477
  - lib/morpheus/cli/commands/image_builder_command.rb
@@ -606,6 +612,7 @@ files:
606
612
  - lib/morpheus/cli/errors.rb
607
613
  - lib/morpheus/cli/expression_parser.rb
608
614
  - lib/morpheus/cli/mixins/accounts_helper.rb
615
+ - lib/morpheus/cli/mixins/affinity_helper.rb
609
616
  - lib/morpheus/cli/mixins/backups_helper.rb
610
617
  - lib/morpheus/cli/mixins/deployments_helper.rb
611
618
  - lib/morpheus/cli/mixins/execution_request_helper.rb
@@ -645,18 +652,32 @@ files:
645
652
  - lib/morpheus/terminal.rb
646
653
  - lib/morpheus/util.rb
647
654
  - morpheus-cli.gemspec
655
+ - test/api/affinity_groups_interface_test.rb
648
656
  - test/api/clients_interface_test.rb
657
+ - test/api/clouds_interface_test.rb
649
658
  - test/api/containers_interface_test.rb
659
+ - test/api/host_vm_groups_interface_test.rb
650
660
  - test/api/instances_interface_test.rb
651
661
  - test/api/network_routers_interface_test.rb
652
662
  - test/api/systems_interface_test.rb
653
663
  - test/api/whoami_interface_test.rb
654
664
  - test/cli/access_token_test.rb
665
+ - test/cli/affinity_groups_cloud_cluster_scope_test.rb
666
+ - test/cli/affinity_groups_cluster_scope_test.rb
667
+ - test/cli/affinity_groups_test.rb
668
+ - test/cli/affinity_groups_violations_scope_test.rb
669
+ - test/cli/affinity_helper_test.rb
655
670
  - test/cli/auth_test.rb
671
+ - test/cli/backups_test.rb
656
672
  - test/cli/cli_test.rb
657
673
  - test/cli/clients_test.rb
674
+ - test/cli/clusters_uuid_test.rb
658
675
  - test/cli/containers_test.rb
676
+ - test/cli/distributed_workers_test.rb
659
677
  - test/cli/help_test.rb
678
+ - test/cli/host_vm_groups_cluster_scope_test.rb
679
+ - test/cli/host_vm_groups_test.rb
680
+ - test/cli/infrastructure_helper_cluster_resolver_test.rb
660
681
  - test/cli/instances_test.rb
661
682
  - test/cli/man_test.rb
662
683
  - test/cli/migrations_test.rb
@@ -697,18 +718,32 @@ signing_key:
697
718
  specification_version: 4
698
719
  summary: Provides CLI Interface to the Morpheus Public/Private Cloud Appliance
699
720
  test_files:
721
+ - test/api/affinity_groups_interface_test.rb
700
722
  - test/api/clients_interface_test.rb
723
+ - test/api/clouds_interface_test.rb
701
724
  - test/api/containers_interface_test.rb
725
+ - test/api/host_vm_groups_interface_test.rb
702
726
  - test/api/instances_interface_test.rb
703
727
  - test/api/network_routers_interface_test.rb
704
728
  - test/api/systems_interface_test.rb
705
729
  - test/api/whoami_interface_test.rb
706
730
  - test/cli/access_token_test.rb
731
+ - test/cli/affinity_groups_cloud_cluster_scope_test.rb
732
+ - test/cli/affinity_groups_cluster_scope_test.rb
733
+ - test/cli/affinity_groups_test.rb
734
+ - test/cli/affinity_groups_violations_scope_test.rb
735
+ - test/cli/affinity_helper_test.rb
707
736
  - test/cli/auth_test.rb
737
+ - test/cli/backups_test.rb
708
738
  - test/cli/cli_test.rb
709
739
  - test/cli/clients_test.rb
740
+ - test/cli/clusters_uuid_test.rb
710
741
  - test/cli/containers_test.rb
742
+ - test/cli/distributed_workers_test.rb
711
743
  - test/cli/help_test.rb
744
+ - test/cli/host_vm_groups_cluster_scope_test.rb
745
+ - test/cli/host_vm_groups_test.rb
746
+ - test/cli/infrastructure_helper_cluster_resolver_test.rb
712
747
  - test/cli/instances_test.rb
713
748
  - test/cli/man_test.rb
714
749
  - test/cli/migrations_test.rb