morpheus-cli 9.0.1 → 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 (39) 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 +17 -0
  5. data/lib/morpheus/api/clouds_interface.rb +42 -0
  6. data/lib/morpheus/api/clusters_interface.rb +55 -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/api/servers_interface.rb +8 -0
  10. data/lib/morpheus/api/virtual_switches_interface.rb +9 -0
  11. data/lib/morpheus/cli/commands/affinity_groups_command.rb +554 -0
  12. data/lib/morpheus/cli/commands/backups_command.rb +11 -0
  13. data/lib/morpheus/cli/commands/clouds.rb +421 -29
  14. data/lib/morpheus/cli/commands/clusters.rb +507 -20
  15. data/lib/morpheus/cli/commands/distributed_workers_command.rb +282 -0
  16. data/lib/morpheus/cli/commands/host_vm_groups_command.rb +423 -0
  17. data/lib/morpheus/cli/commands/hosts.rb +72 -1
  18. data/lib/morpheus/cli/commands/instances.rb +12 -1
  19. data/lib/morpheus/cli/commands/networks_command.rb +35 -3
  20. data/lib/morpheus/cli/commands/virtual_switches_command.rb +336 -0
  21. data/lib/morpheus/cli/mixins/affinity_helper.rb +118 -0
  22. data/lib/morpheus/cli/mixins/infrastructure_helper.rb +143 -4
  23. data/lib/morpheus/cli/option_types.rb +36 -1
  24. data/lib/morpheus/cli/version.rb +1 -1
  25. data/test/api/affinity_groups_interface_test.rb +43 -0
  26. data/test/api/clouds_interface_test.rb +67 -0
  27. data/test/api/host_vm_groups_interface_test.rb +35 -0
  28. data/test/cli/affinity_groups_cloud_cluster_scope_test.rb +270 -0
  29. data/test/cli/affinity_groups_cluster_scope_test.rb +171 -0
  30. data/test/cli/affinity_groups_test.rb +57 -0
  31. data/test/cli/affinity_groups_violations_scope_test.rb +141 -0
  32. data/test/cli/affinity_helper_test.rb +367 -0
  33. data/test/cli/backups_test.rb +212 -0
  34. data/test/cli/clusters_uuid_test.rb +131 -0
  35. data/test/cli/distributed_workers_test.rb +68 -0
  36. data/test/cli/host_vm_groups_cluster_scope_test.rb +187 -0
  37. data/test/cli/host_vm_groups_test.rb +63 -0
  38. data/test/cli/infrastructure_helper_cluster_resolver_test.rb +250 -0
  39. metadata +39 -2
@@ -0,0 +1,171 @@
1
+ require 'test/unit'
2
+ require 'stringio'
3
+ require 'morpheus'
4
+
5
+ class AffinityGroupsClusterScopeTest < Test::Unit::TestCase
6
+ class FakeCloudsInterface
7
+ attr_reader :calls
8
+
9
+ def initialize(cloud)
10
+ @cloud = cloud
11
+ @calls = []
12
+ @dry = false
13
+ end
14
+
15
+ def setopts(_options)
16
+ self
17
+ end
18
+
19
+ def dry
20
+ @dry = true
21
+ self
22
+ end
23
+
24
+ def get(id, params = {})
25
+ @calls << [:get, id, params]
26
+ {'zone' => @cloud}
27
+ end
28
+
29
+ def list(params = {})
30
+ @calls << [:list, params]
31
+ {'zones' => [@cloud]}
32
+ end
33
+
34
+ def list_affinity_groups(id, params = {})
35
+ @calls << [:list_affinity_groups, id, params]
36
+ return {'affinityGroups' => []} unless @dry
37
+
38
+ {method: :get, url: "/api/zones/#{id}/affinity-groups", params: params}
39
+ end
40
+ end
41
+
42
+ class FakeResourcePoolsInterface
43
+ attr_reader :calls
44
+
45
+ def initialize(pool)
46
+ @pool = pool
47
+ @calls = []
48
+ end
49
+
50
+ def get(cloud_id, id, params = {})
51
+ @calls << [:get, cloud_id, id, params]
52
+ {'resourcePool' => @pool}
53
+ end
54
+
55
+ def list(cloud_id, params = {})
56
+ @calls << [:list, cloud_id, params]
57
+ {'resourcePools' => [@pool]}
58
+ end
59
+
60
+ def list_without_cloud(params = {})
61
+ @calls << [:list_without_cloud, params]
62
+ {'resourcePools' => [@pool]}
63
+ end
64
+ end
65
+
66
+ class FakeClustersInterface
67
+ attr_reader :calls
68
+
69
+ def initialize(clusters = [])
70
+ @clusters = clusters
71
+ @calls = []
72
+ @dry = false
73
+ end
74
+
75
+ def setopts(_options)
76
+ self
77
+ end
78
+
79
+ def dry
80
+ @dry = true
81
+ self
82
+ end
83
+
84
+ def list(params = {})
85
+ @calls << [:list, params]
86
+ {'clusters' => @clusters.select {|cluster| cluster['name'] == params[:name]}}
87
+ end
88
+
89
+ def list_affinity_groups(id, params = {})
90
+ @calls << [:list_affinity_groups, id, params]
91
+ return {'affinityGroups' => []} unless @dry
92
+
93
+ {method: :get, url: "/api/clusters/#{id}/affinity-groups", params: params}
94
+ end
95
+ end
96
+
97
+ def setup
98
+ @cloud = {'id' => 59, 'name' => 'vCenter Cloud'}
99
+ @pool = {
100
+ 'id' => 501,
101
+ 'name' => 'QA',
102
+ 'type' => 'Cluster',
103
+ 'zone' => @cloud
104
+ }
105
+ end
106
+
107
+ def test_cloud_qualified_cluster_sends_pool_id
108
+ command, pools, = build_command
109
+
110
+ output = capture_stdout do
111
+ command.list(['--cloud', 'vCenter Cloud', '--cluster', 'QA', '--dry-run'])
112
+ end
113
+
114
+ assert_match(%r{/api/zones/59/affinity-groups}, output)
115
+ assert_match(/poolId=501/, output)
116
+ assert_equal [[:list, 59, {name: 'QA', type: 'Cluster'}]], pools.calls
117
+ end
118
+
119
+ def test_cluster_name_falls_back_to_unique_vsphere_cluster
120
+ command, pools, clusters = build_command
121
+
122
+ output = capture_stdout { command.list(['--cluster', 'QA', '--dry-run']) }
123
+
124
+ assert_match(%r{/api/zones/59/affinity-groups}, output)
125
+ assert_match(/poolId=501/, output)
126
+ assert_equal [[:list, {name: 'QA'}]], clusters.calls
127
+ assert_equal [
128
+ [:list_without_cloud, {name: 'QA', type: 'Cluster', max: 1000}]
129
+ ], pools.calls
130
+ end
131
+
132
+ def test_managed_cluster_takes_precedence
133
+ managed_cluster = {'id' => 8, 'name' => 'QA'}
134
+ command, pools, clusters = build_command([managed_cluster])
135
+
136
+ output = capture_stdout { command.list(['--cluster', 'QA', '--dry-run']) }
137
+
138
+ assert_match(%r{/api/clusters/8/affinity-groups}, output)
139
+ assert_equal [
140
+ [:list, {name: 'QA'}],
141
+ [:list_affinity_groups, 8, {}]
142
+ ], clusters.calls
143
+ assert_empty pools.calls
144
+ end
145
+
146
+ private
147
+
148
+ def build_command(managed_clusters = [])
149
+ clouds = FakeCloudsInterface.new(@cloud)
150
+ pools = FakeResourcePoolsInterface.new(@pool)
151
+ clusters = FakeClustersInterface.new(managed_clusters)
152
+ command = Morpheus::Cli::AffinityGroupsCommand.new
153
+ command.define_singleton_method(:connect) do |_options|
154
+ @clouds_interface = clouds
155
+ @clusters_interface = clusters
156
+ @cloud_resource_pools_interface = pools
157
+ end
158
+ [command, pools, clusters]
159
+ end
160
+
161
+ def capture_stdout
162
+ terminal = Morpheus::Terminal.instance
163
+ original = terminal.stdout
164
+ buffer = StringIO.new
165
+ terminal.set_stdout(buffer)
166
+ yield
167
+ buffer.string
168
+ ensure
169
+ terminal.set_stdout(original)
170
+ end
171
+ end
@@ -0,0 +1,57 @@
1
+ require 'morpheus_test'
2
+
3
+ # Tests for Morpheus::Cli::AffinityGroupsCommand
4
+ #
5
+ # Scope note: these cover only client-side behaviour -- help output and argument
6
+ # validation -- both of which resolve before any API call is made.
7
+ #
8
+ # Tests that would exercise list/get/add/update/remove against the appliance are
9
+ # deliberately omitted until the /api/affinity-groups request and response
10
+ # contract is confirmed against the MORPH-14136 / MORPH-16074 server work.
11
+ # See test/cli/affinity_helper_test.rb for the offline coverage of the shared
12
+ # affinity enums and formatters.
13
+ class MorpheusTest::AffinityGroupsTest < MorpheusTest::TestCase
14
+
15
+ def test_affinity_groups_help
16
+ assert_execute %(affinity-groups --help)
17
+ assert_execute %(affinity-groups list --help)
18
+ assert_execute %(affinity-groups get --help)
19
+ assert_execute %(affinity-groups add --help)
20
+ assert_execute %(affinity-groups update --help)
21
+ assert_execute %(affinity-groups remove --help)
22
+ end
23
+
24
+ ## Argument validation (fails during option parsing, before any request)
25
+
26
+ def test_affinity_groups_unknown_subcommand
27
+ assert_error %(affinity-groups nonsense-subcommand)
28
+ end
29
+
30
+ def test_affinity_groups_get_requires_an_id
31
+ assert_error %(affinity-groups get)
32
+ end
33
+
34
+ def test_affinity_groups_update_requires_an_id
35
+ assert_error %(affinity-groups update)
36
+ end
37
+
38
+ def test_affinity_groups_remove_requires_an_id
39
+ assert_error %(affinity-groups remove -y)
40
+ end
41
+
42
+ def test_affinity_groups_list_rejects_unknown_option
43
+ assert_error %(affinity-groups list --not-a-real-option)
44
+ end
45
+
46
+ # There is no unscoped list/create route -- morpheus-ui ApiV1UrlMappings
47
+ # exposes only /api/affinity-groups/{id} at the top level, so list and add
48
+ # must refuse to run without --cloud or --cluster rather than 404.
49
+ def test_affinity_groups_list_requires_a_scope
50
+ assert_error %(affinity-groups list)
51
+ end
52
+
53
+ def test_affinity_groups_add_requires_a_scope
54
+ assert_error %(affinity-groups add --name cli-test --dry-run)
55
+ end
56
+
57
+ end
@@ -0,0 +1,141 @@
1
+ require 'test/unit'
2
+ require 'stringio'
3
+ require 'morpheus'
4
+
5
+ # Offline exercise of the MORPH-17279 --cluster scope for
6
+ # AffinityGroupsCommand#violations. Unlike list/add, the violations endpoint
7
+ # (AffinityGroupsController#violations) only accepts clusterId or cloudId -- it
8
+ # has no poolId path and no vSphere-pool-scoped violation data. So a managed
9
+ # cluster name resolves to clusterId, while a cloud-owned vSphere cluster name
10
+ # must produce a clear error instead of silently exiting 1 or broadening scope.
11
+ class AffinityGroupsViolationsScopeTest < Test::Unit::TestCase
12
+ class FakeAffinityGroupsInterface
13
+ attr_reader :calls
14
+
15
+ def initialize
16
+ @calls = []
17
+ @dry = false
18
+ end
19
+
20
+ def setopts(_options)
21
+ self
22
+ end
23
+
24
+ def dry
25
+ @dry = true
26
+ self
27
+ end
28
+
29
+ def violations(params = {})
30
+ @calls << [:violations, params]
31
+ return {'violations' => []} unless @dry
32
+
33
+ {method: :get, url: "/api/affinity-groups/violations", params: params}
34
+ end
35
+ end
36
+
37
+ class FakeCloudsInterface
38
+ def initialize(cloud)
39
+ @cloud = cloud
40
+ end
41
+
42
+ def setopts(_options); self; end
43
+ def dry; self; end
44
+ def get(id, params = {}); {'zone' => @cloud}; end
45
+ def list(params = {}); {'zones' => [@cloud]}; end
46
+ end
47
+
48
+ class FakeResourcePoolsInterface
49
+ def initialize(pools)
50
+ @pools = pools
51
+ end
52
+
53
+ def get(cloud_id, id, params = {})
54
+ {'resourcePool' => @pools.find {|p| p['id'].to_i == id.to_i }}
55
+ end
56
+
57
+ def list(cloud_id, params = {})
58
+ {'resourcePools' => @pools}
59
+ end
60
+
61
+ def list_without_cloud(params = {})
62
+ {'resourcePools' => @pools}
63
+ end
64
+ end
65
+
66
+ class FakeClustersInterface
67
+ def initialize(clusters = [])
68
+ @clusters = clusters
69
+ end
70
+
71
+ def list(params = {})
72
+ {'clusters' => @clusters.select {|cluster| cluster['name'] == params[:name]}}
73
+ end
74
+ end
75
+
76
+ def setup
77
+ @cloud = {'id' => 59, 'name' => 'vCenter Cloud'}
78
+ @pool = {'id' => 501, 'name' => 'QA', 'type' => 'Cluster', 'zone' => @cloud}
79
+ end
80
+
81
+ def test_managed_cluster_name_scopes_violations_by_cluster_id
82
+ command, affinity = build_command([{'id' => 8, 'name' => 'QA'}])
83
+
84
+ output = capture_stdout { command.violations(['--cluster', 'QA', '--dry-run']) }
85
+
86
+ assert_match(%r{/api/affinity-groups/violations}, output)
87
+ assert_match(/clusterId=8/, output)
88
+ assert_equal [[:violations, {'clusterId' => 8}]], affinity.calls
89
+ end
90
+
91
+ def test_vsphere_cluster_name_reports_clear_error_without_calling_violations
92
+ command, affinity = build_command([])
93
+
94
+ result = nil
95
+ err = capture_stderr do
96
+ capture_stdout { result = command.violations(['--cluster', 'QA', '--dry-run']) }
97
+ end
98
+
99
+ assert_equal 1, (result.is_a?(Array) ? result[0] : result)
100
+ assert_match(/only available for Morpheus managed clusters/i, err)
101
+ assert_match(/vCenter Cloud/, err)
102
+ assert_empty affinity.calls
103
+ end
104
+
105
+ private
106
+
107
+ def build_command(managed_clusters = [])
108
+ affinity = FakeAffinityGroupsInterface.new
109
+ clouds = FakeCloudsInterface.new(@cloud)
110
+ pools = FakeResourcePoolsInterface.new([@pool])
111
+ clusters = FakeClustersInterface.new(managed_clusters)
112
+ command = Morpheus::Cli::AffinityGroupsCommand.new
113
+ command.define_singleton_method(:connect) do |_options|
114
+ @affinity_groups_interface = affinity
115
+ @clouds_interface = clouds
116
+ @clusters_interface = clusters
117
+ @cloud_resource_pools_interface = pools
118
+ end
119
+ [command, affinity]
120
+ end
121
+
122
+ def capture_stdout
123
+ terminal = Morpheus::Terminal.instance
124
+ original = terminal.stdout
125
+ buffer = StringIO.new
126
+ terminal.set_stdout(buffer)
127
+ yield
128
+ buffer.string
129
+ ensure
130
+ terminal.set_stdout(original)
131
+ end
132
+
133
+ def capture_stderr
134
+ original = $stderr
135
+ $stderr = StringIO.new
136
+ yield
137
+ $stderr.string
138
+ ensure
139
+ $stderr = original
140
+ end
141
+ end