morpheus-cli 8.0.9.1 → 8.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 11090de632f0ba25e07141f1e169cbe0d626aafb4180cf472711c0f4a97c9799
4
- data.tar.gz: 256e3748b3805cf0490ca9d0d6acc8c255743b535de5ab46159f1cb11169c012
3
+ metadata.gz: b4fc087520efe1dab49dbe42df70b39da196e9539cd78eb162adec2fc8907a54
4
+ data.tar.gz: b008d63500f56d6a72b12c918db54b3d2869df3fa3407e491411a8eab63ea6be
5
5
  SHA512:
6
- metadata.gz: f265f198cba356cfbda69235e80e6cd95b2261dcc2482ec08603ed463067af56c6acef8a7989aa079a78d8937bd33d5d5734e15956688bf656022b69b0f75298
7
- data.tar.gz: 0db70e3343c6851efaeaaf487c86b92b6a652550bc713e78b435c3ff377e0d7244ae5eb6bfe96348ee22cd1251aedf3e2156d3196ac9137b826242ec160f2bec
6
+ metadata.gz: f00f2ea9c25064ad69f1038a83a5c0a23708c21d2bc670406ff1527dcdd0808361043c7e7c498fd9a03733106fcd8c1884515ebd4951c12b1bc839c37163b879
7
+ data.tar.gz: 8c818cdda02325d65715ee474e72480467a27ce14548483415a555002042cfbb7638193feb9fc3c61138effa50d473703eeb4a6f66d47eb4ccf087e9c9b040c5
data/Dockerfile CHANGED
@@ -1,5 +1,5 @@
1
1
  FROM ruby:2.7.5
2
2
 
3
- RUN gem install morpheus-cli -v 8.0.9.1
3
+ RUN gem install morpheus-cli -v 8.0.10
4
4
 
5
5
  ENTRYPOINT ["morpheus"]
@@ -339,6 +339,13 @@ class Morpheus::ClustersInterface < Morpheus::APIClient
339
339
  execute(opts)
340
340
  end
341
341
 
342
+ def can_use_kubevip(payload)
343
+ url = "#{@base_url}/api/clusters/can-use-kubevip"
344
+ payload["requestSourceType"] = "api"
345
+ headers = { authorization: "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
346
+ execute(method: :get, url: url, headers: headers, payload: payload.to_json)
347
+ end
348
+
342
349
  def list_resources(id, resources, params={})
343
350
  url = "#{base_path}/#{id}/#{resources}"
344
351
  headers = { params: params, authorization: "Bearer #{@access_token}" }
@@ -136,7 +136,7 @@ class Morpheus::InstancesInterface < Morpheus::APIClient
136
136
  execute(opts)
137
137
  end
138
138
 
139
- def action(id, action_code, payload={})
139
+ def action(id, action_code, resource_pool_id=nil, payload={})
140
140
  url, params = "", {}
141
141
  if id.is_a?(Array)
142
142
  url = "#{@base_url}/api/instances/action"
@@ -145,6 +145,9 @@ class Morpheus::InstancesInterface < Morpheus::APIClient
145
145
  url = "#{@base_url}/api/instances/#{id}/action"
146
146
  params = {code: action_code}
147
147
  end
148
+ if resource_pool_id
149
+ params['selectedResourcePoolId'] = resource_pool_id
150
+ end
148
151
  headers = { :params => params, :authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
149
152
  opts = {method: :put, url: url, headers: headers, payload: payload.to_json}
150
153
  execute(opts)
@@ -6,4 +6,11 @@ class Morpheus::StorageVolumesInterface < Morpheus::RestInterface
6
6
  "/api/storage-volumes"
7
7
  end
8
8
 
9
+ def resize(id,payload)
10
+ url = "#{@base_url}/api/storage-volumes/#{id}/resize"
11
+ headers = { :params => {},:authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
12
+ opts = {method: :put, url: url, headers: headers, payload: payload.to_json}
13
+ execute(opts)
14
+ end
15
+
9
16
  end
@@ -244,10 +244,7 @@ class Morpheus::Cli::Clouds
244
244
  build_common_options(opts, options, [:options, :payload, :json, :dry_run, :remote])
245
245
  end
246
246
  optparse.parse!(args)
247
- # if args.count < 1
248
- # puts optparse
249
- # exit 1
250
- # end
247
+ verify_args!(args:args, optparse:optparse, max:1)
251
248
  connect(options)
252
249
 
253
250
  begin
@@ -258,10 +255,12 @@ class Morpheus::Cli::Clouds
258
255
  else
259
256
  cloud_payload = {name: args[0], description: params[:description]}
260
257
  cloud_payload.deep_merge!(parse_passed_options(options))
258
+ # Group
261
259
  # use active group by default
262
260
  params[:group] ||= @active_group_id
263
-
264
- # Group
261
+ if options[:options]['group']
262
+ params[:group] = options[:options]['group']
263
+ end
265
264
  group_id = nil
266
265
  group = params[:group] ? find_group_by_name_or_id_for_provisioning(params[:group]) : nil
267
266
  if group
@@ -274,22 +273,22 @@ class Morpheus::Cli::Clouds
274
273
  group_id = group_prompt['group']
275
274
  end
276
275
  cloud_payload['groupId'] = group_id
276
+ cloud_payload.delete('group')
277
277
  # todo: pass groups as an array instead
278
278
 
279
279
  # Cloud Name
280
280
  if args[0]
281
- cloud_payload[:name] = args[0]
281
+ cloud_payload['name'] = args[0]
282
282
  options[:options]['name'] = args[0] # to skip prompt
283
- elsif !options[:no_prompt]
284
- # name_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true}], options[:options])
285
- # cloud_payload[:name] = name_prompt['name']
286
283
  end
284
+ # name_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true}], options[:options])
285
+ # cloud_payload['name'] = name_prompt['name']
287
286
 
288
287
  # Cloud Type
289
288
  cloud_type = nil
290
289
  if params[:zone_type]
291
290
  cloud_type = cloud_type_for_name(params[:zone_type])
292
- elsif !options[:no_prompt]
291
+ else
293
292
  # print_red_alert "Cloud Type not found or specified!"
294
293
  # exit 1
295
294
  cloud_types_dropdown = cloud_types_for_dropdown
@@ -301,7 +300,8 @@ class Morpheus::Cli::Clouds
301
300
  print_red_alert "A cloud type is required."
302
301
  exit 1
303
302
  end
304
- cloud_payload[:zoneType] = {code: cloud_type['code']}
303
+ cloud_payload.delete('type')
304
+ cloud_payload['zoneType'] = {'code' => cloud_type['code']}
305
305
 
306
306
  cloud_payload['config'] ||= {}
307
307
  if params[:certificate_provider]
@@ -624,11 +624,13 @@ class Morpheus::Cli::Clusters
624
624
  end
625
625
 
626
626
  if controller_provision_type && resource_pool = prompt_resource_pool(group, cloud, service_plan, controller_provision_type, options)
627
- server_payload['config']['resourcePoolId'] = resource_pool['id']
628
- api_params['config'] ||= {}
629
- api_params['config']['resourcePool'] = resource_pool['id']
630
- api_params['resourcePoolId'] = resource_pool['id']
631
- api_params['zonePoolId'] = resource_pool['id']
627
+ if resource_pool
628
+ server_payload['config']['resourcePoolId'] = resource_pool['id']
629
+ api_params['config'] ||= {}
630
+ api_params['config']['resourcePool'] = resource_pool['id']
631
+ api_params['resourcePoolId'] = resource_pool['id']
632
+ api_params['zonePoolId'] = resource_pool['id']
633
+ end
632
634
  end
633
635
  end
634
636
 
@@ -746,6 +748,13 @@ class Morpheus::Cli::Clusters
746
748
  default_repo = options[:default_repo] || Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'defaultRepoAccount', 'fieldLabel' => 'Cluster Repo Account', 'type' => 'select', 'required' => false, 'optionSource' => 'dockerHubRegistries'}], options[:options], @api_client, api_params)['defaultRepoAccount']
747
749
  if default_repo != ""
748
750
  server_payload['config']['defaultRepoAccount'] = default_repo
751
+
752
+ # Act As Image Server
753
+ image_server = !options[:image_server].nil? ? options[:image_server] : Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'imageServer', 'fieldLabel' => 'Act As Image Server', 'type' => 'checkbox', 'required' => false, 'defaultValue' => 'on'}], options[:options], @api_client, api_params)['imageServer']
754
+ if image_server != ""
755
+ # api used to expect "on" so don't booleanize this one
756
+ server_payload['config']['imageServer'] = image_server ? 'on' : 'off'
757
+ end
749
758
  end
750
759
  end
751
760
 
@@ -761,7 +770,8 @@ class Morpheus::Cli::Clusters
761
770
 
762
771
  if loadbalancer_option_type
763
772
  lb_payload = { computeTypeLayoutId: cluster_payload['layout']['id']}
764
- load_balancer_id = prompt_cluster_load_balancer(cluster_payload, options)
773
+ # look to see if "can provision Kubevip" if so pass true as third param
774
+ load_balancer_id = prompt_cluster_load_balancer(cluster_payload, options, can_use_kubevip(provision_type["code"], cluster_payload["type"]))
765
775
  if load_balancer_id != false
766
776
  lb_payload['loadBalancerId'] = load_balancer_id
767
777
  lb_payload['loadBalancerInstanceId'] = -1
@@ -789,6 +799,24 @@ class Morpheus::Cli::Clusters
789
799
  cluster_payload['lbInstances'] = [load_balancer_payload.compact!]
790
800
  end
791
801
  end
802
+ # convert sshHosts to array
803
+ if server_payload['sshHosts']
804
+ if server_payload['sshHosts'].is_a?(String)
805
+ server_payload['sshHosts'] = server_payload['sshHosts'].split(",").collect {|it| {"ip" => it} }
806
+ elsif server_payload['sshHosts'].is_a?(Object)
807
+ server_payload['sshHosts'] = [server_payload['sshHosts']]
808
+ elsif server_payload['sshHosts'].is_a?(Array)
809
+ server_payload['sshHosts'] = server_payload['sshHosts'].collect {|it| it.is_a?(String) ? {"ip" => it} : it }
810
+ end
811
+
812
+ # inject the optionalNames array into the sshHosts if present
813
+ if server_payload['optionalNames']
814
+ optional_names = server_payload['optionalNames'].is_a?(String) ? server_payload['optionalNames'].split(",") : [server_payload['optionalNames']].flatten
815
+ server_payload['sshHosts'].each_with_index do|host, i|
816
+ host['name'] = optional_names[i].strip if optional_names[i]
817
+ end
818
+ end
819
+ end
792
820
 
793
821
  cluster_payload['server'] = server_payload
794
822
  payload = {'cluster' => cluster_payload}
@@ -4692,6 +4720,12 @@ class Morpheus::Cli::Clusters
4692
4720
  @clouds_interface.cloud_type(zone_type_id)['zoneType']['provisionTypes'].first rescue nil
4693
4721
  end
4694
4722
 
4723
+ def can_use_kubevip(type, group_type)
4724
+ payload = {'provisionType' => type, 'groupType' => group_type }
4725
+ can_use = @clusters_interface.can_use_kubevip(payload)
4726
+ return can_use
4727
+ end
4728
+
4695
4729
  def load_group(group_type, options)
4696
4730
  # Group / Site
4697
4731
  group_id = nil
@@ -4869,8 +4903,8 @@ class Morpheus::Cli::Clusters
4869
4903
  opts.on('--security-groups LIST', Array, "Security Groups") do |list|
4870
4904
  options[:securityGroups] = list
4871
4905
  end
4872
- opts.on("--create-user on|off", String, "User Config: Create Your User. Default is off") do |val|
4873
- options[:createUser] = ['true','on','1'].include?(val.to_s)
4906
+ opts.on("--create-user [on|off]", String, "User Config: Create Your User. Default is off") do |val|
4907
+ options[:createUser] = ['true','on','1',''].include?(val.to_s)
4874
4908
  end
4875
4909
  opts.on("--user-group USERGROUP", String, "User Config: User Group") do |val|
4876
4910
  options[:userGroup] = val
@@ -4881,6 +4915,12 @@ class Morpheus::Cli::Clusters
4881
4915
  opts.on('--hostname VALUE', String, "Hostname") do |val|
4882
4916
  options[:hostname] = val
4883
4917
  end
4918
+ opts.on('--default-repo-account VALUE', String, "Default Repo Account") do |val|
4919
+ options[:default_repo] = val
4920
+ end
4921
+ opts.on("--image-server [on|off]", ['on','off'], "Act As Image Server") do |val|
4922
+ options[:image_server] = ['true','on','1',''].include?(val.to_s)
4923
+ end
4884
4924
  end
4885
4925
 
4886
4926
  def add_datastore_option_types
@@ -4903,15 +4943,17 @@ class Morpheus::Cli::Clusters
4903
4943
  resource_pool_options = @options_interface.options_for_source('zonePools', {groupId: group['id'], zoneId: cloud['id']}.merge(service_plan ? {planId: service_plan['id']} : {}))['data'].reject { |it| it['id'].nil? && it['name'].nil? }
4904
4944
 
4905
4945
  if resource_pool_options.empty?
4906
- print_red_alert "Cloud #{cloud['name']} has no available resource pools"
4907
- exit 1
4946
+ print yellow,bold, "Cloud #{cloud['name']} has no available resource pools",reset,"\n\n"
4947
+ return
4948
+ #exit 1
4908
4949
  elsif resource_pool_options.count > 1 && !options[:no_prompt]
4909
4950
  resource_pool_id = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'resourcePool', 'type' => 'select', 'fieldLabel' => 'Resource Pool', 'selectOptions' => resource_pool_options, 'required' => true, 'skipSingleOption' => true, 'description' => 'Select resource pool.'}],options[:options],api_client, {})['resourcePool']
4910
4951
  else
4911
4952
  first_option = resource_pool_options.find {|it| !it['id'].nil? }
4912
4953
  if first_option.nil?
4913
- print_red_alert "Cloud #{cloud['name']} has no available resource pools"
4914
- exit 1
4954
+ print yellow,bold, "Cloud #{cloud['name']} has no available resource pools",reset,"\n\n"
4955
+ return
4956
+ #exit 1
4915
4957
  end
4916
4958
  resource_pool_id = first_option['id']
4917
4959
  end
@@ -256,8 +256,8 @@ class Morpheus::Cli::Hosts
256
256
  rows = servers.collect {|server|
257
257
  stats = server['stats']
258
258
 
259
- if !stats['maxMemory']
260
- stats['maxMemory'] = stats['usedMemory'] + stats['freeMemory']
259
+ if !stats['maxMemory'] && (stats['usedMemory'] || stats['freeMemory'])
260
+ stats['maxMemory'] = stats['usedMemory'].to_i + stats['freeMemory'].to_i
261
261
  end
262
262
  cpu_usage_str = !stats ? "" : generate_usage_bar((stats['usedCpu'] || stats['cpuUsage']).to_f, 100, {max_bars: 10})
263
263
  memory_usage_str = !stats ? "" : generate_usage_bar(stats['usedMemory'], stats['maxMemory'], {max_bars: 10})
@@ -849,23 +849,30 @@ class Morpheus::Cli::Hosts
849
849
  end
850
850
 
851
851
  payload = {}
852
- # prompt for service plan
853
- service_plans_json = @servers_interface.service_plans({zoneId: cloud['id'], serverTypeId: server_type["id"]})
854
- service_plans = service_plans_json["plans"]
855
- service_plans_dropdown = service_plans.collect {|sp| {'name' => sp["name"], 'value' => sp["id"]} } # already sorted
856
- plan_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'plan', 'type' => 'select', 'fieldLabel' => 'Plan', 'selectOptions' => service_plans_dropdown, 'required' => true, 'description' => 'Choose the appropriately sized plan for this server'}],options[:options])
857
- service_plan = service_plans.find {|sp| sp["id"] == plan_prompt['plan'].to_i }
858
-
859
- # uh ok, this actually expects config at root level, sibling of server
860
- # payload.deep_merge!({'server' => passed_options}) unless passed_options.empty?
861
852
  payload.deep_merge!(passed_options) unless passed_options.empty?
862
853
  payload.deep_merge!({'server' => {
863
854
  'name' => host_name,
864
855
  'zone' => {'id' => cloud['id']},
865
856
  'computeServerType' => {'id' => server_type['id']},
866
- 'plan' => {'id' => service_plan["id"]}
867
- }
868
- })
857
+ }})
858
+
859
+ is_baremetal_host = server_type['bareMetalHost']
860
+
861
+ # Service plans do not apply during BareMetal server imports
862
+ has_service_plans = !is_baremetal_host
863
+
864
+ # uh ok, this actually expects config at root level, sibling of server
865
+ # payload.deep_merge!({'server' => passed_options}) unless passed_options.empty?
866
+ # prompt for service plan
867
+ service_plan = nil
868
+ if has_service_plans
869
+ service_plans_json = @servers_interface.service_plans({ zoneId: cloud['id'], serverTypeId: server_type["id"] })
870
+ service_plans = service_plans_json["plans"]
871
+ service_plans_dropdown = service_plans.collect { |sp| { 'name' => sp["name"], 'value' => sp["id"] } } # already sorted
872
+ plan_prompt = Morpheus::Cli::OptionTypes.prompt([{ 'fieldName' => 'plan', 'type' => 'select', 'fieldLabel' => 'Plan', 'selectOptions' => service_plans_dropdown, 'required' => true, 'description' => 'Choose the appropriately sized plan for this server' }], options[:options])
873
+ service_plan = service_plans.find { |sp| sp["id"] == plan_prompt['plan'].to_i }
874
+ payload['server']['plan'] = {'id' => service_plan['id']}
875
+ end
869
876
 
870
877
  option_type_list = server_type['optionTypes']
871
878
 
@@ -893,7 +900,7 @@ class Morpheus::Cli::Hosts
893
900
  resource_pool_option_type = option_type_list.find {|opt| ['resourcePool','resourcePoolId','azureResourceGroupId'].include?(opt['fieldName']) }
894
901
  option_type_list = option_type_list.reject {|opt| ['resourcePool','resourcePoolId','azureResourceGroupId'].include?(opt['fieldName']) }
895
902
  resource_pool_option_type ||= {'fieldContext' => 'config', 'fieldName' => 'resourcePool', 'type' => 'select', 'fieldLabel' => 'Resource Pool', 'optionSource' => 'zonePools', 'required' => true, 'skipSingleOption' => true, 'description' => 'Select resource pool.'}
896
- resource_pool_prompt = Morpheus::Cli::OptionTypes.prompt([resource_pool_option_type],options[:options],api_client,{groupId: group_id, siteId: group_id, zoneId: cloud_id, cloudId: cloud_id, planId: service_plan["id"], serverTypeId: server_type['id']})
903
+ resource_pool_prompt = Morpheus::Cli::OptionTypes.prompt([resource_pool_option_type],options[:options],api_client,{groupId: group_id, siteId: group_id, zoneId: cloud_id, cloudId: cloud_id, planId: service_plan ? service_plan["id"] : "", serverTypeId: server_type['id']})
897
904
  resource_pool_prompt.deep_compact!
898
905
  payload.deep_merge!(resource_pool_prompt)
899
906
  if resource_pool_option_type['fieldContext'] && resource_pool_prompt[resource_pool_option_type['fieldContext']]
@@ -903,20 +910,23 @@ class Morpheus::Cli::Hosts
903
910
  end
904
911
  end
905
912
 
906
- # prompt for volumes
907
- volumes = prompt_volumes(service_plan, provision_type, options, @api_client, {zoneId: cloud_id, serverTypeId: server_type['id'], siteId: group_id})
908
- if !volumes.empty?
909
- payload['volumes'] = volumes
910
- end
913
+ if has_service_plans
914
+ # prompt for volumes
915
+ volumes = prompt_volumes(service_plan, provision_type, options, @api_client, {zoneId: cloud_id, serverTypeId: server_type['id'], siteId: group_id})
916
+ if !volumes.empty?
917
+ payload['volumes'] = volumes
918
+ end
911
919
 
912
- # plan customizations
913
- plan_opts = prompt_service_plan_options(service_plan, options, @api_client, {})
914
- if plan_opts && !plan_opts.empty?
915
- payload['servicePlanOptions'] = plan_opts
920
+ # plan customizations
921
+ plan_opts = prompt_service_plan_options(service_plan, options, @api_client, {})
922
+ if plan_opts && !plan_opts.empty?
923
+ payload['servicePlanOptions'] = plan_opts
924
+ end
916
925
  end
917
926
 
918
927
  # prompt for network interfaces (if supported)
919
- if server_type["provisionType"] && server_type["provisionType"]["id"] && server_type["provisionType"]["hasNetworks"]
928
+ # Does not apply during BareMetal server imports
929
+ if !is_baremetal_host && server_type["provisionType"] && server_type["provisionType"]["id"] && server_type["provisionType"]["hasNetworks"]
920
930
  begin
921
931
  network_interfaces = prompt_network_interfaces(cloud['id'], server_type["provisionType"]["id"], pool_id, options)
922
932
  if !network_interfaces.empty?
@@ -971,6 +981,7 @@ class Morpheus::Cli::Hosts
971
981
  end
972
982
  #api_params.deep_merge(payload)
973
983
  params = Morpheus::Cli::OptionTypes.prompt(option_type_list,options[:options],@api_client, api_params)
984
+ params.booleanize!
974
985
  payload.deep_merge!(params)
975
986
 
976
987
  end
@@ -1054,6 +1065,7 @@ class Morpheus::Cli::Hosts
1054
1065
  new_group = nil
1055
1066
  passed_options = options[:options] ? options[:options].reject {|k,v| k.is_a?(Symbol) } : {}
1056
1067
  params.deep_merge!(passed_options) unless passed_options.empty?
1068
+ params.booleanize!
1057
1069
  # metadata tags
1058
1070
  if options[:tags]
1059
1071
  params['tags'] = parse_metadata(options[:tags])
@@ -1405,6 +1417,79 @@ class Morpheus::Cli::Hosts
1405
1417
  end
1406
1418
  end
1407
1419
 
1420
+ # Prompt for instance version and layout based on instance type.
1421
+ def prompt_make_managed_instance_options(instance_type, group_id, cloud_id, options={})
1422
+ opt_bucket = options[:options] || {}
1423
+
1424
+ # available versions for this instance type
1425
+ version_source_params = {groupId: group_id, cloudId: cloud_id, instanceTypeId: instance_type['id']}
1426
+ available_versions = options_interface.options_for_source('instanceVersions', version_source_params)['data'] || []
1427
+ # filter out versions that have no layouts
1428
+ available_versions.reject! { |ver| ver['layouts'].nil? || ver['layouts'].empty? }
1429
+ available_versions.sort! { |x,y| x['name']<=> y['name'] }
1430
+
1431
+ # validate version option
1432
+ if opt_bucket['version']
1433
+ selected_version = opt_bucket['version']
1434
+ unless available_versions.find {|av| av['name'].to_s == selected_version.to_s }
1435
+ raise_command_error "Invalid version option '#{selected_version}' passed. Valid versions are: #{available_versions.collect {|av| av['name']}.join(', ')}"
1436
+ end
1437
+ end
1438
+ # prompt for version
1439
+ version_prompt = Morpheus::Cli::OptionTypes.prompt([{
1440
+ 'fieldName' => 'version',
1441
+ 'type' => 'select',
1442
+ 'fieldLabel' => 'Version',
1443
+ 'selectOptions' => available_versions,
1444
+ 'required' => true,
1445
+ 'defaultValue' => available_versions.first ? available_versions.first['value'] : '',
1446
+ 'description' => 'Instance Type Version'
1447
+ }],
1448
+ opt_bucket
1449
+ )
1450
+ selected_version_value = version_prompt['version']
1451
+
1452
+ # filter layouts that support the selected version and are unmanaged or support convert to managed
1453
+ layouts_for_version = (instance_type['instanceTypeLayouts'] || []).select { |layout|
1454
+ (layout['instanceVersion'] == selected_version_value) &&
1455
+ (layout['supportsConvertToManaged'] == true || layout['serverType'] == 'unmanaged')
1456
+ }
1457
+
1458
+ if layouts_for_version.empty?
1459
+ print_red_alert "No available layouts that support converting to managed for the given instance type"
1460
+ return nil
1461
+ end
1462
+
1463
+ layout_options = layouts_for_version.collect { |layout|
1464
+ {'name' => layout['name'], 'value' => layout['id']}
1465
+ }
1466
+
1467
+ # validate layout input option
1468
+ if opt_bucket['layout']
1469
+ default_layout = opt_bucket['layout']
1470
+ unless layout_options.find {|lo| lo['name'].to_s == default_layout.to_s }
1471
+ raise_command_error "Invalid layout option '#{default_layout}' passed. Valid layouts are: #{layout_options.collect {|lo| lo['name']}.join(', ')}"
1472
+ end
1473
+ end
1474
+
1475
+ layout_prompt = Morpheus::Cli::OptionTypes.prompt(
1476
+ [{
1477
+ 'fieldName' => 'layout',
1478
+ 'type' => 'select',
1479
+ 'fieldLabel' => 'Layout',
1480
+ 'selectOptions' => layout_options,
1481
+ 'required' => true,
1482
+ 'defaultValue' => layout_options.first ? layout_options.first['value'] : '',
1483
+ 'description' => 'Choose a layout (template) for this instance.'
1484
+ }],
1485
+ opt_bucket
1486
+ )
1487
+ chosen_layout_id = layout_prompt['layout']
1488
+
1489
+ {'instanceVersion' => selected_version_value, 'instanceLayoutId' => chosen_layout_id}
1490
+ end
1491
+ private :prompt_make_managed_instance_options
1492
+
1408
1493
  def make_managed(args)
1409
1494
  options = {}
1410
1495
  optparse = Morpheus::Cli::OptionParser.new do |opts|
@@ -1416,9 +1501,9 @@ class Morpheus::Cli::Hosts
1416
1501
  opts.on('-g', '--group GROUP', String, "Group to assign to new instance.") do |val|
1417
1502
  options[:group] = val
1418
1503
  end
1419
- # opts.on('--instance-type-id ID', String, "Instance Type ID for the new instance.") do |val|
1420
- # options['instanceTypeId'] = val.to_s == 'on' || val.to_s == 'true' || val.to_s == ''
1421
- # end
1504
+ opts.on('--instance-type ID', String, "Instance Type ID or Name for the new instance. Optional - only specify if you want to set a specific instance type for conversion.") do |val|
1505
+ options['instanceType'] = val
1506
+ end
1422
1507
  build_common_options(opts, options, [:options, :json, :dry_run, :quiet, :remote])
1423
1508
  end
1424
1509
  optparse.parse!(args)
@@ -1453,11 +1538,33 @@ class Morpheus::Cli::Hosts
1453
1538
  params['provisionSiteId'] = group['id']
1454
1539
  end
1455
1540
  payload['server'].merge!(params)
1456
- ['installAgent','instanceTypeId'].each do |k|
1541
+ ['installAgent'].each do |k|
1457
1542
  if options[k] != nil
1458
1543
  payload[k] = options[k]
1459
1544
  end
1460
1545
  end
1546
+ # If instance type is passed, prompt for version and layout
1547
+ unless options['instanceType'].nil?
1548
+ instance_type = find_instance_type_by_name_or_id(options['instanceType'])
1549
+ if instance_type.nil?
1550
+ print_red_alert "Instance Type not found by name or id #{options['instanceType']}"
1551
+ return 1
1552
+ end
1553
+ if instance_type['active'] != true
1554
+ print_red_alert "Instance Type '#{options['instanceType']}' (#{instance_type['name']}) is not active"
1555
+ return 1
1556
+ end
1557
+ resp=prompt_make_managed_instance_options(instance_type, params['provisionSiteId'] || host['siteId'],
1558
+ (host['zoneId'] || host['zone']['id']), options)
1559
+ if resp.nil?
1560
+ print_red_alert "Failed to select instance version and layout for given instance type '#{instance_type['name']}'"
1561
+ return 1
1562
+ end
1563
+ payload['instanceTypeId'] = instance_type['id']
1564
+ payload['layout'] = resp['instanceLayoutId']
1565
+ payload['version'] = resp['instanceVersion']
1566
+ end
1567
+
1461
1568
  @servers_interface.setopts(options)
1462
1569
  if options[:dry_run]
1463
1570
  print_dry_run(@servers_interface.dry.make_managed(host['id'], payload), options)
@@ -53,7 +53,9 @@ class Morpheus::Cli::Instances
53
53
  @library_layouts_interface = @api_client.library_layouts
54
54
  @clouds_interface = @api_client.clouds
55
55
  @clouds_datastores_interface = @api_client.cloud_datastores
56
+ @cloud_resource_pools_interface = @api_client.cloud_resource_pools
56
57
  @servers_interface = @api_client.servers
58
+ @server_types_interface = @api_client.server_types
57
59
  @provision_types_interface = @api_client.provision_types
58
60
  @options_interface = @api_client.options
59
61
  @active_group_id = Morpheus::Cli::Groups.active_groups[@appliance_name]
@@ -2655,6 +2657,84 @@ class Morpheus::Cli::Instances
2655
2657
  end
2656
2658
  end
2657
2659
 
2660
+ def find_server_type_by_id(id)
2661
+ begin
2662
+ json_response =@server_types_interface.get(id.to_i)
2663
+ return json_response['serverType']
2664
+ rescue RestClient::Exception => e
2665
+ if e.response && e.response.code == 404
2666
+ print_red_alert "Server Type not found by id #{id}"
2667
+ return nil
2668
+ else
2669
+ raise e
2670
+ end
2671
+ end
2672
+ end
2673
+ private :find_server_type_by_id
2674
+
2675
+ # checks whether adding a node to the specified instance requires a resource pool input
2676
+ def addnode_requires_resource_pool?(instance, provision_type)
2677
+ return false unless provision_type && provision_type["hasZonePools"] && provision_type["zonePoolRequired"] && instance['servers']
2678
+ instance['servers'].each do |server_id|
2679
+ server = find_server_by_id(server_id)
2680
+ next unless server
2681
+ cst_id = server.dig('computeServerType', 'id')
2682
+ cst = find_server_type_by_id(cst_id)
2683
+ unless cst
2684
+ print_red_alert "Server Type not found by id #{cst_id}"
2685
+ return false
2686
+ end
2687
+ return true if cst['bareMetalHost']
2688
+ end
2689
+ false
2690
+ end
2691
+ private :addnode_requires_resource_pool?
2692
+
2693
+ # Prompts for a resource pool selection when adding a node to an instance.
2694
+ # This is invoked during the Add Node action to choose a resource pool (eg. for HPE bare metal host provisioning types mandates a pool).
2695
+ def addnode_prompt_for_resource_pool(instance, options)
2696
+ zone_id = instance['cloud']['id']
2697
+ if zone_id.nil?
2698
+ print_red_alert "Instance #{instance['name']} does not have a cloud/zone assigned."
2699
+ return nil
2700
+ end
2701
+ zone_pools = @cloud_resource_pools_interface.list(zone_id)['resourcePools']
2702
+ if zone_pools.empty?
2703
+ print_red_alert "No Resource Pools are defined for the instance's cloud #{instance['cloud']['name']}. This is required to perform this action."
2704
+ return nil
2705
+ end
2706
+ zone_pools_dropdown = zone_pools.collect {|zp| {'name' => zp['name'], 'value' => zp['id']} }
2707
+ # validate input if given
2708
+ if options[:options] && options[:options]['resourcePool']
2709
+ selected_resource_pool_id = options[:options]['resourcePool'].to_i
2710
+ selected_pool = zone_pools.find {|zp| zp['id'] == selected_resource_pool_id }
2711
+ if selected_pool.nil?
2712
+ print_red_alert "Resource Pool ID #{selected_resource_pool_id} is not valid for the instance's cloud."
2713
+ return nil
2714
+ end
2715
+ end
2716
+ # auto select single
2717
+ pool_prompt = Morpheus::Cli::OptionTypes.prompt(
2718
+ [{
2719
+ 'fieldName' => 'resourcePool',
2720
+ 'type' => 'select',
2721
+ 'fieldLabel' => 'Resource Pool',
2722
+ 'selectOptions' => zone_pools_dropdown,
2723
+ 'required' => true,
2724
+ 'defaultValue' => zone_pools[0]['id'],
2725
+ 'description' => 'Choose the Resource Pool to use for this action'
2726
+ }],
2727
+ options[:options]
2728
+ )
2729
+ selected_pool = zone_pools.find {|zp| zp['id'] == pool_prompt['resourcePool'] }
2730
+ if selected_pool.nil?
2731
+ print_red_alert "A Resource Pool is required to perform this action."
2732
+ return nil
2733
+ end
2734
+ selected_resource_pool_id = selected_pool['id']
2735
+ end
2736
+ private :addnode_prompt_for_resource_pool
2737
+
2658
2738
  def action(args)
2659
2739
  options = {}
2660
2740
  action_id = nil
@@ -2663,7 +2743,11 @@ class Morpheus::Cli::Instances
2663
2743
  opts.on('-a', '--action CODE', "Instance Action CODE to execute") do |val|
2664
2744
  action_id = val.to_s
2665
2745
  end
2666
- build_common_options(opts, options, [:auto_confirm, :json, :dry_run, :quiet, :remote])
2746
+ opts.on( '--resource-pool ID', String, "Resource pool ID for Add node action" ) do |val|
2747
+ options[:options] ||= {}
2748
+ options[:options]['resourcePool'] = val
2749
+ end
2750
+ build_common_options(opts, options, [:auto_confirm, :json, :dry_run, :quiet, :remote,])
2667
2751
  opts.footer = "Execute an action for one or many instances."
2668
2752
  end
2669
2753
  optparse.parse!(args)
@@ -2716,20 +2800,46 @@ class Morpheus::Cli::Instances
2716
2800
  raise_command_error "Instance Action '#{action_id}' not found."
2717
2801
  end
2718
2802
 
2719
- action_display_name = "#{instance_action['name']} [#{instance_action['code']}]"
2803
+ action_display_name = "#{instance_action['name']} [#{instance_action['code']}]"
2804
+ if action_id == 'generic-add-node' && instances.size > 1
2805
+ raise_command_error "The #{action_display_name} action can only be performed on a single instance at a time."
2806
+ end
2807
+
2720
2808
  unless options[:yes] || ::Morpheus::Cli::OptionTypes::confirm("Are you sure you would like to perform action #{action_display_name} on #{id_list.size == 1 ? 'instance' : 'instances'} #{anded_list(id_list)}?", options)
2721
2809
  return 9, "aborted command"
2722
2810
  end
2723
2811
 
2812
+ selected_resource_pool_id = nil
2813
+ # Special handling for generic-add-node action to prompt for resource pool if needed.
2814
+ if action_id == 'generic-add-node'
2815
+ instance = instances[0]
2816
+ layout = instance['layout']
2817
+ provision_type_code = layout['provisionTypeCode']
2818
+ provision_type = nil
2819
+ if provision_type_code
2820
+ provision_type = provision_types_interface.list({code:provision_type_code})['provisionTypes'][0]
2821
+ if provision_type.nil?
2822
+ print_red_alert "Provision Type not found by code #{provision_type_code}"
2823
+ return 1
2824
+ end
2825
+ end
2826
+
2827
+ # Check if instance containers are of 'baremetalhost' type, if so, then only prompt for resource pool.
2828
+ pool_required = addnode_requires_resource_pool?(instance, provision_type)
2829
+ if pool_required
2830
+ selected_resource_pool_id = addnode_prompt_for_resource_pool(instance, options )
2831
+ return 1 if selected_resource_pool_id.nil?
2832
+ end
2833
+ end # End of generic-add-node special handling
2724
2834
  # return run_command_for_each_arg(containers) do |arg|
2725
2835
  # _action(arg, action_id, options)
2726
2836
  # end
2727
2837
  @instances_interface.setopts(options)
2728
2838
  if options[:dry_run]
2729
- print_dry_run @instances_interface.dry.action(instance_ids, action_id)
2839
+ print_dry_run @instances_interface.dry.action(instance_ids, action_id, selected_resource_pool_id)
2730
2840
  return 0
2731
2841
  end
2732
- json_response = @instances_interface.action(instance_ids, action_id)
2842
+ json_response = @instances_interface.action(instance_ids, action_id, selected_resource_pool_id)
2733
2843
  # just assume json_response["success"] == true, it always is with 200 OK
2734
2844
  if options[:json]
2735
2845
  puts as_json(json_response, options)
@@ -2937,7 +3047,7 @@ class Morpheus::Cli::Instances
2937
3047
  opts.footer = <<-EOT
2938
3048
  Create a snapshot for an instance.
2939
3049
  [instance] is required. This is the name or id of an instance
2940
- EOT
3050
+ EOT
2941
3051
  end
2942
3052
  optparse.parse!(args)
2943
3053
  verify_args!(args:args, optparse:optparse, count:1)
@@ -3067,7 +3177,7 @@ EOT
3067
3177
  Cancel removal of an instance.
3068
3178
  This is a way to undo delete of an instance still pending removal.
3069
3179
  [instance] is required. This is the name or id of an instance
3070
- EOT
3180
+ EOT
3071
3181
  end
3072
3182
  optparse.parse!(args)
3073
3183
  verify_args!(args:args, optparse:optparse, count:1)
@@ -3098,7 +3208,7 @@ EOT
3098
3208
  opts.footer = <<-EOT
3099
3209
  Cancel expiration of an instance.
3100
3210
  [instance] is required. This is the name or id of an instance
3101
- EOT
3211
+ EOT
3102
3212
  end
3103
3213
  optparse.parse!(args)
3104
3214
  verify_args!(args:args, optparse:optparse, count:1)
@@ -3129,7 +3239,7 @@ EOT
3129
3239
  opts.footer = <<-EOT
3130
3240
  Cancel shutdown for an instance.
3131
3241
  [instance] is required. This is the name or id of an instance
3132
- EOT
3242
+ EOT
3133
3243
  end
3134
3244
  optparse.parse!(args)
3135
3245
  verify_args!(args:args, optparse:optparse, count:1)
@@ -3160,7 +3270,7 @@ EOT
3160
3270
  opts.footer = <<-EOT
3161
3271
  Extend expiration for an instance.
3162
3272
  [instance] is required. This is the name or id of an instance
3163
- EOT
3273
+ EOT
3164
3274
  end
3165
3275
  optparse.parse!(args)
3166
3276
  verify_args!(args:args, optparse:optparse, count:1)
@@ -3191,7 +3301,7 @@ EOT
3191
3301
  opts.footer = <<-EOT
3192
3302
  Extend shutdown for an instance.
3193
3303
  [instance] is required. This is the name or id of an instance
3194
- EOT
3304
+ EOT
3195
3305
  end
3196
3306
  optparse.parse!(args)
3197
3307
  verify_args!(args:args, optparse:optparse, count:1)
@@ -3437,7 +3547,7 @@ Run workflow for an instance.
3437
3547
  By default the provision phase is executed.
3438
3548
  Use the --phase option to execute a different phase.
3439
3549
  The available phases are start, stop, preProvision, provision, postProvision, preDeploy, deploy, reconfigure, teardown, startup and shutdown.
3440
- EOT
3550
+ EOT
3441
3551
  end
3442
3552
  optparse.parse!(args)
3443
3553
  if args.count != 2
@@ -3505,7 +3615,7 @@ EOT
3505
3615
  List snapshots for an instance.
3506
3616
  [instance] is required. This is the name or id of an instance
3507
3617
  [snapshot] is optional. this is the name or id a snapshot to filter by.
3508
- EOT
3618
+ EOT
3509
3619
  end
3510
3620
  optparse.parse!(args)
3511
3621
  verify_args!(args:args, optparse:optparse, min:1, max: 2)
@@ -4001,7 +4111,7 @@ EOT
4001
4111
  opts.footer = <<-EOT
4002
4112
  List instance scaling threshold schedules.
4003
4113
  [instance] is required. This is the name or id of an instance
4004
- EOT
4114
+ EOT
4005
4115
  end
4006
4116
  optparse.parse!(args)
4007
4117
  verify_args!(args:args, optparse:optparse, count:1)
@@ -4045,7 +4155,7 @@ EOT
4045
4155
  Get details about an instance scaling threshold schedule.
4046
4156
  [instance] is required. This is the name or id of an instance
4047
4157
  [schedule] is required. This is id of an instance schedule
4048
- EOT
4158
+ EOT
4049
4159
  end
4050
4160
  optparse.parse!(args)
4051
4161
  verify_args!(args:args, optparse:optparse, count:2)
@@ -4084,7 +4194,7 @@ EOT
4084
4194
  opts.footer = <<-EOT
4085
4195
  Update an existing instance scaling threshold schedule
4086
4196
  [instance] is required. This is the name or id of an instance
4087
- EOT
4197
+ EOT
4088
4198
  end
4089
4199
  optparse.parse!(args)
4090
4200
  verify_args!(args:args, optparse:optparse, count:1)
@@ -4145,7 +4255,7 @@ EOT
4145
4255
  Update an existing instance scaling threshold schedule.
4146
4256
  [instance] is required. This is the name or id of an instance
4147
4257
  [schedule] is required. This is id of an instance schedule
4148
- EOT
4258
+ EOT
4149
4259
  end
4150
4260
  optparse.parse!(args)
4151
4261
  verify_args!(args:args, optparse:optparse, count:2)
@@ -4205,7 +4315,7 @@ EOT
4205
4315
  Delete an existing instance scaling threshold schedule
4206
4316
  [instance] is required. This is the name or id of an instance
4207
4317
  [schedule] is required. This is id of an instance schedule
4208
- EOT
4318
+ EOT
4209
4319
  end
4210
4320
  optparse.parse!(args)
4211
4321
  verify_args!(args:args, optparse:optparse, count:2)
@@ -4769,7 +4879,7 @@ EOT
4769
4879
  List deployments for an instance.
4770
4880
  [instance] is required. This is the name or id of an instance
4771
4881
  [search] is optional. Filters on deployment version identifier
4772
- EOT
4882
+ EOT
4773
4883
  end
4774
4884
  optparse.parse!(args)
4775
4885
  verify_args!(args:args, optparse:optparse, min:1)
@@ -4827,7 +4937,7 @@ EOT
4827
4937
  opts.footer = <<-EOT
4828
4938
  Clone to image (template) for an instance
4829
4939
  [instance] is required. This is the name or id of an instance
4830
- EOT
4940
+ EOT
4831
4941
  end
4832
4942
  optparse.parse!(args)
4833
4943
  verify_args!(args:args, optparse:optparse, count:1)
@@ -4899,7 +5009,7 @@ EOT
4899
5009
  opts.footer = <<-EOT
4900
5010
  Lock an instance
4901
5011
  [instance] is required. This is the name or id of an instance
4902
- EOT
5012
+ EOT
4903
5013
  end
4904
5014
  optparse.parse!(args)
4905
5015
  verify_args!(args:args, optparse:optparse, count:1)
@@ -4933,7 +5043,7 @@ EOT
4933
5043
  opts.footer = <<-EOT
4934
5044
  Unlock an instance
4935
5045
  [instance] is required. This is the name or id of an instance
4936
- EOT
5046
+ EOT
4937
5047
  end
4938
5048
  optparse.parse!(args)
4939
5049
  verify_args!(args:args, optparse:optparse, count:1)
@@ -4968,7 +5078,7 @@ EOT
4968
5078
  Refresh an instance.
4969
5079
  [instance] is required. This is the name or id of an instance.
4970
5080
  This is only supported by certain types of instances such as terraform.
4971
- EOT
5081
+ EOT
4972
5082
  end
4973
5083
  optparse.parse!(args)
4974
5084
  verify_args!(args:args, optparse:optparse, count:1)
@@ -5017,7 +5127,7 @@ Prepare to apply an instance.
5017
5127
  [instance] is required. This is the name or id of an instance.
5018
5128
  Displays the current configuration data used by the apply command.
5019
5129
  This is only supported by certain types of instances such as terraform.
5020
- EOT
5130
+ EOT
5021
5131
  end
5022
5132
  optparse.parse!(args)
5023
5133
  if args.count != 1
@@ -5090,7 +5200,7 @@ This is only supported by certain types of instances such as terraform.
5090
5200
  By default this executes two requests to validate and then apply the changes.
5091
5201
  The first request corresponds to the terraform plan command only.
5092
5202
  Use --no-validate to skip this step apply changes in one step.
5093
- EOT
5203
+ EOT
5094
5204
  end
5095
5205
  optparse.parse!(args)
5096
5206
  verify_args!(args:args, optparse:optparse, count:1)
@@ -5240,7 +5350,7 @@ EOT
5240
5350
  View state of an instance.
5241
5351
  [instance] is required. This is the name or id of an instance.
5242
5352
  This is only supported by certain types of apps such as terraform.
5243
- EOT
5353
+ EOT
5244
5354
  end
5245
5355
  optparse.parse!(args)
5246
5356
  verify_args!(args:args, optparse:optparse, count:1)
@@ -7,7 +7,7 @@ class Morpheus::Cli::StorageVolumes
7
7
 
8
8
  set_command_name :'storage-volumes'
9
9
  set_command_description "View and manage storage volumes."
10
- register_subcommands %w{list get add remove}
10
+ register_subcommands :list, :get, :add, :remove, :resize
11
11
 
12
12
  # RestCommand settings
13
13
  register_interfaces :storage_volumes, :storage_volume_types
@@ -80,6 +80,12 @@ class Morpheus::Cli::StorageVolumes
80
80
  ]
81
81
  end
82
82
 
83
+ def resize_storage_volume_option_types()
84
+ [
85
+ {'fieldName' => 'maxStorage', 'fieldLabel' => 'New Size', 'type' => 'number', 'required' => true},
86
+ ]
87
+ end
88
+
83
89
  def load_option_types_for_storage_volume(type_record, parent_record)
84
90
  storage_volume_type = type_record
85
91
  option_types = storage_volume_type['optionTypes']
@@ -94,4 +100,62 @@ class Morpheus::Cli::StorageVolumes
94
100
  return option_types
95
101
  end
96
102
 
103
+ def resize(args)
104
+ options = {}
105
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
106
+ opts.banner = subcommand_usage("[name]")
107
+ build_common_options(opts, options, [:options, :json, :dry_run, :quiet, :remote])
108
+ end
109
+ optparse.parse!(args)
110
+ if args.count < 1
111
+ puts optparse
112
+ exit 1
113
+ end
114
+ connect(options)
115
+ begin
116
+ volume = find_volume_by_name_or_id(args[0])
117
+ payload = {}
118
+ id = volume['id'].to_i
119
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'size', 'type' => 'number', 'fieldLabel' => "Volume Size (bytes)", 'required' => true, 'description' => 'Enter a volume size (bytes).', 'defaultValue' => volume['maxStorage']}], options[:options])
120
+ payload['maxStorage'] = v_prompt['size'].to_i
121
+ @storage_volumes_interface.resize(id, payload)
122
+ end
123
+ end
124
+
125
+ def find_volume_by_id(id)
126
+ begin
127
+ json_response = @storage_volumes_interface.get(id.to_i)
128
+ return json_response['storageVolume']
129
+ rescue RestClient::Exception => e
130
+ if e.response && e.response.code == 404
131
+ print_red_alert "Volume not found by id #{id}"
132
+ exit 1
133
+ else
134
+ raise e
135
+ end
136
+ end
137
+ end
138
+
139
+ def find_volume_by_name(name)
140
+ results = @storage_volumes_interface.list({name: name})
141
+ if results['storageVolumes'].empty?
142
+ print_red_alert "Volume not found by name #{name}"
143
+ exit 1
144
+ elsif results['storageVolumes'].size > 1
145
+ print_red_alert "Multiple Volumes exist with the name '#{name}'"
146
+ puts_error as_pretty_table(results['storageVolumes'], [:id, :name], {color:red})
147
+ print_red_alert "Try using ID instead"
148
+ exit 1
149
+ end
150
+ return results['storageVolumes'][0]
151
+ end
152
+
153
+ def find_volume_by_name_or_id(val)
154
+ if val.to_s =~ /\A\d{1,}\Z/
155
+ return find_volume_by_id(val)
156
+ else
157
+ return find_volume_by_name(val)
158
+ end
159
+ end
160
+
97
161
  end
@@ -192,6 +192,11 @@ class Morpheus::Cli::ErrorHandler
192
192
  @stderr.print reset
193
193
  end
194
194
  else
195
+ # quick parse only for msg display
196
+ response = (JSON.parse(e.response.to_s) rescue nil)
197
+ if response.is_a?(Hash) && response['success'] == false && response['msg']
198
+ @stderr.print red, response['msg'].to_s, reset, "\n"
199
+ end
195
200
  @stderr.puts "Use -V or --debug for more verbose debugging information."
196
201
  end
197
202
  end
@@ -28,9 +28,15 @@ module Morpheus::Cli::ProcessesHelper
28
28
  "Start Date" => lambda {|it| format_local_dt(it['startDate']) },
29
29
  "End Date" => lambda {|it| format_local_dt(it['endDate']) },
30
30
  "Duration" => lambda {|it| format_process_duration(it) },
31
+ }
32
+ if process['message'].to_s.strip != ''
33
+ description_cols.merge!({ "Message" => lambda {|it| it['message']}
34
+ })
35
+ end
36
+ description_cols.merge!({
31
37
  "Status" => lambda {|it| format_process_status(it) },
32
38
  # "# Events" => lambda {|it| (it['events'] || []).size() },
33
- }
39
+ })
34
40
  print_description_list(description_cols, process, options)
35
41
 
36
42
  if process['error']
@@ -1067,7 +1067,7 @@ module Morpheus::Cli::ProvisioningHelper
1067
1067
 
1068
1068
  no_prompt = (options[:no_prompt] || (options[:options] && options[:options][:no_prompt]))
1069
1069
  volumes = []
1070
- plan_size = nil
1070
+ plan_size = 0
1071
1071
  if plan_info['maxStorage'].to_i > 0
1072
1072
  plan_size = plan_info['maxStorage'].to_i / (1024 * 1024 * 1024)
1073
1073
  end
@@ -1909,25 +1909,16 @@ module Morpheus::Cli::ProvisioningHelper
1909
1909
  default_interface_type_value = (network_interface_type_options.find {|t| t['value'].to_s == network_interface['networkInterfaceTypeId'].to_s} || {})['name']
1910
1910
  v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldContext' => field_context, 'fieldName' => 'networkInterfaceTypeId', 'type' => 'select', 'fieldLabel' => "Network Interface Type", 'selectOptions' => network_interface_type_options, 'required' => true, 'skipSingleOption' => true, 'description' => 'Choose a network interface type.', 'defaultValue' => default_interface_type_value}], options[:options])
1911
1911
  network_interface['networkInterfaceTypeId'] = v_prompt[field_context]['networkInterfaceTypeId'].to_i
1912
+ selected_network_interface_type = network_interface_types.find {|it| it["id"] == network_interface['networkInterfaceTypeId']}
1912
1913
  end
1913
1914
 
1914
- # choose IP if network allows it
1915
- # allowStaticOverride is only returned in 4.2.1+, so treat null as true for now..
1916
- ip_available = selected_network['allowStaticOverride'] == true || selected_network['allowStaticOverride'].nil?
1917
- ip_required = true
1918
- if selected_network['id'].to_s.include?('networkGroup')
1919
- #puts "IP Address: Using network group." if !no_prompt
1920
- ip_available = false
1921
- ip_required = false
1922
- elsif selected_network['pool']
1923
- #puts "IP Address: Using pool '#{selected_network['pool']['name']}'" if !no_prompt
1924
- ip_required = false
1925
- elsif selected_network['dhcpServer']
1926
- #puts "IP Address: Using DHCP" if !no_prompt
1927
- ip_required = false
1928
- end
1915
+ # determine if IP can/should be specified for selected network
1916
+ ip_flags = determine_ip_requirements(selected_network)
1917
+ ip_available = ip_flags[:ip_available]
1918
+ ip_required = ip_flags[:ip_required]
1929
1919
 
1930
- if ip_available
1920
+ # Prompt for IP input when static assignment is allowed or an IP is explicitly required
1921
+ if ip_available || ip_required
1931
1922
  v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldContext' => field_context, 'fieldName' => 'ipAddress', 'type' => 'text', 'fieldLabel' => "IP Address", 'required' => ip_required, 'description' => 'Enter an IP for this network interface. x.x.x.x', 'defaultValue' => network_interface['ipAddress']}], options[:options])
1932
1923
  if v_prompt[field_context] && !v_prompt[field_context]['ipAddress'].to_s.empty?
1933
1924
  network_interface['ipAddress'] = v_prompt[field_context]['ipAddress']
@@ -1940,6 +1931,20 @@ module Morpheus::Cli::ProvisioningHelper
1940
1931
  network_interface['ipMode'] = 'static'
1941
1932
  end
1942
1933
 
1934
+ # prompt for virtual interfaces if supported by the selected network interface type
1935
+ virtual_interfaces = prompt_virtual_interfaces(
1936
+ selected_network_interface_type,
1937
+ networks,
1938
+ network_options,
1939
+ field_context,
1940
+ interface_index,
1941
+ options,
1942
+ no_prompt
1943
+ )
1944
+ virtual_interfaces ||= []
1945
+ network_interface['networkInterfaces'] = virtual_interfaces unless virtual_interfaces.empty?
1946
+
1947
+ # append the interface
1943
1948
  network_interfaces << network_interface
1944
1949
  interface_index += 1
1945
1950
  if options[:options] && options[:options]['networkInterfaces'] && options[:options]['networkInterfaces'][interface_index]
@@ -1957,6 +1962,153 @@ module Morpheus::Cli::ProvisioningHelper
1957
1962
 
1958
1963
  end
1959
1964
 
1965
+ # determine if IP can/should be specified for selected network
1966
+ def determine_ip_requirements(selected_network)
1967
+ ip_available = selected_network['allowStaticOverride'] != false
1968
+ ip_required = true
1969
+ if selected_network['id'].to_s.include?('networkGroup')
1970
+ ip_available = false
1971
+ ip_required = false
1972
+ elsif selected_network['pool']
1973
+ ip_required = false
1974
+ elsif selected_network['dhcpServer']
1975
+ ip_required = false
1976
+ end
1977
+ {ip_available: ip_available, ip_required: ip_required}
1978
+ end
1979
+
1980
+ # Prompt for virtual interfaces if supported by the selected network interface type
1981
+ def prompt_virtual_interfaces(selected_network_interface_type, networks, network_options, field_context, interface_index, options, no_prompt)
1982
+ return [] unless selected_network_interface_type && selected_network_interface_type['hasVirtualInterfaces']
1983
+
1984
+ virtual_interfaces = []
1985
+ # gather virtual interface types for the selected network interface type
1986
+ network_interface_types = selected_network_interface_type['virtualInterfaces'] || []
1987
+ return [] if network_interface_types.empty?
1988
+
1989
+ network_interface_type_options = []
1990
+ network_interface_types.each do |opt|
1991
+ next if opt.nil?
1992
+ network_interface_type_options << {'name' => opt['name'], 'value' => opt['id']}
1993
+ end
1994
+
1995
+ # Preconfigured virtual interfaces passed in options
1996
+ prespecified = false
1997
+ preconfigured_vi = []
1998
+ if options[:options]
1999
+ if options[:options]['networkInterfaces'] &&
2000
+ options[:options]['networkInterfaces'][interface_index]
2001
+ preconfigured_vi = options[:options]['networkInterfaces'][interface_index]['networkInterfaces'] || []
2002
+ prespecified = true
2003
+ end
2004
+ if options[:options][field_context].is_a?(Hash) && !options[:options][field_context].empty?
2005
+ prespecified = true
2006
+ end
2007
+ end
2008
+
2009
+ vi_index = 0
2010
+ has_another = false
2011
+ # Process any pre-configured virtual interfaces first, then prompt for additional ones.
2012
+ if preconfigured_vi[vi_index]
2013
+ has_another = true
2014
+ elsif options[:options] && options[:options][field_context] && !options[:options][field_context].empty?
2015
+ has_another = options[:options][field_context]['virtualInterface'] || false
2016
+ end
2017
+ confirm_message = prespecified ? "Add virtual network interface? (under prespecified interface #{interface_index})": "Add virtual network interface?"
2018
+
2019
+ add_another = has_another || (!no_prompt && Morpheus::Cli::OptionTypes.confirm(confirm_message, {default:false}))
2020
+ while add_another
2021
+ vi_field_context = vi_index == 0 ? "virtualInterface" : "virtualInterface#{vi_index+1}"
2022
+ vi = preconfigured_vi[vi_index] ? preconfigured_vi[vi_index].dup : {}
2023
+
2024
+ # Determine default network for this virtual interface
2025
+ default_network_id = vi['networkId'] || (vi['network'] && vi['network']['id'])
2026
+ default_network_name = (network_options.find {|n| n['value'] == default_network_id} || {})['name']
2027
+
2028
+ # Choose network
2029
+ v_prompt = Morpheus::Cli::OptionTypes.prompt(
2030
+ [{
2031
+ 'fieldContext' => "#{field_context}.#{vi_field_context}",
2032
+ 'fieldName' => 'networkId',
2033
+ 'type' => 'select',
2034
+ 'fieldLabel' => "Virtual Network",
2035
+ 'selectOptions' => network_options,
2036
+ 'required' => true,
2037
+ 'skipSingleOption' => false,
2038
+ 'description' => 'Choose a network for this virtual interface.',
2039
+ 'defaultValue' => default_network_name
2040
+ }],
2041
+ options[:options],
2042
+ nil,
2043
+ {},
2044
+ no_prompt,
2045
+ true
2046
+ )
2047
+ vi['network'] ||= {}
2048
+ vi['network']['id'] = v_prompt[field_context][vi_field_context]['networkId'].to_s
2049
+
2050
+ # determine / prompt interface type for virtual interface
2051
+ default_interface_type_id = vi['networkInterfaceTypeId'] ||
2052
+ (vi['networkInterfaceType'] && vi['networkInterfaceType']['id'])
2053
+ default_interface_type_name =
2054
+ (network_interface_type_options.find { |t| t['value'].to_s == default_interface_type_id.to_s } || {})['name']
2055
+ type_prompt = Morpheus::Cli::OptionTypes.prompt(
2056
+ [{
2057
+ 'fieldContext' => "#{field_context}.#{vi_field_context}",
2058
+ 'fieldName' => 'networkInterfaceTypeId',
2059
+ 'type' => 'select',
2060
+ 'fieldLabel' => "Virtual Network Interface Type",
2061
+ 'selectOptions' => network_interface_type_options,
2062
+ 'required' => true,
2063
+ 'skipSingleOption' => true,
2064
+ 'description' => 'Choose a network interface type.',
2065
+ 'defaultValue' => default_interface_type_name
2066
+ }],
2067
+ options[:options]
2068
+ )
2069
+ vi['networkInterfaceTypeId'] = type_prompt[field_context][vi_field_context]['networkInterfaceTypeId'].to_i
2070
+
2071
+ # IP logic
2072
+ selected_network = networks.find {|n| n['id'].to_s == vi['network']['id'] }
2073
+ if selected_network
2074
+ ip_flags = determine_ip_requirements(selected_network)
2075
+ if ip_flags[:ip_available] || ip_flags[:ip_required]
2076
+ v_prompt = Morpheus::Cli::OptionTypes.prompt(
2077
+ [{
2078
+ 'fieldContext' => "#{field_context}.#{vi_field_context}",
2079
+ 'fieldName' => 'ipAddress',
2080
+ 'type' => 'text',
2081
+ 'fieldLabel' => "IP Address for Virtual Interface",
2082
+ 'required' => ip_flags[:ip_required],
2083
+ 'description' => 'Enter an IP for this virtual interface.',
2084
+ 'defaultValue' => vi['ipAddress']
2085
+ }],
2086
+ options[:options]
2087
+ )
2088
+ if v_prompt[field_context][vi_field_context] && !v_prompt[field_context][vi_field_context]['ipAddress'].to_s.empty?
2089
+ vi['ipAddress'] = v_prompt[field_context][vi_field_context]['ipAddress']
2090
+ vi['ipMode'] = 'static'
2091
+ elsif ip_flags[:ip_required] == false && selected_network['dhcpServer']
2092
+ vi['ipMode'] = 'dhcp'
2093
+ end
2094
+ end
2095
+ end
2096
+
2097
+ virtual_interfaces << vi
2098
+ vi_index += 1
2099
+
2100
+ has_another = false
2101
+ confirm_message = prespecified ? "Add another virtual network interface? (under prespecified interface #{interface_index})": "Add another virtual network interface?"
2102
+ if preconfigured_vi[vi_index]
2103
+ has_another = true
2104
+ elsif options[:options] && options[:options][field_context]
2105
+ has_another = options[:options][field_context]["virtualInterface#{vi_index+1}"] && !options[:options][field_context]["virtualInterface#{vi_index+1}"].empty?
2106
+ end
2107
+ add_another = has_another || (!no_prompt && Morpheus::Cli::OptionTypes.confirm(confirm_message,{default:false}))
2108
+ end
2109
+ virtual_interfaces
2110
+ end
2111
+
1960
2112
  # Prompts user for environment variables for new instance
1961
2113
  # returns array of evar objects {id: null, name: "VAR", value: "somevalue"}
1962
2114
  def prompt_evars(options={})
@@ -2145,8 +2297,8 @@ module Morpheus::Cli::ProvisioningHelper
2145
2297
  return payload
2146
2298
  end
2147
2299
 
2148
- def prompt_cluster_load_balancer(cluster, options)
2149
- v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'loadBalancerTypeId', 'type' => 'select', 'fieldLabel' => "Load Balancer", 'optionSource' => 'loadBalancerTypes', 'required' => false, 'description' => 'Select Load Balancer for Cluster', 'defaultValue' => '', 'excludeKubevip' => true}], options[:options], api_client, cluster)
2300
+ def prompt_cluster_load_balancer(cluster, options, can_use_kubevip)
2301
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'loadBalancerTypeId', 'type' => 'select', 'fieldLabel' => "Load Balancer", 'optionSource' => 'loadBalancerTypes', 'required' => false, 'description' => 'Select Load Balancer for Cluster', 'defaultValue' => '', 'excludeKubevip' => !can_use_kubevip}], options[:options], api_client, cluster)
2150
2302
  lb_type_id = v_prompt['loadBalancerTypeId']
2151
2303
 
2152
2304
  if lb_type_id.empty?
@@ -1,6 +1,6 @@
1
1
 
2
2
  module Morpheus
3
3
  module Cli
4
- VERSION = "8.0.9.1"
4
+ VERSION = "8.0.10"
5
5
  end
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: morpheus-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.0.9.1
4
+ version: 8.0.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Estes
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2025-08-26 00:00:00.000000000 Z
14
+ date: 2025-10-06 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: public_suffix