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,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']