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.
- 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 +13 -0
- data/lib/morpheus/api/clouds_interface.rb +42 -0
- data/lib/morpheus/api/clusters_interface.rb +7 -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/cli/commands/affinity_groups_command.rb +554 -0
- data/lib/morpheus/cli/commands/backups_command.rb +1 -1
- data/lib/morpheus/cli/commands/clouds.rb +421 -29
- data/lib/morpheus/cli/commands/clusters.rb +37 -39
- 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/mixins/affinity_helper.rb +118 -0
- data/lib/morpheus/cli/mixins/infrastructure_helper.rb +137 -0
- 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 +37 -2
|
@@ -1501,13 +1501,48 @@ module Morpheus
|
|
|
1501
1501
|
end
|
|
1502
1502
|
|
|
1503
1503
|
def self.sort_option_types(option_types)
|
|
1504
|
-
if option_types.find {|it| it['fieldGroup'] || it['displayOrder'] }
|
|
1504
|
+
sorted = if option_types.find {|it| it['fieldGroup'] || it['displayOrder'] }
|
|
1505
1505
|
option_types.select {|it| (it['fieldGroup'] || 'default').casecmp?('default')}.sort {|a,b| a['displayOrder'].to_i <=> b['displayOrder'].to_i} +
|
|
1506
1506
|
option_types.reject {|it| ['default', 'advanced'].include?((it['fieldGroup'] || 'default').downcase)}.sort{|a,b| a['displayOrder'] <=> b['displayOrder']}.group_by{|it| it['fieldGroup']}.values.collect { |it| it.sort{|a,b| a['displayOrder'].to_i <=> b['displayOrder'].to_i}}.flatten +
|
|
1507
1507
|
option_types.select {|it| 'advanced'.casecmp?(it['fieldGroup'])}.sort {|a,b| a['displayOrder'].to_i <=> b['displayOrder'].to_i}
|
|
1508
1508
|
else
|
|
1509
1509
|
option_types
|
|
1510
1510
|
end
|
|
1511
|
+
reorder_option_type_dependencies(sorted)
|
|
1512
|
+
end
|
|
1513
|
+
|
|
1514
|
+
# displayOrder alone can put a field ahead of another field it depends on
|
|
1515
|
+
# via dependsOnCode/visibleOnCode (eg. a hidden, optionSource-populated field
|
|
1516
|
+
# with a high displayOrder that a lower displayOrder field is gated on). Move
|
|
1517
|
+
# any depended-upon option type ahead of the option type(s) that depend on it,
|
|
1518
|
+
# preserving displayOrder order otherwise.
|
|
1519
|
+
def self.reorder_option_type_dependencies(option_types)
|
|
1520
|
+
field_key_for = lambda {|it| it['fieldContext'].to_s.empty? ? it['fieldName'].to_s : "#{it['fieldContext']}.#{it['fieldName']}" }
|
|
1521
|
+
depends_on_codes_for = lambda {|option_type|
|
|
1522
|
+
depends_value = option_type['visibleOnCode'].to_s.empty? ? option_type['dependsOnCode'] : option_type['visibleOnCode']
|
|
1523
|
+
next [] if depends_value.to_s.empty? || depends_value == 'networkInterfaces'
|
|
1524
|
+
depends_value = depends_value[(depends_value.index('::') + 2)..-1] if depends_value.include?('::')
|
|
1525
|
+
depends_value.to_s.sub(',', ' ').split(' ').collect {|value| value.split(':')[0] }
|
|
1526
|
+
}
|
|
1527
|
+
result = option_types.dup
|
|
1528
|
+
passes = 0
|
|
1529
|
+
changed = true
|
|
1530
|
+
while changed && passes < result.size
|
|
1531
|
+
changed = false
|
|
1532
|
+
passes += 1
|
|
1533
|
+
result.each_with_index do |option_type, i|
|
|
1534
|
+
depends_on_codes_for.call(option_type).each do |depends_on_code|
|
|
1535
|
+
dep_index = result.index {|it| it['code'] == depends_on_code || field_key_for.call(it) == depends_on_code }
|
|
1536
|
+
if dep_index && dep_index > i
|
|
1537
|
+
result.insert(i, result.delete_at(dep_index))
|
|
1538
|
+
changed = true
|
|
1539
|
+
break
|
|
1540
|
+
end
|
|
1541
|
+
end
|
|
1542
|
+
break if changed
|
|
1543
|
+
end
|
|
1544
|
+
end
|
|
1545
|
+
result
|
|
1511
1546
|
end
|
|
1512
1547
|
|
|
1513
1548
|
def self.display_select_options(opt, select_options = [], paging = nil)
|
data/lib/morpheus/cli/version.rb
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require 'test/unit'
|
|
2
|
+
$LOAD_PATH.unshift(File.expand_path('../../lib', __dir__))
|
|
3
|
+
require 'morpheus/api/affinity_groups_interface'
|
|
4
|
+
|
|
5
|
+
class AffinityGroupsInterfaceTest < Test::Unit::TestCase
|
|
6
|
+
|
|
7
|
+
def setup
|
|
8
|
+
@affinity_groups_interface = Morpheus::AffinityGroupsInterface.new(access_token: 'token', url: 'https://example.test', verify_ssl: false)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_get_builds_bearer_authorization_header
|
|
12
|
+
request = @affinity_groups_interface.dry.get(123, details: true)
|
|
13
|
+
assert_equal :get, request[:method]
|
|
14
|
+
assert_equal 'https://example.test/api/affinity-groups/123', request[:url]
|
|
15
|
+
assert_equal({details: true}, request[:headers][:params])
|
|
16
|
+
assert_equal 'Bearer token', request[:headers][:authorization]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_update_builds_bearer_authorization_header
|
|
20
|
+
request = @affinity_groups_interface.dry.update(123, {name: 'rule-a'})
|
|
21
|
+
assert_equal :put, request[:method]
|
|
22
|
+
assert_equal 'https://example.test/api/affinity-groups/123', request[:url]
|
|
23
|
+
assert_equal({name: 'rule-a'}.to_json, request[:payload])
|
|
24
|
+
assert_equal 'Bearer token', request[:headers][:authorization]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_destroy_builds_bearer_authorization_header
|
|
28
|
+
request = @affinity_groups_interface.dry.destroy(123, force: true)
|
|
29
|
+
assert_equal :delete, request[:method]
|
|
30
|
+
assert_equal 'https://example.test/api/affinity-groups/123', request[:url]
|
|
31
|
+
assert_equal({force: true}, request[:headers][:params])
|
|
32
|
+
assert_equal 'Bearer token', request[:headers][:authorization]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_violations_builds_bearer_authorization_header
|
|
36
|
+
request = @affinity_groups_interface.dry.violations(scope: 'cloud')
|
|
37
|
+
assert_equal :get, request[:method]
|
|
38
|
+
assert_equal 'https://example.test/api/affinity-groups/violations', request[:url]
|
|
39
|
+
assert_equal({scope: 'cloud'}, request[:headers][:params])
|
|
40
|
+
assert_equal 'Bearer token', request[:headers][:authorization]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
require 'test/unit'
|
|
2
|
+
$LOAD_PATH.unshift(File.expand_path('../../lib', __dir__))
|
|
3
|
+
require 'morpheus/api/clouds_interface'
|
|
4
|
+
|
|
5
|
+
class CloudsInterfaceTest < Test::Unit::TestCase
|
|
6
|
+
|
|
7
|
+
def setup
|
|
8
|
+
@clouds_interface = Morpheus::CloudsInterface.new(access_token: 'token', url: 'https://example.test', verify_ssl: false)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_affinity_group_form_options_builds_bearer_authorization_header
|
|
12
|
+
request = @clouds_interface.dry.affinity_group_form_options(59, phase: 'create')
|
|
13
|
+
assert_equal :get, request[:method]
|
|
14
|
+
assert_equal 'https://example.test/api/zones/59/affinity-groups/form-options', request[:url]
|
|
15
|
+
assert_equal({phase: 'create'}, request[:headers][:params])
|
|
16
|
+
assert_equal 'Bearer token', request[:headers][:authorization]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_list_host_vm_groups_builds_bearer_authorization_header
|
|
20
|
+
request = @clouds_interface.dry.list_host_vm_groups(59, phrase: 'group')
|
|
21
|
+
assert_equal :get, request[:method]
|
|
22
|
+
assert_equal 'https://example.test/api/zones/59/host-vm-groups', request[:url]
|
|
23
|
+
assert_equal({phrase: 'group'}, request[:headers][:params])
|
|
24
|
+
assert_equal 'Bearer token', request[:headers][:authorization]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_get_host_vm_group_builds_bearer_authorization_header
|
|
28
|
+
request = @clouds_interface.dry.get_host_vm_group(59, 415, details: true)
|
|
29
|
+
assert_equal :get, request[:method]
|
|
30
|
+
assert_equal 'https://example.test/api/zones/59/host-vm-groups/415', request[:url]
|
|
31
|
+
assert_equal({details: true}, request[:headers][:params])
|
|
32
|
+
assert_equal 'Bearer token', request[:headers][:authorization]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_create_host_vm_group_builds_bearer_authorization_header
|
|
36
|
+
request = @clouds_interface.dry.create_host_vm_group(59, {name: 'group-a'})
|
|
37
|
+
assert_equal :post, request[:method]
|
|
38
|
+
assert_equal 'https://example.test/api/zones/59/host-vm-groups', request[:url]
|
|
39
|
+
assert_equal({name: 'group-a'}.to_json, request[:payload])
|
|
40
|
+
assert_equal 'Bearer token', request[:headers][:authorization]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_update_host_vm_group_builds_bearer_authorization_header
|
|
44
|
+
request = @clouds_interface.dry.update_host_vm_group(59, 415, {name: 'group-b'})
|
|
45
|
+
assert_equal :put, request[:method]
|
|
46
|
+
assert_equal 'https://example.test/api/zones/59/host-vm-groups/415', request[:url]
|
|
47
|
+
assert_equal({name: 'group-b'}.to_json, request[:payload])
|
|
48
|
+
assert_equal 'Bearer token', request[:headers][:authorization]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def test_destroy_host_vm_group_builds_bearer_authorization_header
|
|
52
|
+
request = @clouds_interface.dry.destroy_host_vm_group(59, 415, force: true)
|
|
53
|
+
assert_equal :delete, request[:method]
|
|
54
|
+
assert_equal 'https://example.test/api/zones/59/host-vm-groups/415', request[:url]
|
|
55
|
+
assert_equal({force: true}, request[:headers][:params])
|
|
56
|
+
assert_equal 'Bearer token', request[:headers][:authorization]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def test_host_vm_group_form_options_builds_bearer_authorization_header
|
|
60
|
+
request = @clouds_interface.dry.host_vm_group_form_options(59, scope: 'qa')
|
|
61
|
+
assert_equal :get, request[:method]
|
|
62
|
+
assert_equal 'https://example.test/api/zones/59/host-vm-groups/form-options', request[:url]
|
|
63
|
+
assert_equal({scope: 'qa'}, request[:headers][:params])
|
|
64
|
+
assert_equal 'Bearer token', request[:headers][:authorization]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require 'test/unit'
|
|
2
|
+
$LOAD_PATH.unshift(File.expand_path('../../lib', __dir__))
|
|
3
|
+
require 'morpheus/api/host_vm_groups_interface'
|
|
4
|
+
|
|
5
|
+
class HostVmGroupsInterfaceTest < Test::Unit::TestCase
|
|
6
|
+
|
|
7
|
+
def setup
|
|
8
|
+
@host_vm_groups_interface = Morpheus::HostVmGroupsInterface.new(access_token: 'token', url: 'https://example.test', verify_ssl: false)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_get_builds_bearer_authorization_header
|
|
12
|
+
request = @host_vm_groups_interface.dry.get(415, details: true)
|
|
13
|
+
assert_equal :get, request[:method]
|
|
14
|
+
assert_equal 'https://example.test/api/host-vm-groups/415', request[:url]
|
|
15
|
+
assert_equal({details: true}, request[:headers][:params])
|
|
16
|
+
assert_equal 'Bearer token', request[:headers][:authorization]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_update_builds_bearer_authorization_header
|
|
20
|
+
request = @host_vm_groups_interface.dry.update(415, {name: 'group-a'})
|
|
21
|
+
assert_equal :put, request[:method]
|
|
22
|
+
assert_equal 'https://example.test/api/host-vm-groups/415', request[:url]
|
|
23
|
+
assert_equal({name: 'group-a'}.to_json, request[:payload])
|
|
24
|
+
assert_equal 'Bearer token', request[:headers][:authorization]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_destroy_builds_bearer_authorization_header
|
|
28
|
+
request = @host_vm_groups_interface.dry.destroy(415, force: true)
|
|
29
|
+
assert_equal :delete, request[:method]
|
|
30
|
+
assert_equal 'https://example.test/api/host-vm-groups/415', request[:url]
|
|
31
|
+
assert_equal({force: true}, request[:headers][:params])
|
|
32
|
+
assert_equal 'Bearer token', request[:headers][:authorization]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
end
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
require 'test/unit'
|
|
2
|
+
require 'stringio'
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'morpheus'
|
|
5
|
+
|
|
6
|
+
# Command-level offline exercise of the MORPH-17279 --cloud + --cluster
|
|
7
|
+
# (vSphere cluster) scope for AffinityGroupsCommand and HostVmGroupsCommand
|
|
8
|
+
# list/add. Runs the real command methods end-to-end (option parsing, scope
|
|
9
|
+
# resolution, --dry-run request construction) against fake interfaces, so no
|
|
10
|
+
# live appliance or credentials are required.
|
|
11
|
+
#
|
|
12
|
+
# This complements infrastructure_helper_cluster_resolver_test.rb (which
|
|
13
|
+
# unit-tests the resolver in isolation) by proving the resolved pool actually
|
|
14
|
+
# flows into list (poolId query param) and add (pool.id in the create
|
|
15
|
+
# payload), exactly as it would against a real appliance.
|
|
16
|
+
class AffinityGroupsCloudClusterScopeTest < Test::Unit::TestCase
|
|
17
|
+
|
|
18
|
+
# Fake for Morpheus::CloudsInterface -- records every call, answers cloud
|
|
19
|
+
# name/id lookups, and answers .dry.* calls with a print_dry_run-compatible
|
|
20
|
+
# request hash (mirrors how Morpheus::APIClient#dry / #execute behave).
|
|
21
|
+
class FakeCloudsInterface
|
|
22
|
+
attr_reader :calls
|
|
23
|
+
|
|
24
|
+
def initialize(cloud:)
|
|
25
|
+
@cloud = cloud
|
|
26
|
+
@calls = []
|
|
27
|
+
@dry = false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def setopts(options); self; end
|
|
31
|
+
def dry; @dry = true; self; end
|
|
32
|
+
|
|
33
|
+
def get(id, params = {})
|
|
34
|
+
@calls << [:get, id, params]
|
|
35
|
+
{'zone' => @cloud}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def list(params = {})
|
|
39
|
+
@calls << [:list, params]
|
|
40
|
+
{'zones' => [@cloud]}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def list_affinity_groups(id, params = {})
|
|
44
|
+
@calls << [:list_affinity_groups, id, params]
|
|
45
|
+
respond({'affinityGroups' => []}) { {method: :get, url: "/api/zones/#{id}/affinity-groups", params: params} }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def create_affinity_group(id, payload)
|
|
49
|
+
@calls << [:create_affinity_group, id, payload]
|
|
50
|
+
respond({'affinityGroup' => {'id' => 1}}) { {method: :post, url: "/api/zones/#{id}/affinity-groups", payload: payload.to_json} }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def affinity_group_form_options(id, params = {})
|
|
54
|
+
@calls << [:affinity_group_form_options, id, params]
|
|
55
|
+
{'supportsCombinedRules' => true, 'availableVmGroups' => [], 'availableHostGroups' => []}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def list_host_vm_groups(id, params = {})
|
|
59
|
+
@calls << [:list_host_vm_groups, id, params]
|
|
60
|
+
respond({'hostVmGroups' => []}) { {method: :get, url: "/api/zones/#{id}/host-vm-groups", params: params} }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def create_host_vm_group(id, payload)
|
|
64
|
+
@calls << [:create_host_vm_group, id, payload]
|
|
65
|
+
respond({'hostVmGroup' => {'id' => 1}}) { {method: :post, url: "/api/zones/#{id}/host-vm-groups", payload: payload.to_json} }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def host_vm_group_form_options(id, params = {})
|
|
69
|
+
@calls << [:host_vm_group_form_options, id, params]
|
|
70
|
+
{'availableServers' => []}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def respond(real_result)
|
|
76
|
+
@dry ? yield : real_result
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Fake for Morpheus::CloudResourcePoolsInterface -- used by
|
|
81
|
+
# find_resource_pool_cluster_by_name_or_id.
|
|
82
|
+
class FakeResourcePoolsInterface
|
|
83
|
+
attr_reader :calls
|
|
84
|
+
|
|
85
|
+
def initialize(pool:)
|
|
86
|
+
@pool = pool
|
|
87
|
+
@calls = []
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def get(cloud_id, id, params = {})
|
|
91
|
+
@calls << [:get, cloud_id, id, params]
|
|
92
|
+
{'resourcePool' => @pool}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def list(cloud_id, params = {})
|
|
96
|
+
@calls << [:list, cloud_id, params]
|
|
97
|
+
{'resourcePools' => [@pool]}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def list_without_cloud(params = {})
|
|
101
|
+
@calls << [:list_without_cloud, params]
|
|
102
|
+
{'resourcePools' => [@pool]}
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
class FakeClustersInterface
|
|
107
|
+
attr_reader :calls
|
|
108
|
+
|
|
109
|
+
def initialize(clusters: [])
|
|
110
|
+
@clusters = clusters
|
|
111
|
+
@calls = []
|
|
112
|
+
@dry = false
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def setopts(options); self; end
|
|
116
|
+
def dry; @dry = true; self; end
|
|
117
|
+
|
|
118
|
+
def get(id, params = {})
|
|
119
|
+
@calls << [:get, id, params]
|
|
120
|
+
{'cluster' => @clusters.find {|cluster| cluster['id'].to_i == id.to_i}}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def list(params = {})
|
|
124
|
+
@calls << [:list, params]
|
|
125
|
+
{'clusters' => @clusters.select {|cluster| cluster['name'] == params[:name]}}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def list_affinity_groups(id, params = {})
|
|
129
|
+
@calls << [:list_affinity_groups, id, params]
|
|
130
|
+
respond({'affinityGroups' => []}) { {method: :get, url: "/api/clusters/#{id}/affinity-groups", params: params} }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def list_host_vm_groups(id, params = {})
|
|
134
|
+
@calls << [:list_host_vm_groups, id, params]
|
|
135
|
+
respond({'hostVmGroups' => []}) { {method: :get, url: "/api/clusters/#{id}/host-vm-groups", params: params} }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
def respond(real_result)
|
|
141
|
+
@dry ? yield : real_result
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def cloud
|
|
146
|
+
{'id' => 59, 'name' => 'vCenter Cloud'}
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def pool
|
|
150
|
+
{'id' => 501, 'name' => 'QA', 'type' => 'Cluster', 'zone' => {'id' => 59, 'name' => 'vCenter Cloud'}}
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def build_affinity_command(managed_clusters: [])
|
|
154
|
+
cmd = Morpheus::Cli::AffinityGroupsCommand.new
|
|
155
|
+
fake_clouds = FakeCloudsInterface.new(cloud: cloud)
|
|
156
|
+
fake_pools = FakeResourcePoolsInterface.new(pool: pool)
|
|
157
|
+
fake_clusters = FakeClustersInterface.new(clusters: managed_clusters)
|
|
158
|
+
cmd.define_singleton_method(:connect) do |opts|
|
|
159
|
+
instance_variable_set(:@affinity_groups_interface, nil)
|
|
160
|
+
instance_variable_set(:@clouds_interface, fake_clouds)
|
|
161
|
+
instance_variable_set(:@clusters_interface, fake_clusters)
|
|
162
|
+
instance_variable_set(:@cloud_resource_pools_interface, fake_pools)
|
|
163
|
+
end
|
|
164
|
+
[cmd, fake_clouds, fake_pools, fake_clusters]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def build_host_vm_command(managed_clusters: [])
|
|
168
|
+
cmd = Morpheus::Cli::HostVmGroupsCommand.new
|
|
169
|
+
fake_clouds = FakeCloudsInterface.new(cloud: cloud)
|
|
170
|
+
fake_pools = FakeResourcePoolsInterface.new(pool: pool)
|
|
171
|
+
fake_clusters = FakeClustersInterface.new(clusters: managed_clusters)
|
|
172
|
+
cmd.define_singleton_method(:connect) do |opts|
|
|
173
|
+
instance_variable_set(:@host_vm_groups_interface, nil)
|
|
174
|
+
instance_variable_set(:@clouds_interface, fake_clouds)
|
|
175
|
+
instance_variable_set(:@clusters_interface, fake_clusters)
|
|
176
|
+
instance_variable_set(:@cloud_resource_pools_interface, fake_pools)
|
|
177
|
+
end
|
|
178
|
+
[cmd, fake_clouds, fake_pools, fake_clusters]
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def capture_stdout
|
|
182
|
+
terminal = Morpheus::Terminal.instance
|
|
183
|
+
original = terminal.stdout
|
|
184
|
+
buffer = StringIO.new
|
|
185
|
+
terminal.set_stdout(buffer)
|
|
186
|
+
yield
|
|
187
|
+
buffer.string
|
|
188
|
+
ensure
|
|
189
|
+
terminal.set_stdout(original)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
## AffinityGroupsCommand
|
|
193
|
+
|
|
194
|
+
def test_affinity_groups_list_cloud_cluster_combo_sends_pool_id
|
|
195
|
+
cmd, _fake_clouds, fake_pools, _fake_clusters = build_affinity_command
|
|
196
|
+
output = capture_stdout { cmd.list(['--cloud', 'vCenter Cloud', '--cluster', 'QA', '--dry-run']) }
|
|
197
|
+
assert_match(/poolId=501/, output, "dry-run request must carry the resolved poolId, got: #{output}")
|
|
198
|
+
assert_match(%r{/api/zones/59/affinity-groups}, output)
|
|
199
|
+
# Name resolution stayed scoped to the cloud and to type=Cluster.
|
|
200
|
+
assert_equal [[:list, 59, {name: 'QA', type: 'Cluster'}]], fake_pools.calls
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def test_affinity_groups_add_cloud_cluster_combo_sets_pool_id_payload
|
|
204
|
+
cmd, fake_clouds, _fake_pools, _fake_clusters = build_affinity_command
|
|
205
|
+
payload_json = '{"affinityGroup":{"name":"cli-test","affinityType":"KEEP_TOGETHER","servers":[{"id":1}]}}'
|
|
206
|
+
output = capture_stdout do
|
|
207
|
+
cmd.add(['--cloud', 'vCenter Cloud', '--cluster', 'QA', '--payload-json', payload_json, '--dry-run'])
|
|
208
|
+
end
|
|
209
|
+
assert_match(%r{/api/zones/59/affinity-groups}, output, "must POST to the zone endpoint, not a cluster endpoint")
|
|
210
|
+
create_call = fake_clouds.calls.find {|c| c[0] == :create_affinity_group }
|
|
211
|
+
assert_not_nil create_call, "create_affinity_group must be called on the clouds interface"
|
|
212
|
+
payload = create_call[2].is_a?(String) ? JSON.parse(create_call[2]) : create_call[2]
|
|
213
|
+
assert_equal({'id' => 501}, payload['affinityGroup']['pool'])
|
|
214
|
+
assert_nil payload['affinityGroup']['cloudId']
|
|
215
|
+
assert_nil payload['affinityGroup']['clusterId']
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def test_affinity_groups_list_cluster_name_falls_back_to_unique_vsphere_cluster
|
|
219
|
+
cmd, _fake_clouds, fake_pools, fake_clusters = build_affinity_command
|
|
220
|
+
output = capture_stdout { cmd.list(['--cluster', 'QA', '--dry-run']) }
|
|
221
|
+
assert_match(%r{/api/zones/59/affinity-groups}, output)
|
|
222
|
+
assert_match(/poolId=501/, output)
|
|
223
|
+
assert_equal [[:list, {name: 'QA'}]], fake_clusters.calls
|
|
224
|
+
assert_equal [[:list_without_cloud, {name: 'QA', type: 'Cluster', max: 1000}]], fake_pools.calls
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def test_affinity_groups_list_cluster_name_preserves_managed_cluster_precedence
|
|
228
|
+
managed_cluster = {'id' => 8, 'name' => 'QA'}
|
|
229
|
+
cmd, _fake_clouds, fake_pools, fake_clusters = build_affinity_command(managed_clusters: [managed_cluster])
|
|
230
|
+
output = capture_stdout { cmd.list(['--cluster', 'QA', '--dry-run']) }
|
|
231
|
+
assert_match(%r{/api/clusters/8/affinity-groups}, output)
|
|
232
|
+
assert_equal [[:list, {name: 'QA'}], [:list_affinity_groups, 8, {}]], fake_clusters.calls
|
|
233
|
+
assert_empty fake_pools.calls
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
## HostVmGroupsCommand
|
|
237
|
+
|
|
238
|
+
def test_host_vm_groups_list_cloud_cluster_combo_sends_pool_id
|
|
239
|
+
cmd, _fake_clouds, fake_pools, _fake_clusters = build_host_vm_command
|
|
240
|
+
output = capture_stdout { cmd.list(['--cloud', 'vCenter Cloud', '--cluster', 'QA', '--dry-run']) }
|
|
241
|
+
assert_match(/poolId=501/, output, "dry-run request must carry the resolved poolId, got: #{output}")
|
|
242
|
+
assert_match(%r{/api/zones/59/host-vm-groups}, output)
|
|
243
|
+
assert_equal [[:list, 59, {name: 'QA', type: 'Cluster'}]], fake_pools.calls
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def test_host_vm_groups_add_cloud_cluster_combo_sets_pool_id_payload
|
|
247
|
+
cmd, fake_clouds, _fake_pools, _fake_clusters = build_host_vm_command
|
|
248
|
+
payload_json = '{"hostVmGroup":{"name":"cli-test","type":"HOST_GROUP","servers":[{"id":1}]}}'
|
|
249
|
+
output = capture_stdout do
|
|
250
|
+
cmd.add(['--cloud', 'vCenter Cloud', '--cluster', 'QA', '--payload-json', payload_json, '--dry-run'])
|
|
251
|
+
end
|
|
252
|
+
assert_match(%r{/api/zones/59/host-vm-groups}, output, "must POST to the zone endpoint, not a cluster endpoint")
|
|
253
|
+
create_call = fake_clouds.calls.find {|c| c[0] == :create_host_vm_group }
|
|
254
|
+
assert_not_nil create_call, "create_host_vm_group must be called on the clouds interface"
|
|
255
|
+
payload = create_call[2].is_a?(String) ? JSON.parse(create_call[2]) : create_call[2]
|
|
256
|
+
assert_equal({'id' => 501}, payload['hostVmGroup']['pool'])
|
|
257
|
+
assert_nil payload['hostVmGroup']['cloudId']
|
|
258
|
+
assert_nil payload['hostVmGroup']['clusterId']
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def test_host_vm_groups_list_cluster_name_falls_back_to_unique_vsphere_cluster
|
|
262
|
+
cmd, _fake_clouds, fake_pools, fake_clusters = build_host_vm_command
|
|
263
|
+
output = capture_stdout { cmd.list(['--cluster', 'QA', '--dry-run']) }
|
|
264
|
+
assert_match(%r{/api/zones/59/host-vm-groups}, output)
|
|
265
|
+
assert_match(/poolId=501/, output)
|
|
266
|
+
assert_equal [[:list, {name: 'QA'}]], fake_clusters.calls
|
|
267
|
+
assert_equal [[:list_without_cloud, {name: 'QA', type: 'Cluster', max: 1000}]], fake_pools.calls
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
end
|
|
@@ -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
|