morpheus-cli 9.0.0 → 9.0.2

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.
@@ -13,6 +13,7 @@ class Morpheus::Cli::Clusters
13
13
  register_subcommands :list_workers, :add_worker, :remove_worker, :update_worker_count
14
14
  register_subcommands :list_masters
15
15
  register_subcommands :upgrade_cluster
16
+ register_subcommands :update_version
16
17
  register_subcommands :list_volumes, :remove_volume
17
18
  register_subcommands :list_namespaces, :get_namespace, :add_namespace, :update_namespace, :remove_namespace
18
19
  register_subcommands :list_containers, :remove_container, :restart_container, :get_container
@@ -30,6 +31,8 @@ class Morpheus::Cli::Clusters
30
31
  register_subcommands :list_replicasets, :list_daemonsets, :list_endpoints, :list_ingresses, :list_policies, :list_volumes, :list_volume_claims, :list_config_maps, :list_secrets
31
32
  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
32
33
  register_subcommands :list_affinity_groups, :get_affinity_group, :update_affinity_group, :add_affinity_group, :remove_affinity_group
34
+ register_subcommands :'list-affinity-group-violations' => :list_affinity_group_violations
35
+ register_subcommands :list_host_vm_groups, :get_host_vm_group, :add_host_vm_group, :update_host_vm_group, :remove_host_vm_group
33
36
 
34
37
  def connect(opts)
35
38
  @api_client = establish_remote_appliance_connection(opts)
@@ -697,6 +700,13 @@ class Morpheus::Cli::Clusters
697
700
  ssh_host_option = option_type_list.select{|it| it['fieldName'] == 'sshHosts'}.first
698
701
  ssh_host_option['minCount'] = server_count unless ssh_host_option.nil?
699
702
 
703
+ # Fix for bug in 9.0.0 - 9.0.2 with optionType data where defaultValue is set to "unmanaged" for witness.id, which is not a valid value. Remove it so the user can select a valid value.
704
+ option_type_list.each do |option_type|
705
+ if option_type['fieldName'] == 'witness.id' && option_type['defaultValue'] == 'unmanaged'
706
+ option_type.delete('defaultValue')
707
+ end
708
+ end
709
+
700
710
  # Server options
701
711
  server_payload.deep_merge!(Morpheus::Cli::OptionTypes.prompt(option_type_list, options[:options].deep_merge({:context_map => {'domain' => ''}}), @api_client, api_params, options[:no_prompt], true))
702
712
 
@@ -3237,6 +3247,9 @@ class Morpheus::Cli::Clusters
3237
3247
  opts.on('--supports-vm-secure-metadata [on|off]', String, "Enable VM Secure Metadata support") do |val|
3238
3248
  options[:supportsVmSecureMetadata] = val.to_s == 'on' || val.to_s == 'true' || val.to_s == ''
3239
3249
  end
3250
+ opts.on('--heartbeat-target [on|off]', String, "Set as the heartbeat target. Default is on") do |val|
3251
+ options[:heartbeatTarget] = val.to_s == 'on' || val.to_s == 'true' || val.to_s == '1' || val.to_s == ''
3252
+ end
3240
3253
  add_perms_options(opts, options, ['plans', 'groupDefaults'])
3241
3254
  build_common_options(opts, options, [:options, :payload, :json, :dry_run, :remote])
3242
3255
  opts.footer = "Update a cluster datastore.\n" +
@@ -3254,6 +3267,7 @@ class Morpheus::Cli::Clusters
3254
3267
  cluster = find_cluster_by_name_or_id(args[0])
3255
3268
  return 1 if cluster.nil?
3256
3269
  datastore = find_datastore_by_name_or_id(cluster['id'], args[1])
3270
+ datastore_type = find_datastore_type_by_code(datastore['datastoreType']['code']) rescue nil
3257
3271
  if datastore.nil?
3258
3272
  print_red_alert "Datastore not found by '#{args[1]}'"
3259
3273
  exit 1
@@ -3269,6 +3283,14 @@ class Morpheus::Cli::Clusters
3269
3283
  payload = {'datastore' => {}}
3270
3284
  payload['datastore']['active'] = options[:active].nil? ? (Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'active', 'fieldLabel' => 'Active', 'type' => 'checkbox', 'description' => 'Datastore Active', 'defaultValue' => true}], options[:options], @api_client))['active'] == 'on' : options[:active]
3271
3285
  payload['datastore']['supportsVmSecureMetadata'] = options[:supportsVmSecureMetadata] unless options[:supportsVmSecureMetadata].nil?
3286
+
3287
+ if !options[:heartbeatTarget].nil?
3288
+ payload['datastore']['heartbeatTarget'] = options[:heartbeatTarget]
3289
+ elsif datastore_type && datastore_type['heartbeatTargetCapable'] == true
3290
+ val = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'heartbeatTarget', 'fieldLabel' => 'Heartbeat Target', 'type' => 'checkbox', 'description' => 'Heartbeat Target', 'defaultValue' => datastore['heartbeatTarget']}], options[:options], @api_client)['heartbeatTarget']
3291
+ payload['datastore']['heartbeatTarget'] = val == 'on' || val == true unless val.nil? || val == ''
3292
+ end
3293
+
3272
3294
 
3273
3295
  perms = prompt_permissions(options.merge({:available_plans => namespace_service_plans}), datastore['owner']['id'] == current_user['accountId'] ? ['plans', 'groupDefaults'] : ['plans', 'groupDefaults', 'visibility', 'tenants'])
3274
3296
  perms_payload = {}
@@ -3343,10 +3365,11 @@ class Morpheus::Cli::Clusters
3343
3365
  "ID" => 'id',
3344
3366
  "Name" => 'name',
3345
3367
  "Type" => lambda {|it| format_affinity_type(it['affinityType']) },
3368
+ "VM Group" => lambda {|it| it['vmGroup'] ? (it['vmGroup']['name'] || it['vmGroup']['id']) : '' },
3369
+ "Host Group" => lambda {|it| it['hostGroup'] ? (it['hostGroup']['name'] || it['hostGroup']['id']) : '' },
3346
3370
  "Resource Pool" => lambda {|it| it['pool'] ? (it['pool']['name'] || it['pool']['id']) : '' },
3347
3371
  "Visibility" => lambda {|it| it['visibility'].to_s.capitalize },
3348
- "Servers" => lambda {|it| it['servers'].size() },
3349
- # "Source" => lambda {|it| it['source'] },
3372
+ "Servers" => lambda {|it| (it['servers'] || []).size },
3350
3373
  }.upcase_keys!
3351
3374
  print as_pretty_table(affinity_groups, columns, options)
3352
3375
  print_results_pagination(json_response)
@@ -3394,14 +3417,18 @@ class Morpheus::Cli::Clusters
3394
3417
  "ID" => 'id',
3395
3418
  "Name" => 'name',
3396
3419
  "Type" => lambda {|it| format_affinity_type(it['affinityType']) },
3420
+ "Ref Type" => 'refType',
3421
+ "Ref ID" => 'refId',
3422
+ "VM Group" => lambda {|it| it['vmGroup'] ? (it['vmGroup']['name'] || it['vmGroup']['id']) : '' },
3423
+ "Host Group" => lambda {|it| it['hostGroup'] ? (it['hostGroup']['name'] || it['hostGroup']['id']) : '' },
3397
3424
  "Resource Pool" => lambda {|it| it['pool'] ? (it['pool']['name'] || it['pool']['id']) : '' },
3398
3425
  "Visibility" => lambda {|it| it['visibility'].to_s.capitalize },
3399
- "Servers" => lambda {|it| it['servers'].size() },
3426
+ "Servers" => lambda {|it| (it['servers'] || []).size },
3400
3427
  "Source" => lambda {|it| it['source'] },
3401
3428
  "Active" => lambda {|it| format_boolean(it['active']) },
3402
3429
  }
3403
3430
  print_description_list(columns, affinity_group)
3404
- if affinity_group['servers'].size > 0
3431
+ if affinity_group['servers'] && affinity_group['servers'].size > 0
3405
3432
  print_h2 "Servers", options
3406
3433
  print as_pretty_table(affinity_group['servers'], [:id, :name], options)
3407
3434
  end
@@ -3415,6 +3442,18 @@ class Morpheus::Cli::Clusters
3415
3442
  optparse = Morpheus::Cli::OptionParser.new do |opts|
3416
3443
  opts.banner = subcommand_usage( "[cluster] [name] [options]")
3417
3444
  build_option_type_options(opts, options, add_affinity_group_option_types)
3445
+ opts.on('--vm-group ID', String, "VM Group ID (for VM-to-Host rules)") do |val|
3446
+ options[:options] ||= {}
3447
+ options[:options]['vmGroup'] = {'id' => val.to_i}
3448
+ end
3449
+ opts.on('--host-group ID', String, "Host Group ID (for VM-to-Host rules)") do |val|
3450
+ options[:options] ||= {}
3451
+ options[:options]['hostGroup'] = {'id' => val.to_i}
3452
+ end
3453
+ opts.on('--servers ID1,ID2', Array, "Server IDs (for VM-to-VM rules)") do |val|
3454
+ options[:options] ||= {}
3455
+ options[:options]['servers'] = val.map {|s| {'id' => s.to_i}}
3456
+ end
3418
3457
  add_perms_options(opts, options, ['plans'])
3419
3458
  build_standard_add_options(opts, options)
3420
3459
  opts.footer = "Add affinity group to a cluster.\n" +
@@ -3430,6 +3469,7 @@ class Morpheus::Cli::Clusters
3430
3469
  cluster = find_cluster_by_name_or_id(args[0])
3431
3470
  return 1 if cluster.nil?
3432
3471
  if args[1]
3472
+ options[:options] ||= {}
3433
3473
  options[:options]['name'] = args[1]
3434
3474
  end
3435
3475
  if options[:payload]
@@ -3445,13 +3485,72 @@ class Morpheus::Cli::Clusters
3445
3485
  option_types = add_affinity_group_option_types
3446
3486
  affinity_group = Morpheus::Cli::OptionTypes.prompt(option_types, options[:options], @api_client, options[:params])
3447
3487
 
3448
- # affinity_group_type = find_affinity_group_type_by_code(affinity_group['affinityGroupType'])
3449
- # affinity_group['affinityGroupType'] = {id:affinity_group_type['id']}
3488
+ # Rule shape — servers OR vmGroup+hostGroup. Skip if already provided via CLI flags/-O or --payload merge.
3489
+ preset_servers = options[:options] && options[:options].key?('servers')
3490
+ preset_vmhost = options[:options] && options[:options].key?('vmGroup') && options[:options].key?('hostGroup')
3491
+ unless preset_servers || preset_vmhost
3492
+ form_opts = @clusters_interface.affinity_group_form_options(cluster['id']) rescue {}
3493
+ available_servers = form_opts['availableServers'] || []
3494
+ available_vm_groups = form_opts['availableVmGroups'] || []
3495
+ available_host_groups = form_opts['availableHostGroups'] || []
3496
+
3497
+ shape_options = [{'name' => 'VM-to-VM (pick servers)', 'value' => 'vmToVm'}]
3498
+ if !available_vm_groups.empty? && !available_host_groups.empty?
3499
+ shape_options << {'name' => 'VM-to-Host (pick groups)', 'value' => 'vmToHost'}
3500
+ end
3501
+ shape = if shape_options.size == 1
3502
+ 'vmToVm'
3503
+ else
3504
+ Morpheus::Cli::OptionTypes.prompt([{
3505
+ 'fieldName' => 'shape', 'fieldLabel' => 'Rule Shape', 'type' => 'select', 'required' => true,
3506
+ 'defaultValue' => 'vmToVm', 'selectOptions' => shape_options
3507
+ }], options[:options], @api_client, options[:params])['shape']
3508
+ end
3450
3509
 
3451
- # # affinity_group type options
3452
- # unless affinity_group_type['optionTypes'].empty?
3453
- # 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]))
3454
- # end
3510
+ if shape == 'vmToVm'
3511
+ if available_servers.empty?
3512
+ # Fall back to the classic multiTypeahead prompt when the form-options endpoint returns nothing
3513
+ server_prompt = Morpheus::Cli::OptionTypes.prompt([
3514
+ {'fieldName' => 'servers', 'fieldLabel' => 'Server', 'type' => 'multiTypeahead', 'optionSource' => 'searchServers', 'searchParameter' => 'phrase', 'description' => 'Select servers to be in the affinity group.'}
3515
+ ], options[:options], @api_client, options[:params])
3516
+ affinity_group['servers'] = server_prompt['servers'] if server_prompt['servers']
3517
+ else
3518
+ print cyan, "Available Servers:", reset, "\n"
3519
+ available_servers.each {|s| print " #{s['id']}\t#{s['name']}\n" }
3520
+ raw = Morpheus::Cli::OptionTypes.prompt([{
3521
+ 'fieldName' => 'servers', 'fieldLabel' => 'Servers (comma-separated IDs)', 'type' => 'text', 'required' => true,
3522
+ 'description' => 'Server IDs to include, comma-separated'
3523
+ }], options[:options], @api_client, options[:params])['servers']
3524
+ selected_ids = raw.to_s.split(',').map(&:strip).reject(&:empty?).map(&:to_i)
3525
+ valid_ids = available_servers.map {|s| s['id'].to_i}
3526
+ invalid = selected_ids - valid_ids
3527
+ if !invalid.empty?
3528
+ print_red_alert "Invalid server IDs for this cluster: #{invalid.join(', ')}"
3529
+ return 1
3530
+ end
3531
+ if selected_ids.empty?
3532
+ print_red_alert "At least one server ID is required"
3533
+ return 1
3534
+ end
3535
+ affinity_group['servers'] = selected_ids.map {|id| {'id' => id}}
3536
+ end
3537
+ else
3538
+ vm_group_id = Morpheus::Cli::OptionTypes.prompt([{
3539
+ 'fieldName' => 'vmGroup', 'fieldLabel' => 'VM Group', 'type' => 'select', 'required' => true,
3540
+ 'selectOptions' => available_vm_groups.map {|g| {'name' => g['name'], 'value' => g['id']} }
3541
+ }], options[:options], @api_client, options[:params])['vmGroup']
3542
+ host_group_id = Morpheus::Cli::OptionTypes.prompt([{
3543
+ 'fieldName' => 'hostGroup', 'fieldLabel' => 'Host Group', 'type' => 'select', 'required' => true,
3544
+ 'selectOptions' => available_host_groups.map {|g| {'name' => g['name'], 'value' => g['id']} }
3545
+ }], options[:options], @api_client, options[:params])['hostGroup']
3546
+ affinity_group['vmGroup'] = {'id' => vm_group_id.to_i}
3547
+ affinity_group['hostGroup'] = {'id' => host_group_id.to_i}
3548
+ end
3549
+ else
3550
+ affinity_group['servers'] = options[:options]['servers'] if preset_servers
3551
+ affinity_group['vmGroup'] = options[:options]['vmGroup'] if preset_vmhost
3552
+ affinity_group['hostGroup'] = options[:options]['hostGroup'] if preset_vmhost
3553
+ end
3455
3554
 
3456
3555
  # perms
3457
3556
  perms = prompt_permissions(options, is_master_account ? ['plans'] : ['plans', 'visibility', 'tenants'])
@@ -3639,6 +3738,367 @@ class Morpheus::Cli::Clusters
3639
3738
  return 0, nil
3640
3739
  end
3641
3740
 
3741
+ def list_affinity_group_violations(args)
3742
+ options = {}
3743
+ params = {}
3744
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
3745
+ opts.banner = subcommand_usage("[cluster]")
3746
+ build_standard_list_options(opts, options)
3747
+ opts.footer = "List active MUST-rule affinity violations.\n" +
3748
+ "[cluster] is required. This is the name or id of an existing cluster."
3749
+ end
3750
+ optparse.parse!(args)
3751
+ verify_args!(args:args, optparse:optparse, count:1)
3752
+ connect(options)
3753
+ cluster = find_cluster_by_name_or_id(args[0])
3754
+ return 1 if cluster.nil?
3755
+ params['clusterId'] = cluster['id']
3756
+ params.merge!(parse_list_options(options))
3757
+ @clusters_interface.setopts(options)
3758
+ if options[:dry_run]
3759
+ print_dry_run @clusters_interface.dry.affinity_group_violations(params)
3760
+ return
3761
+ end
3762
+ json_response = @clusters_interface.affinity_group_violations(params)
3763
+ render_response(json_response, options, 'violations') do
3764
+ rows = json_response['violations'] || []
3765
+ print_h1 "Affinity Rule Violations", parse_list_subtitles(options), options
3766
+ if rows.empty?
3767
+ print cyan, "No violations.", reset, "\n"
3768
+ else
3769
+ columns = {
3770
+ "Rule" => 'ruleName',
3771
+ "Type" => lambda {|r| r['keepTogether'] ? 'Keep Together' : 'Keep Separate' },
3772
+ "VMs" => lambda {|r| (r['affectedVms'] || []).map {|v| v['name']}.join(', ') },
3773
+ "Occurred" => 'occurredAt'
3774
+ }.upcase_keys!
3775
+ print as_pretty_table(rows, columns, options)
3776
+ end
3777
+ print reset, "\n"
3778
+ end
3779
+ return 0, nil
3780
+ end
3781
+
3782
+ def list_host_vm_groups(args)
3783
+ options = {}
3784
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
3785
+ opts.banner = subcommand_usage("[cluster]")
3786
+ build_standard_list_options(opts, options)
3787
+ opts.footer = "List host/VM groups on a cluster.\n" +
3788
+ "[cluster] is required. This is the name or id of an existing cluster."
3789
+ end
3790
+ optparse.parse!(args)
3791
+ verify_args!(args:args, optparse:optparse, count:1)
3792
+ connect(options)
3793
+ cluster = find_cluster_by_name_or_id(args[0])
3794
+ return 1 if cluster.nil?
3795
+ params = {}
3796
+ params.merge!(parse_list_options(options))
3797
+ @clusters_interface.setopts(options)
3798
+ if options[:dry_run]
3799
+ print_dry_run @clusters_interface.dry.list_host_vm_groups(cluster['id'], params)
3800
+ return
3801
+ end
3802
+ json_response = @clusters_interface.list_host_vm_groups(cluster['id'], params)
3803
+ render_response(json_response, options, 'hostVmGroups') do
3804
+ rows = json_response['hostVmGroups'] || []
3805
+ print_h1 "Morpheus Cluster Host/VM Groups: #{cluster['name']}", parse_list_subtitles(options), options
3806
+ if rows.empty?
3807
+ print cyan, "No host/VM groups found.", reset, "\n"
3808
+ else
3809
+ columns = {
3810
+ "ID" => 'id',
3811
+ "Name" => 'name',
3812
+ "Type" => lambda {|it| format_host_vm_group_type(it['type']) },
3813
+ "Servers" => lambda {|it| (it['servers'] || []).size },
3814
+ "Visibility" => lambda {|it| it['visibility'].to_s.capitalize },
3815
+ }.upcase_keys!
3816
+ print as_pretty_table(rows, columns, options)
3817
+ print_results_pagination(json_response)
3818
+ end
3819
+ print reset, "\n"
3820
+ end
3821
+ return 0, nil
3822
+ end
3823
+
3824
+ def get_host_vm_group(args)
3825
+ options = {}
3826
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
3827
+ opts.banner = subcommand_usage("[cluster] [host-vm-group]")
3828
+ build_standard_get_options(opts, options)
3829
+ opts.footer = "Get details about a cluster host/VM group.\n" +
3830
+ "[cluster] is required. This is the name or id of an existing cluster.\n" +
3831
+ "[host-vm-group] is required. This is the name or id of an existing host/VM group."
3832
+ end
3833
+ optparse.parse!(args)
3834
+ verify_args!(args:args, optparse:optparse, count:2)
3835
+ connect(options)
3836
+ cluster = find_cluster_by_name_or_id(args[0])
3837
+ return 1 if cluster.nil?
3838
+ host_vm_group = find_cluster_host_vm_group_by_name_or_id(cluster['id'], args[1])
3839
+ if host_vm_group.nil?
3840
+ print_red_alert "Host/VM Group not found for '#{args[1]}'"
3841
+ exit 1
3842
+ end
3843
+ params = {}
3844
+ params.merge!(parse_query_options(options))
3845
+ @clusters_interface.setopts(options)
3846
+ if options[:dry_run]
3847
+ print_dry_run @clusters_interface.dry.get_host_vm_group(cluster['id'], host_vm_group['id'], params)
3848
+ return
3849
+ end
3850
+ json_response = @clusters_interface.get_host_vm_group(cluster['id'], host_vm_group['id'], params)
3851
+ render_response(json_response, options, 'hostVmGroup') do
3852
+ row = json_response['hostVmGroup']
3853
+ print_h1 "Host/VM Group Details", [], options
3854
+ columns = {
3855
+ "ID" => 'id',
3856
+ "Name" => 'name',
3857
+ "Type" => lambda {|it| format_host_vm_group_type(it['type']) },
3858
+ "Ref Type" => 'refType',
3859
+ "Ref ID" => 'refId',
3860
+ "Servers" => lambda {|it| (it['servers'] || []).size },
3861
+ "Visibility" => lambda {|it| it['visibility'].to_s.capitalize },
3862
+ "Active" => lambda {|it| format_boolean(it['active']) },
3863
+ }
3864
+ print_description_list(columns, row)
3865
+ if row['servers'] && row['servers'].size > 0
3866
+ print_h2 "Servers", options
3867
+ print as_pretty_table(row['servers'], [:id, :name], options)
3868
+ end
3869
+ print reset, "\n"
3870
+ end
3871
+ return 0, nil
3872
+ end
3873
+
3874
+ def add_host_vm_group(args)
3875
+ options = {}
3876
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
3877
+ opts.banner = subcommand_usage("[cluster] [name] [options]")
3878
+ build_option_type_options(opts, options, add_host_vm_group_option_types)
3879
+ opts.on('--servers ID1,ID2', Array, "Server IDs to include") do |val|
3880
+ options[:options] ||= {}
3881
+ options[:options]['servers'] = val.map {|s| {'id' => s.to_i}}
3882
+ end
3883
+ add_perms_options(opts, options, ['plans'])
3884
+ build_standard_add_options(opts, options)
3885
+ opts.footer = "Add a host/VM group to a cluster.\n" +
3886
+ "[cluster] is required. This is the name or id of an existing cluster.\n" +
3887
+ "[name] is required. This is the name of the new host/VM group."
3888
+ end
3889
+ optparse.parse!(args)
3890
+ verify_args!(args:args, optparse:optparse, min:1, max:2)
3891
+ connect(options)
3892
+
3893
+ begin
3894
+ cluster = find_cluster_by_name_or_id(args[0])
3895
+ return 1 if cluster.nil?
3896
+ if args[1]
3897
+ options[:options] ||= {}
3898
+ options[:options]['name'] = args[1]
3899
+ end
3900
+ if options[:payload]
3901
+ payload = options[:payload]
3902
+ if options[:options]
3903
+ payload ||= {}
3904
+ payload.deep_merge!(options[:options].reject {|k,v| k.is_a?(Symbol) })
3905
+ end
3906
+ else
3907
+ options[:params] ||= {}
3908
+ options[:params].merge!({:serverGroupId => cluster['id']})
3909
+ option_types = add_host_vm_group_option_types
3910
+ host_vm_group = Morpheus::Cli::OptionTypes.prompt(option_types, options[:options], @api_client, options[:params])
3911
+
3912
+ # Servers — form-options driven, filtered by chosen type
3913
+ unless options[:options] && options[:options].key?('servers')
3914
+ form_opts = @clusters_interface.host_vm_group_form_options(cluster['id'], {'hostVmGroupType' => host_vm_group['type']}) rescue {}
3915
+ available_servers = form_opts['availableServers'] || []
3916
+ if available_servers.empty?
3917
+ print_red_alert "No servers available on cluster #{cluster['name']} for type #{host_vm_group['type']}"
3918
+ return 1
3919
+ end
3920
+ print cyan, "Available Servers:", reset, "\n"
3921
+ available_servers.each {|s| print " #{s['id']}\t#{s['name']}\n" }
3922
+ raw = Morpheus::Cli::OptionTypes.prompt([{
3923
+ 'fieldName' => 'servers', 'fieldLabel' => 'Servers (comma-separated IDs)', 'type' => 'text', 'required' => true,
3924
+ 'description' => 'Server IDs to include, comma-separated'
3925
+ }], options[:options], @api_client, options[:params])['servers']
3926
+ selected_ids = raw.to_s.split(',').map(&:strip).reject(&:empty?).map(&:to_i)
3927
+ valid_ids = available_servers.map {|s| s['id'].to_i}
3928
+ invalid = selected_ids - valid_ids
3929
+ if !invalid.empty?
3930
+ print_red_alert "Invalid server IDs for this cluster/type: #{invalid.join(', ')}"
3931
+ return 1
3932
+ end
3933
+ if selected_ids.empty?
3934
+ print_red_alert "At least one server ID is required"
3935
+ return 1
3936
+ end
3937
+ host_vm_group['servers'] = selected_ids.map {|id| {'id' => id}}
3938
+ else
3939
+ host_vm_group['servers'] = options[:options]['servers']
3940
+ end
3941
+
3942
+ perms = prompt_permissions(options, is_master_account ? ['plans'] : ['plans', 'visibility', 'tenants'])
3943
+ host_vm_group['resourcePermissions'] = perms['resourcePermissions'] unless perms['resourcePermissions'].nil?
3944
+ host_vm_group['tenants'] = perms['tenantPermissions'] unless perms['tenantPermissions'].nil? || perms['tenantPermissions']['accounts'].empty?
3945
+ host_vm_group['visibility'] = perms['resourcePool']['visibility'] if !perms['resourcePool'].nil? && !perms['resourcePool']['visibility'].nil?
3946
+
3947
+ payload = {'hostVmGroup' => host_vm_group}
3948
+ end
3949
+
3950
+ @clusters_interface.setopts(options)
3951
+ if options[:dry_run]
3952
+ print_dry_run @clusters_interface.dry.create_host_vm_group(cluster['id'], payload)
3953
+ return
3954
+ end
3955
+ json_response = @clusters_interface.create_host_vm_group(cluster['id'], payload)
3956
+ if options[:json]
3957
+ puts as_json(json_response)
3958
+ else
3959
+ if json_response['success']
3960
+ print_green_success (json_response['msg'] || "Added host/VM group to cluster #{cluster['name']}")
3961
+ else
3962
+ print_red_alert "Failed to create host/VM group #{json_response['msg']}"
3963
+ end
3964
+ end
3965
+ return 0
3966
+ rescue RestClient::Exception => e
3967
+ print_rest_exception(e, options)
3968
+ exit 1
3969
+ end
3970
+ end
3971
+
3972
+ def update_host_vm_group(args)
3973
+ options = {}
3974
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
3975
+ opts.banner = subcommand_usage("[cluster] [host-vm-group] [options]")
3976
+ build_option_type_options(opts, options, update_host_vm_group_option_types)
3977
+ opts.on('--servers ID1,ID2', Array, "Replace server IDs") do |val|
3978
+ options[:options] ||= {}
3979
+ options[:options]['servers'] = val.map {|s| {'id' => s.to_i}}
3980
+ end
3981
+ opts.on('--visibility VISIBILITY', String, "Visibility: private | public") do |val|
3982
+ options[:options] ||= {}
3983
+ options[:options]['visibility'] = val
3984
+ end
3985
+ add_perms_options(opts, options, ['plans'])
3986
+ build_standard_update_options(opts, options)
3987
+ opts.footer = "Update a cluster host/VM group.\n" +
3988
+ "[cluster] is required. This is the name or id of an existing cluster.\n" +
3989
+ "[host-vm-group] is required. This is the name or id of an existing host/VM group."
3990
+ end
3991
+ optparse.parse!(args)
3992
+ if args.count != 2
3993
+ raise_command_error "wrong number of arguments, expected 2 and got (#{args.count}) #{args}\n#{optparse}"
3994
+ end
3995
+ connect(options)
3996
+
3997
+ begin
3998
+ cluster = find_cluster_by_name_or_id(args[0])
3999
+ return 1 if cluster.nil?
4000
+ host_vm_group = find_cluster_host_vm_group_by_name_or_id(cluster['id'], args[1])
4001
+ if host_vm_group.nil?
4002
+ print_red_alert "Host/VM Group not found by '#{args[1]}'"
4003
+ exit 1
4004
+ end
4005
+ payload = nil
4006
+ if options[:payload]
4007
+ payload = options[:payload]
4008
+ if options[:options]
4009
+ payload.deep_merge!({'hostVmGroup' => options[:options].reject {|k,v| k.is_a?(Symbol) }})
4010
+ end
4011
+ else
4012
+ payload = {'hostVmGroup' => {}}
4013
+ options[:params] ||= {}
4014
+ options[:params].merge!({:serverGroupId => cluster['id']})
4015
+ option_types = update_host_vm_group_option_types
4016
+ v_prompt = Morpheus::Cli::OptionTypes.no_prompt(option_types, options[:options], @api_client, options[:params])
4017
+ v_prompt.deep_compact!.booleanize!
4018
+ payload.deep_merge!({'hostVmGroup' => v_prompt})
4019
+
4020
+ perms = prompt_permissions(options.merge({:no_prompt => true}), is_master_account ? ['plans'] : ['plans', 'visibility', 'tenants'])
4021
+ perms_payload = {}
4022
+ perms_payload['resourcePermissions'] = perms['resourcePermissions'] if !perms['resourcePermissions'].nil? && !perms['resourcePermissions'].empty?
4023
+ perms_payload['tenantPermissions'] = perms['tenantPermissions'] if !perms['tenantPermissions'].nil? && !perms['tenantPermissions']['accounts'].empty?
4024
+ payload['hostVmGroup']['permissions'] = perms_payload if !perms_payload.empty?
4025
+ payload['hostVmGroup']['visibility'] = perms['resourcePool']['visibility'] if !perms['resourcePool'].nil? && !perms['resourcePool']['visibility'].nil?
4026
+
4027
+ if options[:options]
4028
+ payload.deep_merge!({'hostVmGroup' => options[:options].reject {|k,v| k.is_a?(Symbol) || payload['hostVmGroup'].key?(k) }})
4029
+ end
4030
+
4031
+ if payload['hostVmGroup'].nil? || payload['hostVmGroup'].empty?
4032
+ raise_command_error "Specify at least one option to update.\n#{optparse}"
4033
+ end
4034
+ end
4035
+
4036
+ @clusters_interface.setopts(options)
4037
+ if options[:dry_run]
4038
+ print_dry_run @clusters_interface.dry.update_host_vm_group(cluster['id'], host_vm_group['id'], payload)
4039
+ return
4040
+ end
4041
+ json_response = @clusters_interface.update_host_vm_group(cluster['id'], host_vm_group['id'], payload)
4042
+ if options[:json]
4043
+ puts as_json(json_response)
4044
+ elsif !options[:quiet]
4045
+ updated = json_response['hostVmGroup'] || host_vm_group
4046
+ print_green_success "Updated host/VM group #{updated['name'] || updated['id']}"
4047
+ end
4048
+ return 0
4049
+ rescue RestClient::Exception => e
4050
+ print_rest_exception(e, options)
4051
+ exit 1
4052
+ end
4053
+ end
4054
+
4055
+ def remove_host_vm_group(args)
4056
+ params = {}
4057
+ options = {}
4058
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
4059
+ opts.banner = subcommand_usage("[cluster] [host-vm-group]")
4060
+ build_standard_remove_options(opts, options)
4061
+ opts.footer = "Delete a host/VM group from a cluster.\n" +
4062
+ "[cluster] is required. This is the name or id of an existing cluster.\n" +
4063
+ "[host-vm-group] is required. This is the name or id of an existing host/VM group."
4064
+ end
4065
+ optparse.parse!(args)
4066
+ verify_args!(args:args, optparse:optparse, count:2)
4067
+ connect(options)
4068
+ params.merge!(parse_query_options(options))
4069
+
4070
+ cluster = find_cluster_by_name_or_id(args[0])
4071
+ return 1 if cluster.nil?
4072
+ host_vm_group_id = args[1]
4073
+ if host_vm_group_id.empty?
4074
+ raise_command_error "missing required host/VM group parameter"
4075
+ end
4076
+ host_vm_group = find_cluster_host_vm_group_by_name_or_id(cluster['id'], host_vm_group_id)
4077
+ if host_vm_group.nil?
4078
+ print_red_alert "Host/VM Group not found for '#{host_vm_group_id}'"
4079
+ return 1
4080
+ end
4081
+ 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)
4082
+ return 9, "aborted command"
4083
+ end
4084
+ @clusters_interface.setopts(options)
4085
+ if options[:dry_run]
4086
+ print_dry_run @clusters_interface.dry.destroy_host_vm_group(cluster['id'], host_vm_group['id'], params)
4087
+ return
4088
+ end
4089
+ json_response = @clusters_interface.destroy_host_vm_group(cluster['id'], host_vm_group['id'], params)
4090
+ if options[:json]
4091
+ puts as_json(json_response)
4092
+ else
4093
+ if json_response['success']
4094
+ print_green_success "Removed host/VM group #{host_vm_group['name']}"
4095
+ else
4096
+ print_red_alert "Failed to remove cluster host/VM group #{json_response['msg']}"
4097
+ end
4098
+ end
4099
+ return 0, nil
4100
+ end
4101
+
3642
4102
  def api_config(args)
3643
4103
  options = {}
3644
4104
  optparse = Morpheus::Cli::OptionParser.new do |opts|
@@ -4426,6 +4886,58 @@ class Morpheus::Cli::Clusters
4426
4886
  Morpheus::Cli::ClusterTypes.new.get(args)
4427
4887
  end
4428
4888
 
4889
+ def update_version(args)
4890
+ params = {}
4891
+ options = {}
4892
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
4893
+ opts.banner = subcommand_usage("[cluster]")
4894
+ build_standard_update_options(opts, options, [:auto_confirm])
4895
+ opts.footer = "Updates HVM cluster layout to a new version.\n" +
4896
+ "[cluster] is required. This is the name or id of an existing cluster.\n"
4897
+ end
4898
+ optparse.parse!(args)
4899
+ verify_args!(args:args, optparse:optparse, count:1)
4900
+ connect(options)
4901
+
4902
+ cluster = find_cluster_by_name_or_id(args[0])
4903
+ return 1 if cluster.nil?
4904
+
4905
+ payload = {}
4906
+ if options[:payload]
4907
+ payload = options[:payload]
4908
+ payload.deep_merge!(parse_passed_options(options))
4909
+ else
4910
+ payload.deep_merge!(parse_passed_options(options))
4911
+
4912
+ available_updates = @clusters_interface.available_updates(cluster['id'])['updateDefinitions']
4913
+ if available_updates.empty?
4914
+ print yellow,"No updates available for cluster #{cluster['name']}",reset,"\n"
4915
+ return 1, "No updates available for cluster #{cluster['name']}"
4916
+ end
4917
+ version_options = available_updates.collect { |it| {'name' => it['name'], 'value' => it['id']} }
4918
+ update_definition_id = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'updateDefinitionId', 'type' => 'select', 'fieldLabel' => 'Update', 'selectOptions' => version_options, 'required' => true, 'description' => 'Select version update to execute' }],options[:options],api_client,{})['updateDefinitionId']
4919
+ payload.deep_merge!({'updateDefinitionId' => update_definition_id})
4920
+ end
4921
+ target_version = available_updates.find { |it| it['id'] == payload['updateDefinitionId'] }['updateVersion'] rescue nil
4922
+ if target_version.nil?
4923
+ print_red_alert "Unable to determine target version for update definition #{payload['updateDefinitionId']}"
4924
+ return 1, "Unable to determine target version for update definition #{payload['updateDefinitionId']}"
4925
+ end
4926
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to update cluster #{cluster['name']} to version #{target_version}?")
4927
+ return 9, "aborted command"
4928
+ end
4929
+ @clusters_interface.setopts(options)
4930
+ if options[:dry_run]
4931
+ print_dry_run @clusters_interface.dry.execute_update(cluster['id'], payload)
4932
+ return
4933
+ end
4934
+ json_response = @clusters_interface.execute_update(cluster['id'], payload)
4935
+ render_response(json_response, options) do
4936
+ print_green_success "Cluster #{cluster['name']} is being updated to version #{target_version}..."
4937
+ end
4938
+ return 0, nil
4939
+ end
4940
+
4429
4941
  private
4430
4942
 
4431
4943
  def print_clusters_table(clusters, opts={})
@@ -5274,22 +5786,71 @@ class Morpheus::Cli::Clusters
5274
5786
  def add_affinity_group_option_types
5275
5787
  [
5276
5788
  {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true},
5277
- {'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'},
5789
+ {'fieldName' => 'affinityType', 'fieldLabel' => 'Type', 'type' => 'select', 'selectOptions' => [
5790
+ {'name' => 'Keep Together (Should)', 'value' => 'KEEP_TOGETHER'},
5791
+ {'name' => 'Keep Separate (Should)', 'value' => 'KEEP_SEPARATE'},
5792
+ {'name' => 'Keep Together (Must)', 'value' => 'KEEP_TOGETHER_MUST'},
5793
+ {'name' => 'Keep Separate (Must)', 'value' => 'KEEP_SEPARATE_MUST'}
5794
+ ], 'description' => 'Choose affinity type.', 'required' => true, 'defaultValue' => 'KEEP_TOGETHER'},
5278
5795
  {'fieldName' => 'active', 'fieldLabel' => 'Active', 'type' => 'checkbox', 'defaultValue' => true},
5279
- # {'fieldName' => 'pool.id', 'fieldLabel' => 'Cluster', 'type' => 'select', 'optionSourceType' => 'vmware', 'optionSource' => 'vmwareZonePoolClusters', 'description' => 'Select cluster for the affinity group.', 'required' => true},
5280
- {'fieldName' => 'servers', 'fieldLabel' => 'Server', 'type' => 'multiTypeahead', 'optionSource' => 'searchServers', 'searchParameter' => 'phrase', 'description' => 'Select servers to be in the affinity group.'},
5281
5796
  ]
5282
5797
  end
5283
5798
 
5284
5799
  def update_affinity_group_option_types
5285
5800
  [
5801
+ {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text'},
5802
+ {'fieldName' => 'affinityType', 'fieldLabel' => 'Type', 'type' => 'select', 'selectOptions' => [
5803
+ {'name' => 'Keep Together (Should)', 'value' => 'KEEP_TOGETHER'},
5804
+ {'name' => 'Keep Separate (Should)', 'value' => 'KEEP_SEPARATE'},
5805
+ {'name' => 'Keep Together (Must)', 'value' => 'KEEP_TOGETHER_MUST'},
5806
+ {'name' => 'Keep Separate (Must)', 'value' => 'KEEP_SEPARATE_MUST'}
5807
+ ], 'description' => 'Change affinity type.'},
5286
5808
  {'fieldName' => 'active', 'fieldLabel' => 'Active', 'type' => 'checkbox'},
5287
5809
  {'fieldName' => 'servers', 'fieldLabel' => 'Server', 'type' => 'multiTypeahead', 'optionSource' => 'searchServers', 'searchParameter' => 'phrase', 'description' => 'Select servers to be in the affinity group.'},
5288
5810
  ]
5289
5811
  end
5290
5812
 
5291
5813
  def format_affinity_type(affinity_type)
5292
- affinity_type == "KEEP_SEPARATE" ? "Keep Separate" : "Keep Together"
5814
+ case affinity_type.to_s
5815
+ when 'KEEP_SEPARATE' then 'Keep Separate'
5816
+ when 'KEEP_TOGETHER' then 'Keep Together'
5817
+ when 'KEEP_SEPARATE_MUST' then 'Keep Separate (Must)'
5818
+ when 'KEEP_TOGETHER_MUST' then 'Keep Together (Must)'
5819
+ else affinity_type.to_s
5820
+ end
5821
+ end
5822
+
5823
+ def add_host_vm_group_option_types
5824
+ [
5825
+ {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true},
5826
+ {'fieldName' => 'type', 'fieldLabel' => 'Group Type', 'type' => 'select', 'selectOptions' => [
5827
+ {'name' => 'Host Group', 'value' => 'HOST_GROUP'},
5828
+ {'name' => 'VM Group', 'value' => 'VM_GROUP'}
5829
+ ], 'required' => true, 'defaultValue' => 'HOST_GROUP', 'description' => 'Choose group type.'},
5830
+ ]
5831
+ end
5832
+
5833
+ def update_host_vm_group_option_types
5834
+ [
5835
+ {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text'},
5836
+ ]
5837
+ end
5838
+
5839
+ def format_host_vm_group_type(type)
5840
+ case type.to_s
5841
+ when 'HOST_GROUP' then 'Host Group'
5842
+ when 'VM_GROUP' then 'VM Group'
5843
+ else type.to_s
5844
+ end
5845
+ end
5846
+
5847
+ def find_cluster_host_vm_group_by_name_or_id(cluster_id, val)
5848
+ if val.to_s =~ /\A\d{1,}\Z/
5849
+ @clusters_interface.get_host_vm_group(cluster_id, val)['hostVmGroup'] rescue nil
5850
+ else
5851
+ list = @clusters_interface.list_host_vm_groups(cluster_id, {name: val})['hostVmGroups'] || []
5852
+ list.first
5853
+ end
5293
5854
  end
5294
5855
 
5295
5856
  end