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
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
require 'morpheus/cli/cli_command'
|
|
2
|
+
|
|
3
|
+
class Morpheus::Cli::AffinityGroupsCommand
|
|
4
|
+
include Morpheus::Cli::CliCommand
|
|
5
|
+
include Morpheus::Cli::ProvisioningHelper
|
|
6
|
+
include Morpheus::Cli::WhoamiHelper
|
|
7
|
+
include Morpheus::Cli::InfrastructureHelper
|
|
8
|
+
include Morpheus::Cli::AffinityHelper
|
|
9
|
+
|
|
10
|
+
set_command_name :'affinity-groups'
|
|
11
|
+
|
|
12
|
+
register_subcommands :list, :get, :add, :update, :remove, :violations
|
|
13
|
+
|
|
14
|
+
def connect(opts)
|
|
15
|
+
@api_client = establish_remote_appliance_connection(opts)
|
|
16
|
+
@affinity_groups_interface = @api_client.affinity_groups
|
|
17
|
+
@clouds_interface = @api_client.clouds
|
|
18
|
+
@clusters_interface = @api_client.clusters
|
|
19
|
+
@cloud_resource_pools_interface = @api_client.cloud_resource_pools
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def handle(args)
|
|
23
|
+
handle_subcommand(args)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def list(args)
|
|
27
|
+
options = {}
|
|
28
|
+
params = {}
|
|
29
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
|
30
|
+
opts.banner = subcommand_usage()
|
|
31
|
+
opts.on('--cloud CLOUD', String, "Cloud name or id. Combine with --cluster to scope to a vSphere cluster (resource pool) owned by this cloud.") do |val|
|
|
32
|
+
options[:cloud] = val
|
|
33
|
+
end
|
|
34
|
+
opts.on('--cluster CLUSTER', String, "Cluster name or id. A unique name can select a managed or vSphere cluster; combine with --cloud to disambiguate a vSphere cluster.") do |val|
|
|
35
|
+
options[:cluster] = val
|
|
36
|
+
end
|
|
37
|
+
build_standard_list_options(opts, options)
|
|
38
|
+
opts.footer = "List affinity groups.\n" \
|
|
39
|
+
"Requires a scope: use --cloud, --cluster, or --cloud CLOUD --cluster CLUSTER. There is no unscoped list endpoint."
|
|
40
|
+
end
|
|
41
|
+
optparse.parse!(args)
|
|
42
|
+
verify_args!(args:args, optparse:optparse, count:0)
|
|
43
|
+
connect(options)
|
|
44
|
+
|
|
45
|
+
if options[:cloud] && options[:cluster]
|
|
46
|
+
cloud = find_cloud_by_name_or_id(options[:cloud])
|
|
47
|
+
return 1 if cloud.nil?
|
|
48
|
+
pool = find_resource_pool_cluster_by_name_or_id(cloud, options[:cluster])
|
|
49
|
+
return 1 if pool.nil?
|
|
50
|
+
params.merge!(parse_list_options(options))
|
|
51
|
+
params['poolId'] = pool['id']
|
|
52
|
+
@clouds_interface.setopts(options)
|
|
53
|
+
if options[:dry_run]
|
|
54
|
+
print_dry_run @clouds_interface.dry.list_affinity_groups(cloud['id'], params)
|
|
55
|
+
return
|
|
56
|
+
end
|
|
57
|
+
json_response = @clouds_interface.list_affinity_groups(cloud['id'], params)
|
|
58
|
+
render_response(json_response, options, 'affinityGroups') do
|
|
59
|
+
affinity_groups = json_response['affinityGroups']
|
|
60
|
+
print_h1 "Affinity Groups: #{cloud['name']} / #{pool['name']}", parse_list_subtitles(options), options
|
|
61
|
+
if affinity_groups.empty?
|
|
62
|
+
print cyan, "No affinity groups found.", reset, "\n"
|
|
63
|
+
else
|
|
64
|
+
print as_pretty_table(affinity_groups, affinity_group_list_columns, options)
|
|
65
|
+
print_results_pagination(json_response)
|
|
66
|
+
end
|
|
67
|
+
print reset, "\n"
|
|
68
|
+
end
|
|
69
|
+
elsif options[:cloud]
|
|
70
|
+
cloud = find_cloud_by_name_or_id(options[:cloud])
|
|
71
|
+
return 1 if cloud.nil?
|
|
72
|
+
params.merge!(parse_list_options(options))
|
|
73
|
+
@clouds_interface.setopts(options)
|
|
74
|
+
if options[:dry_run]
|
|
75
|
+
print_dry_run @clouds_interface.dry.list_affinity_groups(cloud['id'], params)
|
|
76
|
+
return
|
|
77
|
+
end
|
|
78
|
+
json_response = @clouds_interface.list_affinity_groups(cloud['id'], params)
|
|
79
|
+
render_response(json_response, options, 'affinityGroups') do
|
|
80
|
+
affinity_groups = json_response['affinityGroups']
|
|
81
|
+
print_h1 "Affinity Groups: #{cloud['name']}", parse_list_subtitles(options), options
|
|
82
|
+
if affinity_groups.empty?
|
|
83
|
+
print cyan, "No affinity groups found.", reset, "\n"
|
|
84
|
+
else
|
|
85
|
+
print as_pretty_table(affinity_groups, affinity_group_list_columns, options)
|
|
86
|
+
print_results_pagination(json_response)
|
|
87
|
+
end
|
|
88
|
+
print reset, "\n"
|
|
89
|
+
end
|
|
90
|
+
elsif options[:cluster]
|
|
91
|
+
cluster_scope = find_cluster_list_scope_by_name_or_id(options[:cluster])
|
|
92
|
+
return 1 if cluster_scope.nil?
|
|
93
|
+
params.merge!(parse_list_options(options))
|
|
94
|
+
if cluster_scope[:type] == :managed_cluster
|
|
95
|
+
cluster = cluster_scope[:cluster]
|
|
96
|
+
@clusters_interface.setopts(options)
|
|
97
|
+
if options[:dry_run]
|
|
98
|
+
print_dry_run @clusters_interface.dry.list_affinity_groups(cluster['id'], params)
|
|
99
|
+
return
|
|
100
|
+
end
|
|
101
|
+
json_response = @clusters_interface.list_affinity_groups(cluster['id'], params)
|
|
102
|
+
scope_name = cluster['name']
|
|
103
|
+
else
|
|
104
|
+
cloud = cluster_scope[:cloud]
|
|
105
|
+
pool = cluster_scope[:pool]
|
|
106
|
+
params['poolId'] = pool['id']
|
|
107
|
+
@clouds_interface.setopts(options)
|
|
108
|
+
if options[:dry_run]
|
|
109
|
+
print_dry_run @clouds_interface.dry.list_affinity_groups(cloud['id'], params)
|
|
110
|
+
return
|
|
111
|
+
end
|
|
112
|
+
json_response = @clouds_interface.list_affinity_groups(cloud['id'], params)
|
|
113
|
+
scope_name = "#{cloud['name']} / #{pool['name']}"
|
|
114
|
+
end
|
|
115
|
+
render_response(json_response, options, 'affinityGroups') do
|
|
116
|
+
affinity_groups = json_response['affinityGroups']
|
|
117
|
+
print_h1 "Affinity Groups: #{scope_name}", parse_list_subtitles(options), options
|
|
118
|
+
if affinity_groups.empty?
|
|
119
|
+
print cyan, "No affinity groups found.", reset, "\n"
|
|
120
|
+
else
|
|
121
|
+
print as_pretty_table(affinity_groups, affinity_group_list_columns, options)
|
|
122
|
+
print_results_pagination(json_response)
|
|
123
|
+
end
|
|
124
|
+
print reset, "\n"
|
|
125
|
+
end
|
|
126
|
+
else
|
|
127
|
+
# There is no top-level list route: morpheus-ui ApiV1UrlMappings exposes
|
|
128
|
+
# only /api/affinity-groups/{id} for show/update/delete. List stays scoped.
|
|
129
|
+
print_error Morpheus::Terminal.angry_prompt
|
|
130
|
+
puts_error "#{command_name} list requires a scope: pass --cloud CLOUD or --cluster CLUSTER\n#{optparse}"
|
|
131
|
+
return 1, "scope required"
|
|
132
|
+
end
|
|
133
|
+
return 0, nil
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def get(args)
|
|
137
|
+
options = {}
|
|
138
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
|
139
|
+
opts.banner = subcommand_usage("[id]")
|
|
140
|
+
build_standard_get_options(opts, options)
|
|
141
|
+
opts.footer = "Get details about an affinity group.\n" \
|
|
142
|
+
"[id] is required. This is the numeric id of the affinity group."
|
|
143
|
+
end
|
|
144
|
+
optparse.parse!(args)
|
|
145
|
+
verify_args!(args:args, optparse:optparse, count:1)
|
|
146
|
+
connect(options)
|
|
147
|
+
|
|
148
|
+
id = args[0]
|
|
149
|
+
params = {}
|
|
150
|
+
params.merge!(parse_query_options(options))
|
|
151
|
+
@affinity_groups_interface.setopts(options)
|
|
152
|
+
if options[:dry_run]
|
|
153
|
+
print_dry_run @affinity_groups_interface.dry.get(id, params)
|
|
154
|
+
return
|
|
155
|
+
end
|
|
156
|
+
json_response = @affinity_groups_interface.get(id, params)
|
|
157
|
+
render_response(json_response, options, 'affinityGroup') do
|
|
158
|
+
affinity_group = json_response['affinityGroup']
|
|
159
|
+
print_h1 "Affinity Group Details", [], options
|
|
160
|
+
print_description_list(affinity_group_detail_columns, affinity_group)
|
|
161
|
+
if affinity_group['servers'] && affinity_group['servers'].size > 0
|
|
162
|
+
print_h2 "Servers", options
|
|
163
|
+
print as_pretty_table(affinity_group['servers'], [:id, :name], options)
|
|
164
|
+
end
|
|
165
|
+
print reset, "\n"
|
|
166
|
+
end
|
|
167
|
+
return 0, nil
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def add(args)
|
|
171
|
+
options = {}
|
|
172
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
|
173
|
+
opts.banner = subcommand_usage("[options]")
|
|
174
|
+
opts.on('--cloud CLOUD', String, "Cloud name or id (required for cloud-scoped groups)") do |val|
|
|
175
|
+
options[:options] ||= {}
|
|
176
|
+
options[:options]['cloudId'] = val
|
|
177
|
+
end
|
|
178
|
+
opts.on('--cluster CLUSTER', String, "Cluster name or id (required for cluster-scoped groups)") do |val|
|
|
179
|
+
options[:options] ||= {}
|
|
180
|
+
options[:options]['clusterId'] = val
|
|
181
|
+
end
|
|
182
|
+
opts.on('--vm-group ID', String, "VM Group id (for VM-to-Host rules)") do |val|
|
|
183
|
+
options[:options] ||= {}
|
|
184
|
+
options[:options]['vmGroup'] = {'id' => val.to_i}
|
|
185
|
+
end
|
|
186
|
+
opts.on('--host-group ID', String, "Host Group id (for VM-to-Host rules)") do |val|
|
|
187
|
+
options[:options] ||= {}
|
|
188
|
+
options[:options]['hostGroup'] = {'id' => val.to_i}
|
|
189
|
+
end
|
|
190
|
+
opts.on('--servers ID1,ID2', Array, "Server ids (for VM-to-VM rules)") do |val|
|
|
191
|
+
options[:options] ||= {}
|
|
192
|
+
options[:options]['servers'] = val.map {|s| {'id' => s.to_i}}
|
|
193
|
+
end
|
|
194
|
+
add_perms_options(opts, options, ['plans'])
|
|
195
|
+
build_standard_add_options(opts, options)
|
|
196
|
+
opts.footer = "Create a new affinity group.\n" \
|
|
197
|
+
"Specify --cloud or --cluster to scope the group. " \
|
|
198
|
+
"Use --vm-group and --host-group for VM-to-Host rules, or --servers for VM-to-VM rules."
|
|
199
|
+
end
|
|
200
|
+
optparse.parse!(args)
|
|
201
|
+
verify_args!(args:args, optparse:optparse, count:0)
|
|
202
|
+
connect(options)
|
|
203
|
+
|
|
204
|
+
begin
|
|
205
|
+
if options[:payload]
|
|
206
|
+
payload = options[:payload]
|
|
207
|
+
payload.deep_merge!(options[:options].reject {|k,v| k.is_a?(Symbol) }) if options[:options]
|
|
208
|
+
else
|
|
209
|
+
options[:options] ||= {}
|
|
210
|
+
option_types = add_affinity_group_option_types
|
|
211
|
+
affinity_group = Morpheus::Cli::OptionTypes.prompt(option_types, options[:options], @api_client, {})
|
|
212
|
+
|
|
213
|
+
preset_servers = options[:options].key?('servers')
|
|
214
|
+
preset_vmhost = options[:options].key?('vmGroup') && options[:options].key?('hostGroup')
|
|
215
|
+
unless preset_servers || preset_vmhost
|
|
216
|
+
shape_options = [{'name' => 'VM-to-VM (pick servers)', 'value' => 'vmToVm'},
|
|
217
|
+
{'name' => 'VM-to-Host (pick groups)', 'value' => 'vmToHost'}]
|
|
218
|
+
shape = Morpheus::Cli::OptionTypes.prompt([{
|
|
219
|
+
'fieldName' => 'shape', 'fieldLabel' => 'Rule Shape', 'type' => 'select',
|
|
220
|
+
'required' => true, 'defaultValue' => 'vmToVm', 'selectOptions' => shape_options
|
|
221
|
+
}], options[:options], @api_client, {})['shape']
|
|
222
|
+
|
|
223
|
+
if shape == 'vmToVm'
|
|
224
|
+
server_prompt = Morpheus::Cli::OptionTypes.prompt([{
|
|
225
|
+
'fieldName' => 'servers', 'fieldLabel' => 'Server', 'type' => 'multiTypeahead',
|
|
226
|
+
'optionSource' => 'searchServers', 'searchParameter' => 'phrase',
|
|
227
|
+
'description' => 'Select servers to be in the affinity group.'
|
|
228
|
+
}], options[:options], @api_client, {})
|
|
229
|
+
affinity_group['servers'] = server_prompt['servers'] if server_prompt['servers']
|
|
230
|
+
else
|
|
231
|
+
vm_group_id = Morpheus::Cli::OptionTypes.prompt([{
|
|
232
|
+
'fieldName' => 'vmGroupId', 'fieldLabel' => 'VM Group ID', 'type' => 'text', 'required' => true
|
|
233
|
+
}], options[:options], @api_client, {})['vmGroupId']
|
|
234
|
+
host_group_id = Morpheus::Cli::OptionTypes.prompt([{
|
|
235
|
+
'fieldName' => 'hostGroupId', 'fieldLabel' => 'Host Group ID', 'type' => 'text', 'required' => true
|
|
236
|
+
}], options[:options], @api_client, {})['hostGroupId']
|
|
237
|
+
affinity_group['vmGroup'] = {'id' => vm_group_id.to_i}
|
|
238
|
+
affinity_group['hostGroup'] = {'id' => host_group_id.to_i}
|
|
239
|
+
end
|
|
240
|
+
else
|
|
241
|
+
affinity_group['servers'] = options[:options]['servers'] if preset_servers
|
|
242
|
+
affinity_group['vmGroup'] = options[:options]['vmGroup'] if preset_vmhost
|
|
243
|
+
affinity_group['hostGroup'] = options[:options]['hostGroup'] if preset_vmhost
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
perms = prompt_permissions(options, is_master_account ? ['plans'] : ['plans', 'visibility', 'tenants'])
|
|
247
|
+
affinity_group['resourcePermissions'] = perms['resourcePermissions'] unless perms['resourcePermissions'].nil?
|
|
248
|
+
affinity_group['tenants'] = perms['tenantPermissions'] unless perms['tenantPermissions'].nil? || perms['tenantPermissions']['accounts'].empty?
|
|
249
|
+
affinity_group['visibility'] = perms['resourcePool']['visibility'] if !perms['resourcePool'].nil? && !perms['resourcePool']['visibility'].nil?
|
|
250
|
+
|
|
251
|
+
payload = {'affinityGroup' => affinity_group}
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Create is scoped -- POST /api/clusters/{id}/affinity-groups or
|
|
255
|
+
# /api/zones/{id}/affinity-groups. There is no top-level create route.
|
|
256
|
+
scope_cloud = options[:options] && options[:options]['cloudId']
|
|
257
|
+
scope_cluster = options[:options] && options[:options]['clusterId']
|
|
258
|
+
if scope_cloud && scope_cluster
|
|
259
|
+
cloud = find_cloud_by_name_or_id(scope_cloud)
|
|
260
|
+
return 1 if cloud.nil?
|
|
261
|
+
pool = find_resource_pool_cluster_by_name_or_id(cloud, scope_cluster)
|
|
262
|
+
return 1 if pool.nil?
|
|
263
|
+
if payload['affinityGroup'].is_a?(Hash)
|
|
264
|
+
payload['affinityGroup'].delete('cloudId')
|
|
265
|
+
payload['affinityGroup'].delete('clusterId')
|
|
266
|
+
payload['affinityGroup']['pool'] = {'id' => pool['id']}
|
|
267
|
+
end
|
|
268
|
+
@clouds_interface.setopts(options)
|
|
269
|
+
if options[:dry_run]
|
|
270
|
+
print_dry_run @clouds_interface.dry.create_affinity_group(cloud['id'], payload)
|
|
271
|
+
return
|
|
272
|
+
end
|
|
273
|
+
json_response = @clouds_interface.create_affinity_group(cloud['id'], payload)
|
|
274
|
+
elsif scope_cloud
|
|
275
|
+
cloud = find_cloud_by_name_or_id(scope_cloud)
|
|
276
|
+
return 1 if cloud.nil?
|
|
277
|
+
payload['affinityGroup'].delete('cloudId') if payload['affinityGroup'].is_a?(Hash)
|
|
278
|
+
@clouds_interface.setopts(options)
|
|
279
|
+
if options[:dry_run]
|
|
280
|
+
print_dry_run @clouds_interface.dry.create_affinity_group(cloud['id'], payload)
|
|
281
|
+
return
|
|
282
|
+
end
|
|
283
|
+
json_response = @clouds_interface.create_affinity_group(cloud['id'], payload)
|
|
284
|
+
elsif scope_cluster
|
|
285
|
+
cluster_scope = find_cluster_list_scope_by_name_or_id(scope_cluster)
|
|
286
|
+
return 1 if cluster_scope.nil?
|
|
287
|
+
if cluster_scope[:type] == :managed_cluster
|
|
288
|
+
cluster = cluster_scope[:cluster]
|
|
289
|
+
payload['affinityGroup'].delete('clusterId') if payload['affinityGroup'].is_a?(Hash)
|
|
290
|
+
@clusters_interface.setopts(options)
|
|
291
|
+
if options[:dry_run]
|
|
292
|
+
print_dry_run @clusters_interface.dry.create_affinity_group(cluster['id'], payload)
|
|
293
|
+
return
|
|
294
|
+
end
|
|
295
|
+
json_response = @clusters_interface.create_affinity_group(cluster['id'], payload)
|
|
296
|
+
else
|
|
297
|
+
cloud = cluster_scope[:cloud]
|
|
298
|
+
pool = cluster_scope[:pool]
|
|
299
|
+
if payload['affinityGroup'].is_a?(Hash)
|
|
300
|
+
payload['affinityGroup'].delete('clusterId')
|
|
301
|
+
payload['affinityGroup']['pool'] = {'id' => pool['id']}
|
|
302
|
+
end
|
|
303
|
+
@clouds_interface.setopts(options)
|
|
304
|
+
if options[:dry_run]
|
|
305
|
+
print_dry_run @clouds_interface.dry.create_affinity_group(cloud['id'], payload)
|
|
306
|
+
return
|
|
307
|
+
end
|
|
308
|
+
json_response = @clouds_interface.create_affinity_group(cloud['id'], payload)
|
|
309
|
+
end
|
|
310
|
+
else
|
|
311
|
+
print_error Morpheus::Terminal.angry_prompt
|
|
312
|
+
puts_error "#{command_name} add requires a scope: pass --cloud CLOUD or --cluster CLUSTER\n#{optparse}"
|
|
313
|
+
return 1, "scope required"
|
|
314
|
+
end
|
|
315
|
+
render_response(json_response, options, 'affinityGroup') do
|
|
316
|
+
affinity_group = json_response['affinityGroup']
|
|
317
|
+
print_green_success "Created affinity group #{affinity_group ? affinity_group['name'] : ''}"
|
|
318
|
+
end
|
|
319
|
+
return 0, nil
|
|
320
|
+
rescue RestClient::Exception => e
|
|
321
|
+
handle_affinity_rest_exception(e, options)
|
|
322
|
+
exit 1
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def update(args)
|
|
327
|
+
options = {}
|
|
328
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
|
329
|
+
opts.banner = subcommand_usage("[id] [options]")
|
|
330
|
+
build_option_type_options(opts, options, update_affinity_group_option_types)
|
|
331
|
+
opts.on('--vm-group ID', String, "VM Group id (for VM-to-Host rules)") do |val|
|
|
332
|
+
options[:options] ||= {}
|
|
333
|
+
options[:options]['vmGroup'] = {'id' => val.to_i}
|
|
334
|
+
end
|
|
335
|
+
opts.on('--host-group ID', String, "Host Group id (for VM-to-Host rules)") do |val|
|
|
336
|
+
options[:options] ||= {}
|
|
337
|
+
options[:options]['hostGroup'] = {'id' => val.to_i}
|
|
338
|
+
end
|
|
339
|
+
opts.on('--servers ID1,ID2', Array, "Server ids") do |val|
|
|
340
|
+
options[:options] ||= {}
|
|
341
|
+
options[:options]['servers'] = val.map {|s| {'id' => s.to_i}}
|
|
342
|
+
end
|
|
343
|
+
add_perms_options(opts, options, ['plans'])
|
|
344
|
+
build_standard_update_options(opts, options)
|
|
345
|
+
opts.footer = "Update an affinity group.\n" \
|
|
346
|
+
"[id] is required. This is the numeric id of the affinity group."
|
|
347
|
+
end
|
|
348
|
+
optparse.parse!(args)
|
|
349
|
+
verify_args!(args:args, optparse:optparse, count:1)
|
|
350
|
+
connect(options)
|
|
351
|
+
|
|
352
|
+
begin
|
|
353
|
+
id = args[0]
|
|
354
|
+
if options[:payload]
|
|
355
|
+
payload = options[:payload]
|
|
356
|
+
payload.deep_merge!(options[:options].reject {|k,v| k.is_a?(Symbol) }) if options[:options]
|
|
357
|
+
else
|
|
358
|
+
payload = {'affinityGroup' => {}}
|
|
359
|
+
v_prompt = Morpheus::Cli::OptionTypes.no_prompt(update_affinity_group_option_types, options[:options], @api_client, {})
|
|
360
|
+
payload.deep_merge!({'affinityGroup' => v_prompt})
|
|
361
|
+
|
|
362
|
+
perms = prompt_permissions(options, is_master_account ? ['plans'] : ['plans', 'visibility', 'tenants'])
|
|
363
|
+
payload['affinityGroup']['permissions'] = perms['resourcePermissions'] if perms['resourcePermissions']
|
|
364
|
+
payload['affinityGroup']['visibility'] = perms['resourcePool']['visibility'] if !perms['resourcePool'].nil? && !perms['resourcePool']['visibility'].nil?
|
|
365
|
+
|
|
366
|
+
if options[:options]
|
|
367
|
+
payload.deep_merge!({'affinityGroup' => options[:options].reject {|k,v| k.is_a?(Symbol) || payload['affinityGroup'].key?(k) }})
|
|
368
|
+
end
|
|
369
|
+
if payload['affinityGroup'].empty?
|
|
370
|
+
raise_command_error "Specify at least one option to update.\n#{optparse}"
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
@affinity_groups_interface.setopts(options)
|
|
375
|
+
if options[:dry_run]
|
|
376
|
+
print_dry_run @affinity_groups_interface.dry.update(id, payload)
|
|
377
|
+
return
|
|
378
|
+
end
|
|
379
|
+
json_response = @affinity_groups_interface.update(id, payload)
|
|
380
|
+
render_response(json_response, options, 'affinityGroup') do
|
|
381
|
+
updated = json_response['affinityGroup']
|
|
382
|
+
print_green_success "Updated affinity group #{updated ? updated['name'] : id}"
|
|
383
|
+
end
|
|
384
|
+
return 0, nil
|
|
385
|
+
rescue RestClient::Exception => e
|
|
386
|
+
handle_affinity_rest_exception(e, options)
|
|
387
|
+
exit 1
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def remove(args)
|
|
392
|
+
options = {}
|
|
393
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
|
394
|
+
opts.banner = subcommand_usage("[id]")
|
|
395
|
+
build_standard_remove_options(opts, options)
|
|
396
|
+
opts.footer = "Delete an affinity group.\n" \
|
|
397
|
+
"[id] is required. This is the numeric id of the affinity group."
|
|
398
|
+
end
|
|
399
|
+
optparse.parse!(args)
|
|
400
|
+
verify_args!(args:args, optparse:optparse, count:1)
|
|
401
|
+
connect(options)
|
|
402
|
+
|
|
403
|
+
id = args[0]
|
|
404
|
+
unless options[:yes] || ::Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete affinity group '#{id}'?", options)
|
|
405
|
+
return 9, "aborted command"
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
@affinity_groups_interface.setopts(options)
|
|
409
|
+
if options[:dry_run]
|
|
410
|
+
print_dry_run @affinity_groups_interface.dry.destroy(id)
|
|
411
|
+
return
|
|
412
|
+
end
|
|
413
|
+
begin
|
|
414
|
+
json_response = @affinity_groups_interface.destroy(id)
|
|
415
|
+
render_response(json_response, options) do
|
|
416
|
+
print_green_success "Removed affinity group #{id}"
|
|
417
|
+
end
|
|
418
|
+
return 0, nil
|
|
419
|
+
rescue RestClient::Exception => e
|
|
420
|
+
print_rest_exception(e, options)
|
|
421
|
+
exit 1
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def violations(args)
|
|
426
|
+
options = {}
|
|
427
|
+
params = {}
|
|
428
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
|
429
|
+
opts.banner = subcommand_usage()
|
|
430
|
+
opts.on('--cloud CLOUD', String, "Cloud name or id") do |val|
|
|
431
|
+
options[:cloud] = val
|
|
432
|
+
end
|
|
433
|
+
opts.on('--cluster CLUSTER', String, "Cluster name or id") do |val|
|
|
434
|
+
options[:cluster] = val
|
|
435
|
+
end
|
|
436
|
+
build_standard_list_options(opts, options)
|
|
437
|
+
opts.footer = "List active MUST-rule affinity violations.
|
|
438
|
+
" \
|
|
439
|
+
"Requires a scope: use --cloud or --cluster."
|
|
440
|
+
end
|
|
441
|
+
optparse.parse!(args)
|
|
442
|
+
verify_args!(args:args, optparse:optparse, count:0)
|
|
443
|
+
connect(options)
|
|
444
|
+
|
|
445
|
+
# AffinityGroupsController#violations returns 400 unless clusterId or
|
|
446
|
+
# cloudId is supplied; scope travels in the query string, not the path.
|
|
447
|
+
scope_name = nil
|
|
448
|
+
if options[:cloud]
|
|
449
|
+
cloud = find_cloud_by_name_or_id(options[:cloud])
|
|
450
|
+
return 1 if cloud.nil?
|
|
451
|
+
params['cloudId'] = cloud['id']
|
|
452
|
+
scope_name = cloud['name']
|
|
453
|
+
elsif options[:cluster]
|
|
454
|
+
cluster_scope = find_cluster_list_scope_by_name_or_id(options[:cluster])
|
|
455
|
+
return 1 if cluster_scope.nil?
|
|
456
|
+
if cluster_scope[:type] != :managed_cluster
|
|
457
|
+
cloud = cluster_scope[:cloud]
|
|
458
|
+
print_red_alert "Affinity violations are only available for Morpheus managed clusters.\n" \
|
|
459
|
+
"'#{options[:cluster]}' is a vSphere cluster owned by cloud '#{cloud['name']}'; " \
|
|
460
|
+
"affinity violation data is not tracked for cloud-owned vSphere clusters."
|
|
461
|
+
return 1
|
|
462
|
+
end
|
|
463
|
+
cluster = cluster_scope[:cluster]
|
|
464
|
+
params['clusterId'] = cluster['id']
|
|
465
|
+
scope_name = cluster['name']
|
|
466
|
+
else
|
|
467
|
+
print_error Morpheus::Terminal.angry_prompt
|
|
468
|
+
puts_error "#{command_name} violations requires a scope: pass --cloud CLOUD or --cluster CLUSTER
|
|
469
|
+
#{optparse}"
|
|
470
|
+
return 1, "scope required"
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
params.merge!(parse_list_options(options))
|
|
474
|
+
@affinity_groups_interface.setopts(options)
|
|
475
|
+
if options[:dry_run]
|
|
476
|
+
print_dry_run @affinity_groups_interface.dry.violations(params)
|
|
477
|
+
return
|
|
478
|
+
end
|
|
479
|
+
json_response = @affinity_groups_interface.violations(params)
|
|
480
|
+
render_response(json_response, options, 'violations') do
|
|
481
|
+
rows = json_response['violations'] || []
|
|
482
|
+
print_h1 "Affinity Rule Violations: #{scope_name}", parse_list_subtitles(options), options
|
|
483
|
+
if rows.empty?
|
|
484
|
+
print cyan, "No violations.", reset, "
|
|
485
|
+
"
|
|
486
|
+
else
|
|
487
|
+
columns = {
|
|
488
|
+
"Rule" => 'ruleName',
|
|
489
|
+
"Type" => lambda {|r| r['keepTogether'] ? 'Keep Together' : 'Keep Separate' },
|
|
490
|
+
"VMs" => lambda {|r| (r['affectedVms'] || []).collect {|v| v['name']}.join(', ') },
|
|
491
|
+
"Empty Host Group" => lambda {|r| format_boolean(r['emptyHostGroup']) },
|
|
492
|
+
"Occurred" => 'occurredAt'
|
|
493
|
+
}.upcase_keys!
|
|
494
|
+
print as_pretty_table(rows, columns, options)
|
|
495
|
+
print_results_pagination(json_response)
|
|
496
|
+
end
|
|
497
|
+
print reset, "
|
|
498
|
+
"
|
|
499
|
+
end
|
|
500
|
+
return 0, nil
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
private
|
|
504
|
+
|
|
505
|
+
def affinity_group_list_columns
|
|
506
|
+
{
|
|
507
|
+
"ID" => 'id',
|
|
508
|
+
"Name" => 'name',
|
|
509
|
+
"Type" => lambda {|it| format_affinity_type(it['affinityType']) },
|
|
510
|
+
"VM Group" => lambda {|it| it['vmGroup'] ? (it['vmGroup']['name'] || it['vmGroup']['id']) : '' },
|
|
511
|
+
"Host Group" => lambda {|it| it['hostGroup'] ? (it['hostGroup']['name'] || it['hostGroup']['id']) : '' },
|
|
512
|
+
"Resource Pool" => lambda {|it| it['pool'] ? (it['pool']['name'] || it['pool']['id']) : '' },
|
|
513
|
+
"Visibility" => lambda {|it| it['visibility'].to_s.capitalize },
|
|
514
|
+
"Servers" => lambda {|it| (it['servers'] || []).size },
|
|
515
|
+
}.upcase_keys!
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def affinity_group_detail_columns
|
|
519
|
+
{
|
|
520
|
+
"ID" => 'id',
|
|
521
|
+
"Name" => 'name',
|
|
522
|
+
"Type" => lambda {|it| format_affinity_type(it['affinityType']) },
|
|
523
|
+
"Ref Type" => 'refType',
|
|
524
|
+
"Ref ID" => 'refId',
|
|
525
|
+
"VM Group" => lambda {|it| it['vmGroup'] ? (it['vmGroup']['name'] || it['vmGroup']['id']) : '' },
|
|
526
|
+
"Host Group" => lambda {|it| it['hostGroup'] ? (it['hostGroup']['name'] || it['hostGroup']['id']) : '' },
|
|
527
|
+
"Resource Pool" => lambda {|it| it['pool'] ? (it['pool']['name'] || it['pool']['id']) : '' },
|
|
528
|
+
"Visibility" => lambda {|it| it['visibility'].to_s.capitalize },
|
|
529
|
+
"Servers" => lambda {|it| (it['servers'] || []).size },
|
|
530
|
+
"Source" => 'source',
|
|
531
|
+
"Active" => lambda {|it| format_boolean(it['active']) },
|
|
532
|
+
}
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def add_affinity_group_option_types
|
|
536
|
+
[
|
|
537
|
+
{'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true},
|
|
538
|
+
affinity_type_option_type(required: true, default: 'KEEP_TOGETHER'),
|
|
539
|
+
{'fieldName' => 'active', 'fieldLabel' => 'Active', 'type' => 'checkbox', 'defaultValue' => true},
|
|
540
|
+
]
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def update_affinity_group_option_types
|
|
544
|
+
[
|
|
545
|
+
{'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text'},
|
|
546
|
+
affinity_type_option_type,
|
|
547
|
+
{'fieldName' => 'active', 'fieldLabel' => 'Active', 'type' => 'checkbox'},
|
|
548
|
+
{'fieldName' => 'servers', 'fieldLabel' => 'Server', 'type' => 'multiTypeahead',
|
|
549
|
+
'optionSource' => 'searchServers', 'searchParameter' => 'phrase',
|
|
550
|
+
'description' => 'Select servers to be in the affinity group.'},
|
|
551
|
+
]
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
end
|
|
@@ -196,7 +196,7 @@ EOT
|
|
|
196
196
|
plugin_opt_parser.parse!(args)
|
|
197
197
|
v_prompt = Morpheus::Cli::OptionTypes.prompt(plugin_option_type_inputs, options[:options].deep_merge({:context_map => {'domain' => 'backup'}}), @api_client)
|
|
198
198
|
v_prompt.deep_compact!.booleanize! # remove empty values and convert checkbox "on" and "off" to true and false
|
|
199
|
-
params.merge!(v_prompt.delete('backup'))
|
|
199
|
+
params.merge!(v_prompt.delete('backup') || {})
|
|
200
200
|
|
|
201
201
|
# Job / Schedule
|
|
202
202
|
params['jobAction'] = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'jobAction', 'fieldLabel' => 'Backup Job Type', 'type' => 'select', 'optionSource' => 'backupJobActions', 'required' => true, 'defaultValue' => 'new'}], options[:options], @api_client)['jobAction']
|