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.
- checksums.yaml +4 -4
- data/Dockerfile +1 -1
- data/lib/morpheus/api/affinity_groups_interface.rb +46 -0
- data/lib/morpheus/api/api_client.rb +17 -0
- data/lib/morpheus/api/clouds_interface.rb +42 -0
- data/lib/morpheus/api/clusters_interface.rb +55 -1
- data/lib/morpheus/api/distributed_workers_interface.rb +41 -0
- data/lib/morpheus/api/host_vm_groups_interface.rb +37 -0
- data/lib/morpheus/api/servers_interface.rb +8 -0
- data/lib/morpheus/api/virtual_switches_interface.rb +9 -0
- data/lib/morpheus/cli/commands/affinity_groups_command.rb +554 -0
- data/lib/morpheus/cli/commands/backups_command.rb +11 -0
- data/lib/morpheus/cli/commands/clouds.rb +421 -29
- data/lib/morpheus/cli/commands/clusters.rb +507 -20
- data/lib/morpheus/cli/commands/distributed_workers_command.rb +282 -0
- data/lib/morpheus/cli/commands/host_vm_groups_command.rb +423 -0
- data/lib/morpheus/cli/commands/hosts.rb +72 -1
- data/lib/morpheus/cli/commands/instances.rb +12 -1
- data/lib/morpheus/cli/commands/networks_command.rb +35 -3
- data/lib/morpheus/cli/commands/virtual_switches_command.rb +336 -0
- data/lib/morpheus/cli/mixins/affinity_helper.rb +118 -0
- data/lib/morpheus/cli/mixins/infrastructure_helper.rb +143 -4
- data/lib/morpheus/cli/option_types.rb +36 -1
- data/lib/morpheus/cli/version.rb +1 -1
- data/test/api/affinity_groups_interface_test.rb +43 -0
- data/test/api/clouds_interface_test.rb +67 -0
- data/test/api/host_vm_groups_interface_test.rb +35 -0
- data/test/cli/affinity_groups_cloud_cluster_scope_test.rb +270 -0
- data/test/cli/affinity_groups_cluster_scope_test.rb +171 -0
- data/test/cli/affinity_groups_test.rb +57 -0
- data/test/cli/affinity_groups_violations_scope_test.rb +141 -0
- data/test/cli/affinity_helper_test.rb +367 -0
- data/test/cli/backups_test.rb +212 -0
- data/test/cli/clusters_uuid_test.rb +131 -0
- data/test/cli/distributed_workers_test.rb +68 -0
- data/test/cli/host_vm_groups_cluster_scope_test.rb +187 -0
- data/test/cli/host_vm_groups_test.rb +63 -0
- data/test/cli/infrastructure_helper_cluster_resolver_test.rb +250 -0
- 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
|