morpheus-cli 9.0.1 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +1 -1
  3. data/lib/morpheus/api/affinity_groups_interface.rb +46 -0
  4. data/lib/morpheus/api/api_client.rb +17 -0
  5. data/lib/morpheus/api/clouds_interface.rb +42 -0
  6. data/lib/morpheus/api/clusters_interface.rb +55 -1
  7. data/lib/morpheus/api/distributed_workers_interface.rb +41 -0
  8. data/lib/morpheus/api/host_vm_groups_interface.rb +37 -0
  9. data/lib/morpheus/api/servers_interface.rb +8 -0
  10. data/lib/morpheus/api/virtual_switches_interface.rb +9 -0
  11. data/lib/morpheus/cli/commands/affinity_groups_command.rb +554 -0
  12. data/lib/morpheus/cli/commands/backups_command.rb +11 -0
  13. data/lib/morpheus/cli/commands/clouds.rb +421 -29
  14. data/lib/morpheus/cli/commands/clusters.rb +507 -20
  15. data/lib/morpheus/cli/commands/distributed_workers_command.rb +282 -0
  16. data/lib/morpheus/cli/commands/host_vm_groups_command.rb +423 -0
  17. data/lib/morpheus/cli/commands/hosts.rb +72 -1
  18. data/lib/morpheus/cli/commands/instances.rb +12 -1
  19. data/lib/morpheus/cli/commands/networks_command.rb +35 -3
  20. data/lib/morpheus/cli/commands/virtual_switches_command.rb +336 -0
  21. data/lib/morpheus/cli/mixins/affinity_helper.rb +118 -0
  22. data/lib/morpheus/cli/mixins/infrastructure_helper.rb +143 -4
  23. data/lib/morpheus/cli/option_types.rb +36 -1
  24. data/lib/morpheus/cli/version.rb +1 -1
  25. data/test/api/affinity_groups_interface_test.rb +43 -0
  26. data/test/api/clouds_interface_test.rb +67 -0
  27. data/test/api/host_vm_groups_interface_test.rb +35 -0
  28. data/test/cli/affinity_groups_cloud_cluster_scope_test.rb +270 -0
  29. data/test/cli/affinity_groups_cluster_scope_test.rb +171 -0
  30. data/test/cli/affinity_groups_test.rb +57 -0
  31. data/test/cli/affinity_groups_violations_scope_test.rb +141 -0
  32. data/test/cli/affinity_helper_test.rb +367 -0
  33. data/test/cli/backups_test.rb +212 -0
  34. data/test/cli/clusters_uuid_test.rb +131 -0
  35. data/test/cli/distributed_workers_test.rb +68 -0
  36. data/test/cli/host_vm_groups_cluster_scope_test.rb +187 -0
  37. data/test/cli/host_vm_groups_test.rb +63 -0
  38. data/test/cli/infrastructure_helper_cluster_resolver_test.rb +250 -0
  39. metadata +39 -2
@@ -7,6 +7,7 @@ class Morpheus::Cli::Clusters
7
7
  include Morpheus::Cli::WhoamiHelper
8
8
  include Morpheus::Cli::AccountsHelper
9
9
  include Morpheus::Cli::ExecutionRequestHelper
10
+ include Morpheus::Cli::AffinityHelper
10
11
 
11
12
  register_subcommands :list, :count, :get, :view, :add, :update, :remove, :logs, :history, {:'history-details' => :history_details}, {:'history-event' => :history_event_details}
12
13
  register_subcommands :list_types, :get_type
@@ -31,6 +32,8 @@ class Morpheus::Cli::Clusters
31
32
  register_subcommands :list_replicasets, :list_daemonsets, :list_endpoints, :list_ingresses, :list_policies, :list_volumes, :list_volume_claims, :list_config_maps, :list_secrets
32
33
  register_subcommands :get_pod, :get_deployment, :get_replicaset, :get_daemonset, :get_endpoint, :get_ingress, :get_policy, :get_volume_claim, :get_volume, :get_config_map, :get_secret, :get_stateful_set, :get_job, :get_service
33
34
  register_subcommands :list_affinity_groups, :get_affinity_group, :update_affinity_group, :add_affinity_group, :remove_affinity_group
35
+ register_subcommands :'list-affinity-group-violations' => :list_affinity_group_violations
36
+ register_subcommands :list_host_vm_groups, :get_host_vm_group, :add_host_vm_group, :update_host_vm_group, :remove_host_vm_group
34
37
 
35
38
  def connect(opts)
36
39
  @api_client = establish_remote_appliance_connection(opts)
@@ -71,8 +74,11 @@ class Morpheus::Cli::Clusters
71
74
  opts.on('--all-labels LABEL', String, "Filter by labels, must match all of the values") do |val|
72
75
  add_query_parameter(params, 'allLabels', parse_labels(val))
73
76
  end
77
+ opts.on('--uuid UUID', String, "Filter by uuid, can be passed multiple times to match any of the values") do |val|
78
+ add_query_parameter(params, 'uuid', val)
79
+ end
74
80
  build_common_options(opts, options, [:list, :query, :json, :yaml, :csv, :fields, :dry_run, :remote])
75
- opts.footer = "List clusters."
81
+ opts.footer = "List clusters.\nFilter by one or more uuids using --uuid UUID."
76
82
  end
77
83
  optparse.parse!(args)
78
84
  if args.count != 0
@@ -146,7 +152,7 @@ class Morpheus::Cli::Clusters
146
152
  def get(args)
147
153
  options = {}
148
154
  optparse = Morpheus::Cli::OptionParser.new do |opts|
149
- opts.banner = subcommand_usage("[id]")
155
+ opts.banner = subcommand_usage("[cluster]")
150
156
  opts.on( nil, '--hosts', "Display masters and workers" ) do
151
157
  options[:show_masters] = true
152
158
  options[:show_workers] = true
@@ -170,7 +176,7 @@ class Morpheus::Cli::Clusters
170
176
  options[:refresh_until_status] = val.to_s.downcase
171
177
  end
172
178
  build_common_options(opts, options, [:json, :dry_run, :remote])
173
- opts.footer = "Get details about a cluster."
179
+ opts.footer = "Get details about a cluster.\n[cluster] is required. This is the id, name, or uuid of a cluster."
174
180
  end
175
181
  optparse.parse!(args)
176
182
  if args.count < 1
@@ -191,6 +197,8 @@ class Morpheus::Cli::Clusters
191
197
  if options[:dry_run]
192
198
  if arg.to_s =~ /\A\d{1,}\Z/
193
199
  print_dry_run @clusters_interface.dry.get(arg.to_i)
200
+ elsif cluster_uuid?(arg)
201
+ print_dry_run @clusters_interface.dry.get(arg.to_s)
194
202
  else
195
203
  print_dry_run @clusters_interface.dry.list({name:arg})
196
204
  end
@@ -3363,10 +3371,11 @@ class Morpheus::Cli::Clusters
3363
3371
  "ID" => 'id',
3364
3372
  "Name" => 'name',
3365
3373
  "Type" => lambda {|it| format_affinity_type(it['affinityType']) },
3374
+ "VM Group" => lambda {|it| it['vmGroup'] ? (it['vmGroup']['name'] || it['vmGroup']['id']) : '' },
3375
+ "Host Group" => lambda {|it| it['hostGroup'] ? (it['hostGroup']['name'] || it['hostGroup']['id']) : '' },
3366
3376
  "Resource Pool" => lambda {|it| it['pool'] ? (it['pool']['name'] || it['pool']['id']) : '' },
3367
3377
  "Visibility" => lambda {|it| it['visibility'].to_s.capitalize },
3368
- "Servers" => lambda {|it| it['servers'].size() },
3369
- # "Source" => lambda {|it| it['source'] },
3378
+ "Servers" => lambda {|it| (it['servers'] || []).size },
3370
3379
  }.upcase_keys!
3371
3380
  print as_pretty_table(affinity_groups, columns, options)
3372
3381
  print_results_pagination(json_response)
@@ -3414,14 +3423,18 @@ class Morpheus::Cli::Clusters
3414
3423
  "ID" => 'id',
3415
3424
  "Name" => 'name',
3416
3425
  "Type" => lambda {|it| format_affinity_type(it['affinityType']) },
3426
+ "Ref Type" => 'refType',
3427
+ "Ref ID" => 'refId',
3428
+ "VM Group" => lambda {|it| it['vmGroup'] ? (it['vmGroup']['name'] || it['vmGroup']['id']) : '' },
3429
+ "Host Group" => lambda {|it| it['hostGroup'] ? (it['hostGroup']['name'] || it['hostGroup']['id']) : '' },
3417
3430
  "Resource Pool" => lambda {|it| it['pool'] ? (it['pool']['name'] || it['pool']['id']) : '' },
3418
3431
  "Visibility" => lambda {|it| it['visibility'].to_s.capitalize },
3419
- "Servers" => lambda {|it| it['servers'].size() },
3432
+ "Servers" => lambda {|it| (it['servers'] || []).size },
3420
3433
  "Source" => lambda {|it| it['source'] },
3421
3434
  "Active" => lambda {|it| format_boolean(it['active']) },
3422
3435
  }
3423
3436
  print_description_list(columns, affinity_group)
3424
- if affinity_group['servers'].size > 0
3437
+ if affinity_group['servers'] && affinity_group['servers'].size > 0
3425
3438
  print_h2 "Servers", options
3426
3439
  print as_pretty_table(affinity_group['servers'], [:id, :name], options)
3427
3440
  end
@@ -3435,6 +3448,18 @@ class Morpheus::Cli::Clusters
3435
3448
  optparse = Morpheus::Cli::OptionParser.new do |opts|
3436
3449
  opts.banner = subcommand_usage( "[cluster] [name] [options]")
3437
3450
  build_option_type_options(opts, options, add_affinity_group_option_types)
3451
+ opts.on('--vm-group ID', String, "VM Group ID (for VM-to-Host rules)") do |val|
3452
+ options[:options] ||= {}
3453
+ options[:options]['vmGroup'] = {'id' => val.to_i}
3454
+ end
3455
+ opts.on('--host-group ID', String, "Host Group ID (for VM-to-Host rules)") do |val|
3456
+ options[:options] ||= {}
3457
+ options[:options]['hostGroup'] = {'id' => val.to_i}
3458
+ end
3459
+ opts.on('--servers ID1,ID2', Array, "Server IDs (for VM-to-VM rules)") do |val|
3460
+ options[:options] ||= {}
3461
+ options[:options]['servers'] = val.map {|s| {'id' => s.to_i}}
3462
+ end
3438
3463
  add_perms_options(opts, options, ['plans'])
3439
3464
  build_standard_add_options(opts, options)
3440
3465
  opts.footer = "Add affinity group to a cluster.\n" +
@@ -3450,6 +3475,7 @@ class Morpheus::Cli::Clusters
3450
3475
  cluster = find_cluster_by_name_or_id(args[0])
3451
3476
  return 1 if cluster.nil?
3452
3477
  if args[1]
3478
+ options[:options] ||= {}
3453
3479
  options[:options]['name'] = args[1]
3454
3480
  end
3455
3481
  if options[:payload]
@@ -3465,13 +3491,72 @@ class Morpheus::Cli::Clusters
3465
3491
  option_types = add_affinity_group_option_types
3466
3492
  affinity_group = Morpheus::Cli::OptionTypes.prompt(option_types, options[:options], @api_client, options[:params])
3467
3493
 
3468
- # affinity_group_type = find_affinity_group_type_by_code(affinity_group['affinityGroupType'])
3469
- # affinity_group['affinityGroupType'] = {id:affinity_group_type['id']}
3494
+ # Rule shape — servers OR vmGroup+hostGroup. Skip if already provided via CLI flags/-O or --payload merge.
3495
+ preset_servers = options[:options] && options[:options].key?('servers')
3496
+ preset_vmhost = options[:options] && options[:options].key?('vmGroup') && options[:options].key?('hostGroup')
3497
+ unless preset_servers || preset_vmhost
3498
+ form_opts = @clusters_interface.affinity_group_form_options(cluster['id']) rescue {}
3499
+ available_servers = form_opts['availableServers'] || []
3500
+ available_vm_groups = form_opts['availableVmGroups'] || []
3501
+ available_host_groups = form_opts['availableHostGroups'] || []
3502
+
3503
+ shape_options = [{'name' => 'VM-to-VM (pick servers)', 'value' => 'vmToVm'}]
3504
+ if !available_vm_groups.empty? && !available_host_groups.empty?
3505
+ shape_options << {'name' => 'VM-to-Host (pick groups)', 'value' => 'vmToHost'}
3506
+ end
3507
+ shape = if shape_options.size == 1
3508
+ 'vmToVm'
3509
+ else
3510
+ Morpheus::Cli::OptionTypes.prompt([{
3511
+ 'fieldName' => 'shape', 'fieldLabel' => 'Rule Shape', 'type' => 'select', 'required' => true,
3512
+ 'defaultValue' => 'vmToVm', 'selectOptions' => shape_options
3513
+ }], options[:options], @api_client, options[:params])['shape']
3514
+ end
3470
3515
 
3471
- # # affinity_group type options
3472
- # unless affinity_group_type['optionTypes'].empty?
3473
- # affinity_group.merge!(Morpheus::Cli::OptionTypes.prompt(affinity_group_type['optionTypes'], options[:options].deep_merge({:context_map => {'domain' => ''}, :checkbox_as_boolean => true}), @api_client, options[:params]))
3474
- # end
3516
+ if shape == 'vmToVm'
3517
+ if available_servers.empty?
3518
+ # Fall back to the classic multiTypeahead prompt when the form-options endpoint returns nothing
3519
+ server_prompt = Morpheus::Cli::OptionTypes.prompt([
3520
+ {'fieldName' => 'servers', 'fieldLabel' => 'Server', 'type' => 'multiTypeahead', 'optionSource' => 'searchServers', 'searchParameter' => 'phrase', 'description' => 'Select servers to be in the affinity group.'}
3521
+ ], options[:options], @api_client, options[:params])
3522
+ affinity_group['servers'] = server_prompt['servers'] if server_prompt['servers']
3523
+ else
3524
+ print cyan, "Available Servers:", reset, "\n"
3525
+ available_servers.each {|s| print " #{s['id']}\t#{s['name']}\n" }
3526
+ raw = Morpheus::Cli::OptionTypes.prompt([{
3527
+ 'fieldName' => 'servers', 'fieldLabel' => 'Servers (comma-separated IDs)', 'type' => 'text', 'required' => true,
3528
+ 'description' => 'Server IDs to include, comma-separated'
3529
+ }], options[:options], @api_client, options[:params])['servers']
3530
+ selected_ids = raw.to_s.split(',').map(&:strip).reject(&:empty?).map(&:to_i)
3531
+ valid_ids = available_servers.map {|s| s['id'].to_i}
3532
+ invalid = selected_ids - valid_ids
3533
+ if !invalid.empty?
3534
+ print_red_alert "Invalid server IDs for this cluster: #{invalid.join(', ')}"
3535
+ return 1
3536
+ end
3537
+ if selected_ids.empty?
3538
+ print_red_alert "At least one server ID is required"
3539
+ return 1
3540
+ end
3541
+ affinity_group['servers'] = selected_ids.map {|id| {'id' => id}}
3542
+ end
3543
+ else
3544
+ vm_group_id = Morpheus::Cli::OptionTypes.prompt([{
3545
+ 'fieldName' => 'vmGroup', 'fieldLabel' => 'VM Group', 'type' => 'select', 'required' => true,
3546
+ 'selectOptions' => available_vm_groups.map {|g| {'name' => g['name'], 'value' => g['id']} }
3547
+ }], options[:options], @api_client, options[:params])['vmGroup']
3548
+ host_group_id = Morpheus::Cli::OptionTypes.prompt([{
3549
+ 'fieldName' => 'hostGroup', 'fieldLabel' => 'Host Group', 'type' => 'select', 'required' => true,
3550
+ 'selectOptions' => available_host_groups.map {|g| {'name' => g['name'], 'value' => g['id']} }
3551
+ }], options[:options], @api_client, options[:params])['hostGroup']
3552
+ affinity_group['vmGroup'] = {'id' => vm_group_id.to_i}
3553
+ affinity_group['hostGroup'] = {'id' => host_group_id.to_i}
3554
+ end
3555
+ else
3556
+ affinity_group['servers'] = options[:options]['servers'] if preset_servers
3557
+ affinity_group['vmGroup'] = options[:options]['vmGroup'] if preset_vmhost
3558
+ affinity_group['hostGroup'] = options[:options]['hostGroup'] if preset_vmhost
3559
+ end
3475
3560
 
3476
3561
  # perms
3477
3562
  perms = prompt_permissions(options, is_master_account ? ['plans'] : ['plans', 'visibility', 'tenants'])
@@ -3659,6 +3744,367 @@ class Morpheus::Cli::Clusters
3659
3744
  return 0, nil
3660
3745
  end
3661
3746
 
3747
+ def list_affinity_group_violations(args)
3748
+ options = {}
3749
+ params = {}
3750
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
3751
+ opts.banner = subcommand_usage("[cluster]")
3752
+ build_standard_list_options(opts, options)
3753
+ opts.footer = "List active MUST-rule affinity violations.\n" +
3754
+ "[cluster] is required. This is the name or id of an existing cluster."
3755
+ end
3756
+ optparse.parse!(args)
3757
+ verify_args!(args:args, optparse:optparse, count:1)
3758
+ connect(options)
3759
+ cluster = find_cluster_by_name_or_id(args[0])
3760
+ return 1 if cluster.nil?
3761
+ params['clusterId'] = cluster['id']
3762
+ params.merge!(parse_list_options(options))
3763
+ @clusters_interface.setopts(options)
3764
+ if options[:dry_run]
3765
+ print_dry_run @clusters_interface.dry.affinity_group_violations(params)
3766
+ return
3767
+ end
3768
+ json_response = @clusters_interface.affinity_group_violations(params)
3769
+ render_response(json_response, options, 'violations') do
3770
+ rows = json_response['violations'] || []
3771
+ print_h1 "Affinity Rule Violations", parse_list_subtitles(options), options
3772
+ if rows.empty?
3773
+ print cyan, "No violations.", reset, "\n"
3774
+ else
3775
+ columns = {
3776
+ "Rule" => 'ruleName',
3777
+ "Type" => lambda {|r| r['keepTogether'] ? 'Keep Together' : 'Keep Separate' },
3778
+ "VMs" => lambda {|r| (r['affectedVms'] || []).map {|v| v['name']}.join(', ') },
3779
+ "Occurred" => 'occurredAt'
3780
+ }.upcase_keys!
3781
+ print as_pretty_table(rows, columns, options)
3782
+ end
3783
+ print reset, "\n"
3784
+ end
3785
+ return 0, nil
3786
+ end
3787
+
3788
+ def list_host_vm_groups(args)
3789
+ options = {}
3790
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
3791
+ opts.banner = subcommand_usage("[cluster]")
3792
+ build_standard_list_options(opts, options)
3793
+ opts.footer = "List host/VM groups on a cluster.\n" +
3794
+ "[cluster] is required. This is the name or id of an existing cluster."
3795
+ end
3796
+ optparse.parse!(args)
3797
+ verify_args!(args:args, optparse:optparse, count:1)
3798
+ connect(options)
3799
+ cluster = find_cluster_by_name_or_id(args[0])
3800
+ return 1 if cluster.nil?
3801
+ params = {}
3802
+ params.merge!(parse_list_options(options))
3803
+ @clusters_interface.setopts(options)
3804
+ if options[:dry_run]
3805
+ print_dry_run @clusters_interface.dry.list_host_vm_groups(cluster['id'], params)
3806
+ return
3807
+ end
3808
+ json_response = @clusters_interface.list_host_vm_groups(cluster['id'], params)
3809
+ render_response(json_response, options, 'hostVmGroups') do
3810
+ rows = json_response['hostVmGroups'] || []
3811
+ print_h1 "Morpheus Cluster Host/VM Groups: #{cluster['name']}", parse_list_subtitles(options), options
3812
+ if rows.empty?
3813
+ print cyan, "No host/VM groups found.", reset, "\n"
3814
+ else
3815
+ columns = {
3816
+ "ID" => 'id',
3817
+ "Name" => 'name',
3818
+ "Type" => lambda {|it| format_host_vm_group_type(it['type']) },
3819
+ "Servers" => lambda {|it| (it['servers'] || []).size },
3820
+ "Visibility" => lambda {|it| it['visibility'].to_s.capitalize },
3821
+ }.upcase_keys!
3822
+ print as_pretty_table(rows, columns, options)
3823
+ print_results_pagination(json_response)
3824
+ end
3825
+ print reset, "\n"
3826
+ end
3827
+ return 0, nil
3828
+ end
3829
+
3830
+ def get_host_vm_group(args)
3831
+ options = {}
3832
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
3833
+ opts.banner = subcommand_usage("[cluster] [host-vm-group]")
3834
+ build_standard_get_options(opts, options)
3835
+ opts.footer = "Get details about a cluster host/VM group.\n" +
3836
+ "[cluster] is required. This is the name or id of an existing cluster.\n" +
3837
+ "[host-vm-group] is required. This is the name or id of an existing host/VM group."
3838
+ end
3839
+ optparse.parse!(args)
3840
+ verify_args!(args:args, optparse:optparse, count:2)
3841
+ connect(options)
3842
+ cluster = find_cluster_by_name_or_id(args[0])
3843
+ return 1 if cluster.nil?
3844
+ host_vm_group = find_cluster_host_vm_group_by_name_or_id(cluster['id'], args[1])
3845
+ if host_vm_group.nil?
3846
+ print_red_alert "Host/VM Group not found for '#{args[1]}'"
3847
+ exit 1
3848
+ end
3849
+ params = {}
3850
+ params.merge!(parse_query_options(options))
3851
+ @clusters_interface.setopts(options)
3852
+ if options[:dry_run]
3853
+ print_dry_run @clusters_interface.dry.get_host_vm_group(cluster['id'], host_vm_group['id'], params)
3854
+ return
3855
+ end
3856
+ json_response = @clusters_interface.get_host_vm_group(cluster['id'], host_vm_group['id'], params)
3857
+ render_response(json_response, options, 'hostVmGroup') do
3858
+ row = json_response['hostVmGroup']
3859
+ print_h1 "Host/VM Group Details", [], options
3860
+ columns = {
3861
+ "ID" => 'id',
3862
+ "Name" => 'name',
3863
+ "Type" => lambda {|it| format_host_vm_group_type(it['type']) },
3864
+ "Ref Type" => 'refType',
3865
+ "Ref ID" => 'refId',
3866
+ "Servers" => lambda {|it| (it['servers'] || []).size },
3867
+ "Visibility" => lambda {|it| it['visibility'].to_s.capitalize },
3868
+ "Active" => lambda {|it| format_boolean(it['active']) },
3869
+ }
3870
+ print_description_list(columns, row)
3871
+ if row['servers'] && row['servers'].size > 0
3872
+ print_h2 "Servers", options
3873
+ print as_pretty_table(row['servers'], [:id, :name], options)
3874
+ end
3875
+ print reset, "\n"
3876
+ end
3877
+ return 0, nil
3878
+ end
3879
+
3880
+ def add_host_vm_group(args)
3881
+ options = {}
3882
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
3883
+ opts.banner = subcommand_usage("[cluster] [name] [options]")
3884
+ build_option_type_options(opts, options, add_host_vm_group_option_types)
3885
+ opts.on('--servers ID1,ID2', Array, "Server IDs to include") do |val|
3886
+ options[:options] ||= {}
3887
+ options[:options]['servers'] = val.map {|s| {'id' => s.to_i}}
3888
+ end
3889
+ add_perms_options(opts, options, ['plans'])
3890
+ build_standard_add_options(opts, options)
3891
+ opts.footer = "Add a host/VM group to a cluster.\n" +
3892
+ "[cluster] is required. This is the name or id of an existing cluster.\n" +
3893
+ "[name] is required. This is the name of the new host/VM group."
3894
+ end
3895
+ optparse.parse!(args)
3896
+ verify_args!(args:args, optparse:optparse, min:1, max:2)
3897
+ connect(options)
3898
+
3899
+ begin
3900
+ cluster = find_cluster_by_name_or_id(args[0])
3901
+ return 1 if cluster.nil?
3902
+ if args[1]
3903
+ options[:options] ||= {}
3904
+ options[:options]['name'] = args[1]
3905
+ end
3906
+ if options[:payload]
3907
+ payload = options[:payload]
3908
+ if options[:options]
3909
+ payload ||= {}
3910
+ payload.deep_merge!(options[:options].reject {|k,v| k.is_a?(Symbol) })
3911
+ end
3912
+ else
3913
+ options[:params] ||= {}
3914
+ options[:params].merge!({:serverGroupId => cluster['id']})
3915
+ option_types = add_host_vm_group_option_types
3916
+ host_vm_group = Morpheus::Cli::OptionTypes.prompt(option_types, options[:options], @api_client, options[:params])
3917
+
3918
+ # Servers — form-options driven, filtered by chosen type
3919
+ unless options[:options] && options[:options].key?('servers')
3920
+ form_opts = @clusters_interface.host_vm_group_form_options(cluster['id'], {'hostVmGroupType' => host_vm_group['type']}) rescue {}
3921
+ available_servers = form_opts['availableServers'] || []
3922
+ if available_servers.empty?
3923
+ print_red_alert "No servers available on cluster #{cluster['name']} for type #{host_vm_group['type']}"
3924
+ return 1
3925
+ end
3926
+ print cyan, "Available Servers:", reset, "\n"
3927
+ available_servers.each {|s| print " #{s['id']}\t#{s['name']}\n" }
3928
+ raw = Morpheus::Cli::OptionTypes.prompt([{
3929
+ 'fieldName' => 'servers', 'fieldLabel' => 'Servers (comma-separated IDs)', 'type' => 'text', 'required' => true,
3930
+ 'description' => 'Server IDs to include, comma-separated'
3931
+ }], options[:options], @api_client, options[:params])['servers']
3932
+ selected_ids = raw.to_s.split(',').map(&:strip).reject(&:empty?).map(&:to_i)
3933
+ valid_ids = available_servers.map {|s| s['id'].to_i}
3934
+ invalid = selected_ids - valid_ids
3935
+ if !invalid.empty?
3936
+ print_red_alert "Invalid server IDs for this cluster/type: #{invalid.join(', ')}"
3937
+ return 1
3938
+ end
3939
+ if selected_ids.empty?
3940
+ print_red_alert "At least one server ID is required"
3941
+ return 1
3942
+ end
3943
+ host_vm_group['servers'] = selected_ids.map {|id| {'id' => id}}
3944
+ else
3945
+ host_vm_group['servers'] = options[:options]['servers']
3946
+ end
3947
+
3948
+ perms = prompt_permissions(options, is_master_account ? ['plans'] : ['plans', 'visibility', 'tenants'])
3949
+ host_vm_group['resourcePermissions'] = perms['resourcePermissions'] unless perms['resourcePermissions'].nil?
3950
+ host_vm_group['tenants'] = perms['tenantPermissions'] unless perms['tenantPermissions'].nil? || perms['tenantPermissions']['accounts'].empty?
3951
+ host_vm_group['visibility'] = perms['resourcePool']['visibility'] if !perms['resourcePool'].nil? && !perms['resourcePool']['visibility'].nil?
3952
+
3953
+ payload = {'hostVmGroup' => host_vm_group}
3954
+ end
3955
+
3956
+ @clusters_interface.setopts(options)
3957
+ if options[:dry_run]
3958
+ print_dry_run @clusters_interface.dry.create_host_vm_group(cluster['id'], payload)
3959
+ return
3960
+ end
3961
+ json_response = @clusters_interface.create_host_vm_group(cluster['id'], payload)
3962
+ if options[:json]
3963
+ puts as_json(json_response)
3964
+ else
3965
+ if json_response['success']
3966
+ print_green_success (json_response['msg'] || "Added host/VM group to cluster #{cluster['name']}")
3967
+ else
3968
+ print_red_alert "Failed to create host/VM group #{json_response['msg']}"
3969
+ end
3970
+ end
3971
+ return 0
3972
+ rescue RestClient::Exception => e
3973
+ print_rest_exception(e, options)
3974
+ exit 1
3975
+ end
3976
+ end
3977
+
3978
+ def update_host_vm_group(args)
3979
+ options = {}
3980
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
3981
+ opts.banner = subcommand_usage("[cluster] [host-vm-group] [options]")
3982
+ build_option_type_options(opts, options, update_host_vm_group_option_types)
3983
+ opts.on('--servers ID1,ID2', Array, "Replace server IDs") do |val|
3984
+ options[:options] ||= {}
3985
+ options[:options]['servers'] = val.map {|s| {'id' => s.to_i}}
3986
+ end
3987
+ opts.on('--visibility VISIBILITY', String, "Visibility: private | public") do |val|
3988
+ options[:options] ||= {}
3989
+ options[:options]['visibility'] = val
3990
+ end
3991
+ add_perms_options(opts, options, ['plans'])
3992
+ build_standard_update_options(opts, options)
3993
+ opts.footer = "Update a cluster host/VM group.\n" +
3994
+ "[cluster] is required. This is the name or id of an existing cluster.\n" +
3995
+ "[host-vm-group] is required. This is the name or id of an existing host/VM group."
3996
+ end
3997
+ optparse.parse!(args)
3998
+ if args.count != 2
3999
+ raise_command_error "wrong number of arguments, expected 2 and got (#{args.count}) #{args}\n#{optparse}"
4000
+ end
4001
+ connect(options)
4002
+
4003
+ begin
4004
+ cluster = find_cluster_by_name_or_id(args[0])
4005
+ return 1 if cluster.nil?
4006
+ host_vm_group = find_cluster_host_vm_group_by_name_or_id(cluster['id'], args[1])
4007
+ if host_vm_group.nil?
4008
+ print_red_alert "Host/VM Group not found by '#{args[1]}'"
4009
+ exit 1
4010
+ end
4011
+ payload = nil
4012
+ if options[:payload]
4013
+ payload = options[:payload]
4014
+ if options[:options]
4015
+ payload.deep_merge!({'hostVmGroup' => options[:options].reject {|k,v| k.is_a?(Symbol) }})
4016
+ end
4017
+ else
4018
+ payload = {'hostVmGroup' => {}}
4019
+ options[:params] ||= {}
4020
+ options[:params].merge!({:serverGroupId => cluster['id']})
4021
+ option_types = update_host_vm_group_option_types
4022
+ v_prompt = Morpheus::Cli::OptionTypes.no_prompt(option_types, options[:options], @api_client, options[:params])
4023
+ v_prompt.deep_compact!.booleanize!
4024
+ payload.deep_merge!({'hostVmGroup' => v_prompt})
4025
+
4026
+ perms = prompt_permissions(options.merge({:no_prompt => true}), is_master_account ? ['plans'] : ['plans', 'visibility', 'tenants'])
4027
+ perms_payload = {}
4028
+ perms_payload['resourcePermissions'] = perms['resourcePermissions'] if !perms['resourcePermissions'].nil? && !perms['resourcePermissions'].empty?
4029
+ perms_payload['tenantPermissions'] = perms['tenantPermissions'] if !perms['tenantPermissions'].nil? && !perms['tenantPermissions']['accounts'].empty?
4030
+ payload['hostVmGroup']['permissions'] = perms_payload if !perms_payload.empty?
4031
+ payload['hostVmGroup']['visibility'] = perms['resourcePool']['visibility'] if !perms['resourcePool'].nil? && !perms['resourcePool']['visibility'].nil?
4032
+
4033
+ if options[:options]
4034
+ payload.deep_merge!({'hostVmGroup' => options[:options].reject {|k,v| k.is_a?(Symbol) || payload['hostVmGroup'].key?(k) }})
4035
+ end
4036
+
4037
+ if payload['hostVmGroup'].nil? || payload['hostVmGroup'].empty?
4038
+ raise_command_error "Specify at least one option to update.\n#{optparse}"
4039
+ end
4040
+ end
4041
+
4042
+ @clusters_interface.setopts(options)
4043
+ if options[:dry_run]
4044
+ print_dry_run @clusters_interface.dry.update_host_vm_group(cluster['id'], host_vm_group['id'], payload)
4045
+ return
4046
+ end
4047
+ json_response = @clusters_interface.update_host_vm_group(cluster['id'], host_vm_group['id'], payload)
4048
+ if options[:json]
4049
+ puts as_json(json_response)
4050
+ elsif !options[:quiet]
4051
+ updated = json_response['hostVmGroup'] || host_vm_group
4052
+ print_green_success "Updated host/VM group #{updated['name'] || updated['id']}"
4053
+ end
4054
+ return 0
4055
+ rescue RestClient::Exception => e
4056
+ print_rest_exception(e, options)
4057
+ exit 1
4058
+ end
4059
+ end
4060
+
4061
+ def remove_host_vm_group(args)
4062
+ params = {}
4063
+ options = {}
4064
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
4065
+ opts.banner = subcommand_usage("[cluster] [host-vm-group]")
4066
+ build_standard_remove_options(opts, options)
4067
+ opts.footer = "Delete a host/VM group from a cluster.\n" +
4068
+ "[cluster] is required. This is the name or id of an existing cluster.\n" +
4069
+ "[host-vm-group] is required. This is the name or id of an existing host/VM group."
4070
+ end
4071
+ optparse.parse!(args)
4072
+ verify_args!(args:args, optparse:optparse, count:2)
4073
+ connect(options)
4074
+ params.merge!(parse_query_options(options))
4075
+
4076
+ cluster = find_cluster_by_name_or_id(args[0])
4077
+ return 1 if cluster.nil?
4078
+ host_vm_group_id = args[1]
4079
+ if host_vm_group_id.empty?
4080
+ raise_command_error "missing required host/VM group parameter"
4081
+ end
4082
+ host_vm_group = find_cluster_host_vm_group_by_name_or_id(cluster['id'], host_vm_group_id)
4083
+ if host_vm_group.nil?
4084
+ print_red_alert "Host/VM Group not found for '#{host_vm_group_id}'"
4085
+ return 1
4086
+ end
4087
+ unless options[:yes] || ::Morpheus::Cli::OptionTypes::confirm("Are you sure you would like to remove the cluster host/VM group '#{host_vm_group['name'] || host_vm_group['id']}'?", options)
4088
+ return 9, "aborted command"
4089
+ end
4090
+ @clusters_interface.setopts(options)
4091
+ if options[:dry_run]
4092
+ print_dry_run @clusters_interface.dry.destroy_host_vm_group(cluster['id'], host_vm_group['id'], params)
4093
+ return
4094
+ end
4095
+ json_response = @clusters_interface.destroy_host_vm_group(cluster['id'], host_vm_group['id'], params)
4096
+ if options[:json]
4097
+ puts as_json(json_response)
4098
+ else
4099
+ if json_response['success']
4100
+ print_green_success "Removed host/VM group #{host_vm_group['name']}"
4101
+ else
4102
+ print_red_alert "Failed to remove cluster host/VM group #{json_response['msg']}"
4103
+ end
4104
+ end
4105
+ return 0, nil
4106
+ end
4107
+
3662
4108
  def api_config(args)
3663
4109
  options = {}
3664
4110
  optparse = Morpheus::Cli::OptionParser.new do |opts|
@@ -4589,11 +5035,17 @@ class Morpheus::Cli::Clusters
4589
5035
  def find_cluster_by_name_or_id(val)
4590
5036
  if val.to_s =~ /\A\d{1,}\Z/
4591
5037
  find_cluster_by_id(val)
5038
+ elsif cluster_uuid?(val)
5039
+ find_cluster_by_uuid(val)
4592
5040
  else
4593
5041
  find_cluster_by_name(val)
4594
5042
  end
4595
5043
  end
4596
5044
 
5045
+ def cluster_uuid?(val)
5046
+ !!(val.to_s =~ /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i)
5047
+ end
5048
+
4597
5049
  def find_cluster_by_id(id)
4598
5050
  json_results = @clusters_interface.get(id.to_i)
4599
5051
  if json_results['cluster'].empty?
@@ -4603,6 +5055,22 @@ class Morpheus::Cli::Clusters
4603
5055
  json_results['cluster']
4604
5056
  end
4605
5057
 
5058
+ def find_cluster_by_uuid(uuid)
5059
+ json_results = @clusters_interface.get(uuid.to_s)
5060
+ if json_results['cluster'].nil? || json_results['cluster'].empty?
5061
+ print_red_alert "Cluster not found by uuid #{uuid}"
5062
+ exit 1
5063
+ end
5064
+ json_results['cluster']
5065
+ rescue RestClient::Exception => e
5066
+ if e.response && e.response.code == 404
5067
+ print_red_alert "Cluster not found by uuid #{uuid}"
5068
+ exit 1
5069
+ else
5070
+ raise e
5071
+ end
5072
+ end
5073
+
4606
5074
  def find_cluster_by_name(name)
4607
5075
  json_results = @clusters_interface.list({name: name})
4608
5076
  if json_results['clusters'].empty? || json_results['clusters'].count > 1
@@ -5343,25 +5811,44 @@ class Morpheus::Cli::Clusters
5343
5811
  end
5344
5812
  end
5345
5813
 
5346
- def add_affinity_group_option_types
5814
+ def add_affinity_group_option_types
5347
5815
  [
5348
5816
  {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true},
5349
- {'fieldName' => 'affinityType', 'fieldLabel' => 'Type', 'type' => 'select', 'selectOptions' => [{'name' => 'Keep Separate', 'value' => 'KEEP_SEPARATE'}, {'name' => 'Keep Together', 'value' => 'KEEP_TOGETHER'}], 'description' => 'Choose affinity type.', 'required' => true, 'defaultValue' => 'KEEP_SEPARATE'},
5817
+ affinity_type_option_type(required: true, default: 'KEEP_TOGETHER'),
5350
5818
  {'fieldName' => 'active', 'fieldLabel' => 'Active', 'type' => 'checkbox', 'defaultValue' => true},
5351
- # {'fieldName' => 'pool.id', 'fieldLabel' => 'Cluster', 'type' => 'select', 'optionSourceType' => 'vmware', 'optionSource' => 'vmwareZonePoolClusters', 'description' => 'Select cluster for the affinity group.', 'required' => true},
5352
- {'fieldName' => 'servers', 'fieldLabel' => 'Server', 'type' => 'multiTypeahead', 'optionSource' => 'searchServers', 'searchParameter' => 'phrase', 'description' => 'Select servers to be in the affinity group.'},
5353
5819
  ]
5354
5820
  end
5355
5821
 
5356
5822
  def update_affinity_group_option_types
5357
5823
  [
5824
+ {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text'},
5825
+ affinity_type_option_type,
5358
5826
  {'fieldName' => 'active', 'fieldLabel' => 'Active', 'type' => 'checkbox'},
5359
- {'fieldName' => 'servers', 'fieldLabel' => 'Server', 'type' => 'multiTypeahead', 'optionSource' => 'searchServers', 'searchParameter' => 'phrase', 'description' => 'Select servers to be in the affinity group.'},
5827
+ {'fieldName' => 'servers', 'fieldLabel' => 'Server', 'type' => 'multiTypeahead', 'optionSource' => 'searchServers',
5828
+ 'searchParameter' => 'phrase', 'description' => 'Select servers to be in the affinity group.'},
5829
+ ]
5830
+ end
5831
+
5832
+ def add_host_vm_group_option_types
5833
+ [
5834
+ {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true},
5835
+ host_vm_group_type_option_type(required: true, default: 'HOST_GROUP'),
5836
+ ]
5837
+ end
5838
+
5839
+ def update_host_vm_group_option_types
5840
+ [
5841
+ {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text'},
5360
5842
  ]
5361
5843
  end
5362
5844
 
5363
- def format_affinity_type(affinity_type)
5364
- affinity_type == "KEEP_SEPARATE" ? "Keep Separate" : "Keep Together"
5845
+ def find_cluster_host_vm_group_by_name_or_id(cluster_id, val)
5846
+ if val.to_s =~ /\A\d{1,}\Z/
5847
+ @clusters_interface.get_host_vm_group(cluster_id, val)['hostVmGroup'] rescue nil
5848
+ else
5849
+ list = @clusters_interface.list_host_vm_groups(cluster_id, {name: val})['hostVmGroups'] || []
5850
+ list.first
5851
+ end
5365
5852
  end
5366
5853
 
5367
5854
  end