morpheus-cli 8.0.9 → 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 +4 -4
- data/Dockerfile +1 -1
- data/lib/morpheus/api/clusters_interface.rb +7 -0
- data/lib/morpheus/api/instances_interface.rb +4 -1
- data/lib/morpheus/api/storage_volumes_interface.rb +7 -0
- data/lib/morpheus/cli/commands/clouds.rb +12 -12
- data/lib/morpheus/cli/commands/clusters.rb +54 -12
- data/lib/morpheus/cli/commands/hosts.rb +136 -29
- data/lib/morpheus/cli/commands/instances.rb +135 -25
- data/lib/morpheus/cli/commands/storage_volumes.rb +65 -1
- data/lib/morpheus/cli/error_handler.rb +5 -0
- data/lib/morpheus/cli/mixins/processes_helper.rb +7 -1
- data/lib/morpheus/cli/mixins/provisioning_helper.rb +171 -19
- data/lib/morpheus/cli/version.rb +1 -1
- data/morpheus-cli.gemspec +1 -1
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b4fc087520efe1dab49dbe42df70b39da196e9539cd78eb162adec2fc8907a54
|
4
|
+
data.tar.gz: b008d63500f56d6a72b12c918db54b3d2869df3fa3407e491411a8eab63ea6be
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f00f2ea9c25064ad69f1038a83a5c0a23708c21d2bc670406ff1527dcdd0808361043c7e7c498fd9a03733106fcd8c1884515ebd4951c12b1bc839c37163b879
|
7
|
+
data.tar.gz: 8c818cdda02325d65715ee474e72480467a27ce14548483415a555002042cfbb7638193feb9fc3c61138effa50d473703eeb4a6f66d47eb4ccf087e9c9b040c5
|
data/Dockerfile
CHANGED
@@ -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
|
-
|
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
|
-
|
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[
|
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
|
-
|
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
|
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
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
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
|
-
|
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
|
-
|
4907
|
-
|
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
|
-
|
4914
|
-
|
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
|
-
|
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
|
-
|
907
|
-
|
908
|
-
|
909
|
-
|
910
|
-
|
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
|
-
|
913
|
-
|
914
|
-
|
915
|
-
|
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
|
-
|
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
|
-
|
1420
|
-
|
1421
|
-
|
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'
|
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
|
-
|
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
|
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 =
|
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
|
-
#
|
1915
|
-
|
1916
|
-
ip_available =
|
1917
|
-
ip_required =
|
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
|
-
|
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' =>
|
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?
|
data/lib/morpheus/cli/version.rb
CHANGED
data/morpheus-cli.gemspec
CHANGED
@@ -29,7 +29,7 @@ Gem::Specification.new do |spec|
|
|
29
29
|
spec.add_dependency "filesize"
|
30
30
|
spec.add_dependency 'mime-types'
|
31
31
|
spec.add_dependency "http"
|
32
|
-
spec.add_dependency "rubyzip"
|
32
|
+
spec.add_dependency "rubyzip", '~> 2.3.2'
|
33
33
|
spec.add_dependency "money"
|
34
34
|
spec.add_dependency "test-unit"
|
35
35
|
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.
|
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-
|
14
|
+
date: 2025-10-06 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: public_suffix
|
@@ -157,16 +157,16 @@ dependencies:
|
|
157
157
|
name: rubyzip
|
158
158
|
requirement: !ruby/object:Gem::Requirement
|
159
159
|
requirements:
|
160
|
-
- - "
|
160
|
+
- - "~>"
|
161
161
|
- !ruby/object:Gem::Version
|
162
|
-
version:
|
162
|
+
version: 2.3.2
|
163
163
|
type: :runtime
|
164
164
|
prerelease: false
|
165
165
|
version_requirements: !ruby/object:Gem::Requirement
|
166
166
|
requirements:
|
167
|
-
- - "
|
167
|
+
- - "~>"
|
168
168
|
- !ruby/object:Gem::Version
|
169
|
-
version:
|
169
|
+
version: 2.3.2
|
170
170
|
- !ruby/object:Gem::Dependency
|
171
171
|
name: money
|
172
172
|
requirement: !ruby/object:Gem::Requirement
|