morpheus-cli 6.1.1 → 6.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Dockerfile +1 -1
- data/lib/morpheus/api/api_client.rb +8 -4
- data/lib/morpheus/api/backup_jobs_interface.rb +4 -0
- data/lib/morpheus/api/backup_restores_interface.rb +23 -0
- data/lib/morpheus/api/backup_results_interface.rb +28 -0
- data/lib/morpheus/api/backups_interface.rb +5 -4
- data/lib/morpheus/cli/cli_command.rb +172 -45
- data/lib/morpheus/cli/commands/appliance_settings_command.rb +7 -19
- data/lib/morpheus/cli/commands/apps.rb +1 -1
- data/lib/morpheus/cli/commands/backup_jobs_command.rb +77 -20
- data/lib/morpheus/cli/commands/backup_restores_command.rb +144 -0
- data/lib/morpheus/cli/commands/backup_results_command.rb +149 -0
- data/lib/morpheus/cli/commands/backups_command.rb +214 -93
- data/lib/morpheus/cli/commands/hosts.rb +15 -2
- data/lib/morpheus/cli/commands/instances.rb +23 -3
- data/lib/morpheus/cli/commands/load_balancer_pools.rb +37 -1
- data/lib/morpheus/cli/commands/security_groups.rb +58 -37
- data/lib/morpheus/cli/commands/service_catalog_command.rb +50 -83
- data/lib/morpheus/cli/commands/view.rb +20 -20
- data/lib/morpheus/cli/mixins/backups_helper.rb +58 -0
- data/lib/morpheus/cli/mixins/print_helper.rb +27 -4
- data/lib/morpheus/cli/option_types.rb +10 -7
- data/lib/morpheus/cli/version.rb +1 -1
- data/lib/morpheus/formatters.rb +1 -1
- data/lib/morpheus/routes.rb +18 -3
- metadata +6 -8
- data/lib/morpheus/api/doc_interface.rb +0 -50
- data/lib/morpheus/cli/commands/doc.rb +0 -182
- data/test/api/doc_interface_test.rb +0 -35
- data/test/cli/doc_test.rb +0 -35
@@ -210,6 +210,7 @@ class Morpheus::Cli::SecurityGroups
|
|
210
210
|
params = {}
|
211
211
|
options = {:options => {}}
|
212
212
|
cloud_id = nil
|
213
|
+
resource_pool_id = nil
|
213
214
|
tenants = nil
|
214
215
|
group_access_all = nil
|
215
216
|
group_access_list = nil
|
@@ -225,6 +226,9 @@ class Morpheus::Cli::SecurityGroups
|
|
225
226
|
opts.on( '-c', '--cloud CLOUD', "Scoped Cloud Name or ID" ) do |val|
|
226
227
|
cloud_id = val
|
227
228
|
end
|
229
|
+
opts.on( '--resource-pool ID', String, "ID of the Resource Pool for Amazon VPC and Azure Resource Group" ) do |val|
|
230
|
+
resource_pool_id = val
|
231
|
+
end
|
228
232
|
opts.on('--group-access-all [on|off]', String, "Toggle Access for all groups.") do |val|
|
229
233
|
group_access_all = val.to_s == 'on' || val.to_s == 'true' || val.to_s == ''
|
230
234
|
end
|
@@ -321,17 +325,20 @@ class Morpheus::Cli::SecurityGroups
|
|
321
325
|
if !v_prompt['zoneId'].to_s.empty? && v_prompt['zoneId'].to_s != 'all' && v_prompt['zoneId'].to_s != '-1'
|
322
326
|
payload['securityGroup']['zoneId'] = v_prompt['zoneId']
|
323
327
|
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
328
|
+
cloud = find_cloud_by_id(payload['securityGroup']['zoneId'])
|
329
|
+
|
330
|
+
# parse --resource-pool
|
331
|
+
# if resource_pool_id
|
332
|
+
# resource_pool = find_resource_pool_by_name_or_id(cloud['id'], resource_pool_id)
|
333
|
+
# return 1 if resource_pool.nil?
|
334
|
+
# end
|
335
|
+
|
336
|
+
# prompt for zone specific settings that go under "securityGroup.customOptions" for some reason
|
337
|
+
custom_options_values = prompt_security_group_custom_options(options, cloud, resource_pool_id)
|
338
|
+
payload['securityGroup'].deep_merge!(custom_options_values)
|
333
339
|
end
|
334
340
|
rescue => ex
|
341
|
+
# raise ex
|
335
342
|
print yellow,"Failed to determine the available scoped clouds.",reset,"\n"
|
336
343
|
end
|
337
344
|
|
@@ -626,34 +633,8 @@ class Morpheus::Cli::SecurityGroups
|
|
626
633
|
# return 1 if resource_pool.nil?
|
627
634
|
# end
|
628
635
|
|
629
|
-
#
|
630
|
-
|
631
|
-
# default to the cloud type code, since it's the same...
|
632
|
-
# no optionTypes returned here, so hard coded by type
|
633
|
-
network_server_type = cloud['networkServer'] ? cloud['networkServer']['type'] : (cloud['securityServer'] ? cloud['securityServer']['type'] : cloud["zoneType"]["code"])
|
634
|
-
custom_options_values = {}
|
635
|
-
if network_server_type == 'amazon'
|
636
|
-
if cloud['config'] && !cloud['config']['vpc'].to_s.empty?
|
637
|
-
custom_options_values.deep_merge!({'customOptions' => {'vpc' => cloud['config']['vpc']} })
|
638
|
-
else
|
639
|
-
options[:options].deep_merge!({'customOptions' => {'vpc' => resource_pool_id} }) if resource_pool_id
|
640
|
-
custom_options_values = Morpheus::Cli::OptionTypes.prompt([{'fieldContext' => 'customOptions', 'fieldName' => 'vpc', 'fieldLabel' => 'VPC', 'type' => 'select', 'optionSource' => 'zonePools', 'required' => true, 'config' => {'valueField' => 'externalId'}}], options[:options], @api_client, {zoneId: cloud['id'], ignoreDefaultPool: true})
|
641
|
-
end
|
642
|
-
elsif network_server_type == 'azure' || network_server_type == 'azurestack'
|
643
|
-
if cloud['config'] && !cloud['config']['resourceGroup'].to_s.empty?
|
644
|
-
custom_options_values.deep_merge!({'customOptions' => {'resourceGroup' => cloud['config']['resourceGroup']} })
|
645
|
-
else
|
646
|
-
options[:options].deep_merge!({'customOptions' => {'resourceGroup' => resource_pool_id} }) if resource_pool_id
|
647
|
-
custom_options_values = Morpheus::Cli::OptionTypes.prompt([{'fieldContext' => 'customOptions', 'fieldName' => 'resourceGroup', 'fieldLabel' => 'Resource Group', 'type' => 'select', 'optionSource' => 'zonePools', 'required' => true, 'config' => {'valueField' => 'externalId'}}], options[:options], @api_client, {zoneId: cloud['id'], ignoreDefaultPool: true})
|
648
|
-
end
|
649
|
-
elsif network_server_type == 'openstack' || network_server_type == 'opentelekom' || network_server_type == 'huawei'
|
650
|
-
if cloud['config'] && !cloud['config']['resourcePoolId'].to_s.empty?
|
651
|
-
custom_options_values.deep_merge!({'customOptions' => {'resourcePoolId' => cloud['config']['resourcePoolId']} })
|
652
|
-
else
|
653
|
-
options[:options].deep_merge!({'customOptions' => {'resourcePoolId' => resource_pool_id} }) if resource_pool_id
|
654
|
-
custom_options_values = Morpheus::Cli::OptionTypes.prompt([{'fieldContext' => 'customOptions', 'fieldName' => 'resourcePoolId', 'fieldLabel' => 'Resource Pool', 'type' => 'select', 'optionSource' => 'zonePools', 'required' => true}], options[:options], @api_client, {zoneId: cloud['id'], ignoreDefaultPool: true})
|
655
|
-
end
|
656
|
-
end
|
636
|
+
# prompt for zone specific settings that go under "securityGroup.customOptions" for some reason
|
637
|
+
custom_options_values = prompt_security_group_custom_options(options, cloud, resource_pool_id)
|
657
638
|
payload['securityGroupLocation'].deep_merge!(custom_options_values)
|
658
639
|
end
|
659
640
|
@security_groups_interface.setopts(options)
|
@@ -1270,4 +1251,44 @@ class Morpheus::Cli::SecurityGroups
|
|
1270
1251
|
end
|
1271
1252
|
end
|
1272
1253
|
|
1254
|
+
def prompt_security_group_custom_options(options, cloud, resource_pool_id=nil)
|
1255
|
+
custom_options_values = {}
|
1256
|
+
# Custom Options prompt
|
1257
|
+
# securityServer is no longer used, it has been replaced by networkServer,
|
1258
|
+
# default to the cloud type code, since it's the same...
|
1259
|
+
# no optionTypes returned here, so hard coded by type
|
1260
|
+
# The API used to return securityServer which could be fetched to get its optionTypes
|
1261
|
+
if cloud['securityServer']
|
1262
|
+
sec_server = @network_security_servers.get(cloud['securityServer']['id'])['networkSecurityServer']
|
1263
|
+
if sec_server['type']
|
1264
|
+
v_prompt = Morpheus::Cli::OptionTypes.prompt(sec_server['type']['optionTypes'], options[:options], @api_client, {zoneId: cloud['id']})
|
1265
|
+
custom_options_values.deep_merge!(v_prompt)
|
1266
|
+
end
|
1267
|
+
else
|
1268
|
+
network_server_type = cloud['networkServer'] ? cloud['networkServer']['type'] : (cloud['securityServer'] ? cloud['securityServer']['type'] : cloud["zoneType"]["code"])
|
1269
|
+
if network_server_type == 'amazon'
|
1270
|
+
if cloud['config'] && !cloud['config']['vpc'].to_s.empty?
|
1271
|
+
custom_options_values.deep_merge!({'customOptions' => {'vpc' => cloud['config']['vpc']} })
|
1272
|
+
else
|
1273
|
+
options[:options].deep_merge!({'customOptions' => {'vpc' => resource_pool_id} }) if resource_pool_id
|
1274
|
+
custom_options_values = Morpheus::Cli::OptionTypes.prompt([{'fieldContext' => 'customOptions', 'fieldName' => 'vpc', 'fieldLabel' => 'VPC', 'type' => 'select', 'optionSource' => 'zonePools', 'required' => true, 'config' => {'valueField' => 'externalId'}}], options[:options], @api_client, {zoneId: cloud['id'], ignoreDefaultPool: true})
|
1275
|
+
end
|
1276
|
+
elsif network_server_type == 'azure' || network_server_type == 'azurestack'
|
1277
|
+
if cloud['config'] && !cloud['config']['resourceGroup'].to_s.empty?
|
1278
|
+
custom_options_values.deep_merge!({'customOptions' => {'resourceGroup' => cloud['config']['resourceGroup']} })
|
1279
|
+
else
|
1280
|
+
options[:options].deep_merge!({'customOptions' => {'resourceGroup' => resource_pool_id} }) if resource_pool_id
|
1281
|
+
custom_options_values = Morpheus::Cli::OptionTypes.prompt([{'fieldContext' => 'customOptions', 'fieldName' => 'resourceGroup', 'fieldLabel' => 'Resource Group', 'type' => 'select', 'optionSource' => 'zonePools', 'required' => true, 'config' => {'valueField' => 'externalId'}}], options[:options], @api_client, {zoneId: cloud['id'], ignoreDefaultPool: true})
|
1282
|
+
end
|
1283
|
+
elsif network_server_type == 'openstack' || network_server_type == 'opentelekom' || network_server_type == 'huawei'
|
1284
|
+
if cloud['config'] && !cloud['config']['resourcePoolId'].to_s.empty?
|
1285
|
+
custom_options_values.deep_merge!({'customOptions' => {'resourcePoolId' => cloud['config']['resourcePoolId']} })
|
1286
|
+
else
|
1287
|
+
options[:options].deep_merge!({'customOptions' => {'resourcePoolId' => resource_pool_id} }) if resource_pool_id
|
1288
|
+
custom_options_values = Morpheus::Cli::OptionTypes.prompt([{'fieldContext' => 'customOptions', 'fieldName' => 'resourcePoolId', 'fieldLabel' => 'Resource Pool', 'type' => 'select', 'optionSource' => 'zonePools', 'required' => true}], options[:options], @api_client, {zoneId: cloud['id'], ignoreDefaultPool: true})
|
1289
|
+
end
|
1290
|
+
end
|
1291
|
+
end
|
1292
|
+
return custom_options_values
|
1293
|
+
end
|
1273
1294
|
end
|
@@ -560,27 +560,17 @@ EOT
|
|
560
560
|
# fetch current cart
|
561
561
|
# cart = @service_catalog_interface.get_cart()['cart']
|
562
562
|
update_cart_object_key = 'order'
|
563
|
-
|
563
|
+
parse_payload(options, update_cart_object_key) do |payload|
|
564
564
|
payload.deep_merge!({update_cart_object_key => parse_passed_options(options)})
|
565
565
|
if payload[update_cart_object_key].empty? # || options[:no_prompt]
|
566
566
|
raise_command_error "Specify at least one option to update.\n#{optparse}"
|
567
567
|
end
|
568
568
|
end
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
next
|
574
|
-
end
|
575
|
-
json_response = @service_catalog_interface.update_cart(payload)
|
576
|
-
#cart = json_response['cart']
|
577
|
-
#cart = @service_catalog_interface.get_cart()['cart']
|
578
|
-
render_response(json_response, options, 'cart') do
|
579
|
-
print_green_success "Updated cart"
|
580
|
-
get_cart([] + (options[:remote] ? ["-r",options[:remote]] : []))
|
581
|
-
end
|
569
|
+
execute_api(@service_catalog_interface, :update_cart, [params], options, "cart") do |json_response|
|
570
|
+
#cart = json_response["cart"]
|
571
|
+
print_green_success "Updated cart"
|
572
|
+
get_cart([] + (options[:remote] ? ["-r",options[:remote]] : []))
|
582
573
|
end
|
583
|
-
return 0, nil
|
584
574
|
end
|
585
575
|
|
586
576
|
def add(args)
|
@@ -600,6 +590,7 @@ EOT
|
|
600
590
|
end
|
601
591
|
opts.on('--validate','--validate', "Validate Only. Validates the configuration and skips adding the item.") do
|
602
592
|
options[:validate_only] = true
|
593
|
+
params['validate'] = true
|
603
594
|
end
|
604
595
|
opts.on('--context [instance|server]', String, "Context Type for operational workflow types") do |val|
|
605
596
|
workflow_context = val.to_s
|
@@ -608,7 +599,7 @@ EOT
|
|
608
599
|
workflow_target = val.to_s
|
609
600
|
end
|
610
601
|
opts.add_hidden_option('--sigdig')
|
611
|
-
|
602
|
+
build_standard_add_many_options(opts, options, [:sigdig])
|
612
603
|
opts.footer = <<-EOT
|
613
604
|
Add an item to your cart
|
614
605
|
[type] is required, this is name or id of a catalog item type.
|
@@ -622,7 +613,7 @@ EOT
|
|
622
613
|
type_id = args.join(" ")
|
623
614
|
end
|
624
615
|
add_item_object_key = 'item'
|
625
|
-
|
616
|
+
parse_payload(options, add_item_object_key) do |payload|
|
626
617
|
payload.deep_merge!({add_item_object_key => parse_passed_options(options)})
|
627
618
|
# prompt for Type
|
628
619
|
if type_id
|
@@ -712,46 +703,35 @@ EOT
|
|
712
703
|
end
|
713
704
|
end
|
714
705
|
end
|
715
|
-
|
716
|
-
params['validate'] = true
|
717
|
-
end
|
718
|
-
process_payloads(payloads, options) do |payload|
|
719
|
-
@service_catalog_interface.setopts(options)
|
720
|
-
if options[:dry_run]
|
721
|
-
print_dry_run @service_catalog_interface.dry.create_cart_item(payload, params)
|
722
|
-
next
|
723
|
-
end
|
724
|
-
json_response = @service_catalog_interface.create_cart_item(payload, params)
|
706
|
+
execute_api(@service_catalog_interface, :create_cart_item, [params], options, 'item') do |json_response|
|
725
707
|
cart_item = json_response['item']
|
726
|
-
|
727
|
-
if
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
print reset, "\n"
|
747
|
-
else
|
748
|
-
# not needed because it will be http 400
|
749
|
-
print_rest_errors(json_response, options)
|
750
|
-
end
|
708
|
+
if options[:validate_only]
|
709
|
+
if json_response['success']
|
710
|
+
print_h2 "Validated Cart Item", [], options
|
711
|
+
cart_item_columns = {
|
712
|
+
"Type" => lambda {|it| it['type']['name'] rescue '' },
|
713
|
+
#"Qty" => lambda {|it| it['quantity'] },
|
714
|
+
"Price" => lambda {|it| it['price'] ? format_money(it['price'] , it['currency'], {sigdig:options[:sigdig] || default_sigdig}) : "No pricing configured" },
|
715
|
+
"Status" => lambda {|it|
|
716
|
+
status_string = format_catalog_item_status(it)
|
717
|
+
if it['errorMessage'].to_s != ""
|
718
|
+
status_string << " - #{it['errorMessage']}"
|
719
|
+
end
|
720
|
+
status_string
|
721
|
+
},
|
722
|
+
#"Config" => lambda {|it| truncate_string(format_name_values(it['config']), 50) }
|
723
|
+
}
|
724
|
+
print as_pretty_table([cart_item], cart_item_columns.upcase_keys!)
|
725
|
+
print reset, "\n"
|
726
|
+
print_green_success(json_response['msg'] || "Item is valid")
|
727
|
+
print reset, "\n"
|
751
728
|
else
|
752
|
-
|
753
|
-
|
729
|
+
# not needed because it will be http 400
|
730
|
+
print_rest_errors(json_response, options)
|
754
731
|
end
|
732
|
+
else
|
733
|
+
print_green_success "Added item to cart"
|
734
|
+
get_cart([] + (options[:remote] ? ["-r",options[:remote]] : []))
|
755
735
|
end
|
756
736
|
end
|
757
737
|
end
|
@@ -935,6 +915,7 @@ EOT
|
|
935
915
|
end
|
936
916
|
opts.on('--validate','--validate', "Validate Only. Validates the configuration and skips creating the order.") do
|
937
917
|
options[:validate_only] = true
|
918
|
+
params['validate'] = true
|
938
919
|
end
|
939
920
|
opts.on('-a', '--details', "Display all details: item configuration." ) do
|
940
921
|
options[:details] = true
|
@@ -962,7 +943,7 @@ EOT
|
|
962
943
|
end
|
963
944
|
payload = {}
|
964
945
|
order_object_key = 'order'
|
965
|
-
|
946
|
+
parse_payload(options, order_object_key) do |payload|
|
966
947
|
payload.deep_merge!({order_object_key => {}})
|
967
948
|
# Prompt for 1-N Types
|
968
949
|
# still_prompting = options[:no_prompt] != true
|
@@ -1076,38 +1057,24 @@ EOT
|
|
1076
1057
|
end
|
1077
1058
|
end
|
1078
1059
|
|
1079
|
-
end
|
1080
|
-
|
1081
|
-
|
1060
|
+
end
|
1082
1061
|
end
|
1083
|
-
|
1084
|
-
params['validate'] = true
|
1085
|
-
#payload['validate'] = true
|
1086
|
-
end
|
1087
|
-
process_payloads(payloads, options) do |payload|
|
1088
|
-
@service_catalog_interface.setopts(options)
|
1089
|
-
if options[:dry_run]
|
1090
|
-
print_dry_run @service_catalog_interface.dry.create_order(payload, params)
|
1091
|
-
next
|
1092
|
-
end
|
1093
|
-
json_response = @service_catalog_interface.create_order(payload, params)
|
1062
|
+
execute_api(@service_catalog_interface, :create_order, [params], options, "order") do |json_response|
|
1094
1063
|
order = json_response['order'] || json_response['cart']
|
1095
|
-
|
1096
|
-
if
|
1097
|
-
|
1098
|
-
print_h2 "Review Order", [], options
|
1099
|
-
print_order_details(order, options)
|
1100
|
-
print_green_success(json_response['msg'] || "Order is valid")
|
1101
|
-
print reset, "\n"
|
1102
|
-
else
|
1103
|
-
# not needed because it will be http 400
|
1104
|
-
print_rest_errors(json_response, options)
|
1105
|
-
end
|
1106
|
-
else
|
1107
|
-
print_green_success "Order placed"
|
1108
|
-
print_h2 "Order Details", [], options
|
1064
|
+
if options[:validate_only]
|
1065
|
+
if json_response['success']
|
1066
|
+
print_h2 "Review Order", [], options
|
1109
1067
|
print_order_details(order, options)
|
1068
|
+
print_green_success(json_response['msg'] || "Order is valid")
|
1069
|
+
print reset, "\n"
|
1070
|
+
else
|
1071
|
+
# not needed because it will be http 400
|
1072
|
+
print_rest_errors(json_response, options)
|
1110
1073
|
end
|
1074
|
+
else
|
1075
|
+
print_green_success "Order placed"
|
1076
|
+
print_h2 "Order Details", [], options
|
1077
|
+
print_order_details(order, options)
|
1111
1078
|
end
|
1112
1079
|
end
|
1113
1080
|
end
|
@@ -46,20 +46,22 @@ Examples:
|
|
46
46
|
EOT
|
47
47
|
end
|
48
48
|
optparse.parse!(args)
|
49
|
-
|
49
|
+
verify_args!(args:args, optparse:optparse, min: 0, max: 2)
|
50
50
|
connect(options)
|
51
51
|
# todo: it would actually be cool to use the params and include them on the path..
|
52
52
|
# params.merge!(parse_query_options(options))
|
53
|
-
|
53
|
+
# input, *ids = args
|
54
|
+
input = args[0]
|
55
|
+
id = args[1]
|
54
56
|
# default to index page "/"
|
55
|
-
path =
|
57
|
+
path = input || "/"
|
56
58
|
if options[:absolute_path] != true
|
57
59
|
if path.start_with?("/")
|
58
60
|
# treat like absolute path, no lookup
|
59
61
|
else
|
60
62
|
# lookup best matching route from sitemap
|
61
63
|
# lookup plural routes first, so 'app' finds apps and not approvals
|
62
|
-
found_route = Morpheus::Routes.lookup(path)
|
64
|
+
found_route = Morpheus::Routes.lookup(path, id)
|
63
65
|
if found_route
|
64
66
|
# Morpheus::Logging::DarkPrinter.puts "Found matching route: '#{path}' => '#{found_route}'" if Morpheus::Logging.debug?
|
65
67
|
path = found_route
|
@@ -69,26 +71,24 @@ EOT
|
|
69
71
|
end
|
70
72
|
# always add a leading slash
|
71
73
|
path = path.start_with?("/") ? path : "/#{path}"
|
72
|
-
# append id
|
73
|
-
if
|
74
|
-
# convert
|
74
|
+
# append id to path if passed
|
75
|
+
if id
|
76
|
+
# convert name to id
|
75
77
|
# assume the last part of path is the type and use generic finder
|
76
78
|
# only lookup names, and allow any id
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
record['id'].to_s
|
87
|
-
else
|
88
|
-
id
|
79
|
+
if id.to_s !~ /\A\d{1,}\Z/
|
80
|
+
# record type is just args[0]
|
81
|
+
record_type = input
|
82
|
+
# assume the last part of path is the type
|
83
|
+
# record_type = path.split("/").last
|
84
|
+
# record_type.sub!('#!', '')
|
85
|
+
record = find_by_name(record_type, id)
|
86
|
+
if record.nil?
|
87
|
+
raise_command_error("[id] is invalid. No #{record_type} found for '#{id}'", args, optparse)
|
89
88
|
end
|
89
|
+
id = record['id'].to_s
|
90
90
|
end
|
91
|
-
path = "#{path}
|
91
|
+
path = "#{path}/#{id}"
|
92
92
|
end
|
93
93
|
end
|
94
94
|
# build the link to use, either our path or oauth-redirect to that path
|
@@ -110,4 +110,62 @@ module Morpheus::Cli::BackupsHelper
|
|
110
110
|
return backup_jobs[0]
|
111
111
|
end
|
112
112
|
end
|
113
|
+
|
114
|
+
## Backup Results
|
115
|
+
|
116
|
+
def backup_result_list_column_definitions()
|
117
|
+
{
|
118
|
+
"ID" => 'id',
|
119
|
+
"Backup" => lambda {|it| it['backup']['name'] rescue '' },
|
120
|
+
"Status" => lambda {|it| format_backup_result_status(it) },
|
121
|
+
#"Duration" => lambda {|it| format_duration(it['startDate'], it['endDate']) },
|
122
|
+
"Duration" => lambda {|it| format_duration_milliseconds(it['durationMillis']) },
|
123
|
+
"Start Date" => lambda {|it| format_local_dt(it['startDate']) },
|
124
|
+
"End Date" => lambda {|it| format_local_dt(it['endDate']) },
|
125
|
+
"Size" => lambda {|it| format_bytes(it['sizeInMb'], 'MB') },
|
126
|
+
}
|
127
|
+
end
|
128
|
+
|
129
|
+
def backup_result_column_definitions()
|
130
|
+
backup_result_list_column_definitions()
|
131
|
+
end
|
132
|
+
|
133
|
+
def format_backup_result_status(backup_result, return_color=cyan)
|
134
|
+
out = ""
|
135
|
+
status_string = backup_result['status'].to_s.upcase
|
136
|
+
if status_string == 'SUCCEEDED' || status_string == 'SUCCESS'
|
137
|
+
out << "#{green}#{status_string.upcase}#{return_color}"
|
138
|
+
elsif status_string == 'FAILED'
|
139
|
+
out << "#{red}#{status_string.upcase}#{return_color}"
|
140
|
+
elsif status_string
|
141
|
+
out << "#{cyan}#{status_string.upcase}#{return_color}"
|
142
|
+
else
|
143
|
+
out << ""
|
144
|
+
end
|
145
|
+
out
|
146
|
+
end
|
147
|
+
|
148
|
+
## Backup Restores
|
149
|
+
|
150
|
+
def backup_restore_list_column_definitions()
|
151
|
+
{
|
152
|
+
"ID" => 'id',
|
153
|
+
"Backup" => lambda {|it| it['backup']['name'] rescue '' },
|
154
|
+
"Backup Result ID" => lambda {|it| it['backupResultId'] rescue '' },
|
155
|
+
"Target" => lambda {|it| it['instance']['name'] rescue '' },
|
156
|
+
"Status" => lambda {|it| format_backup_result_status(it) },
|
157
|
+
#"Duration" => lambda {|it| format_duration(it['startDate'], it['endDate']) },
|
158
|
+
"Duration" => lambda {|it| format_duration_milliseconds(it['durationMillis']) },
|
159
|
+
"Start Date" => lambda {|it| format_local_dt(it['startDate']) },
|
160
|
+
"End Date" => lambda {|it| format_local_dt(it['endDate']) },
|
161
|
+
}
|
162
|
+
end
|
163
|
+
|
164
|
+
def backup_restore_column_definitions()
|
165
|
+
backup_restore_list_column_definitions()
|
166
|
+
end
|
167
|
+
|
168
|
+
def format_backup_restore_status(backup_restore, return_color=cyan)
|
169
|
+
format_backup_result_status(backup_restore, return_color)
|
170
|
+
end
|
113
171
|
end
|
@@ -191,7 +191,24 @@ module Morpheus::Cli::PrintHelper
|
|
191
191
|
output = ""
|
192
192
|
if api_request[:curl] || options[:curl]
|
193
193
|
output = format_curl_command(http_method, url, headers, payload, options)
|
194
|
+
elsif options[:json]
|
195
|
+
# --dry --json should print the payload only
|
196
|
+
payload_object = payload.is_a?(String) ? JSON.parse(payload) : payload
|
197
|
+
output = as_json(payload_object, options)
|
198
|
+
elsif options[:yaml]
|
199
|
+
# --dry --yaml should print the payload only as yaml
|
200
|
+
payload_object = payload.is_a?(String) ? JSON.parse(payload) : payload
|
201
|
+
output = as_yaml(payload_object, options)
|
194
202
|
else
|
203
|
+
# default format is
|
204
|
+
# DRY RUN
|
205
|
+
# REQUEST
|
206
|
+
# GET https://server/api/things
|
207
|
+
#
|
208
|
+
# JSON
|
209
|
+
# {
|
210
|
+
# "thing": { ... }
|
211
|
+
# }
|
195
212
|
output = format_api_request(http_method, url, headers, payload, options)
|
196
213
|
end
|
197
214
|
# this is an extra scrub, should remove
|
@@ -210,14 +227,20 @@ module Morpheus::Cli::PrintHelper
|
|
210
227
|
if api_request[:curl] || options[:curl]
|
211
228
|
print "\n"
|
212
229
|
print "#{cyan}#{bold}#{dark}CURL COMMAND#{reset}\n"
|
230
|
+
print output
|
231
|
+
print reset, "\n"
|
232
|
+
print reset
|
233
|
+
elsif options[:json] || options[:yaml]
|
234
|
+
# print just the just payload
|
235
|
+
print output, "\n"
|
213
236
|
else
|
214
237
|
print "\n"
|
215
238
|
print "#{cyan}#{bold}#{dark}REQUEST#{reset}\n"
|
239
|
+
print output
|
240
|
+
print reset, "\n"
|
241
|
+
print reset
|
216
242
|
end
|
217
|
-
|
218
|
-
print reset, "\n"
|
219
|
-
print reset
|
220
|
-
return
|
243
|
+
return output
|
221
244
|
end
|
222
245
|
|
223
246
|
def print_system_command_dry_run(cmd, options={})
|
@@ -48,6 +48,7 @@ module Morpheus
|
|
48
48
|
|
49
49
|
# supresses prompting unless --prompt has been passed
|
50
50
|
def self.no_prompt(option_types, options={}, api_client=nil,api_params={})
|
51
|
+
options[:edit_mode] = true # hack used for updates to avoid default values being used
|
51
52
|
if options[:always_prompt]
|
52
53
|
prompt(option_types, options, api_client, api_params)
|
53
54
|
else
|
@@ -218,7 +219,7 @@ module Morpheus
|
|
218
219
|
|
219
220
|
# credential type
|
220
221
|
handle_credential_type = -> {
|
221
|
-
credential_type = select_prompt(option_type.merge({'defaultValue' => value}), api_client, option_params.merge({'credentialTypes' => option_type['config']['credentialTypes']}), !value.nil?, nil, paging_enabled, ignore_empty)
|
222
|
+
credential_type = select_prompt(option_type.merge({'defaultValue' => value}), api_client, option_params.merge({'credentialTypes' => option_type['config']['credentialTypes']}), !value.nil?, nil, paging_enabled, ignore_empty, options[:edit_mode])
|
222
223
|
# continue prompting for local creds
|
223
224
|
if credential_type == 'local'
|
224
225
|
parent_context_map.reject! {|k,v| k == 'credential'}
|
@@ -247,14 +248,14 @@ module Morpheus
|
|
247
248
|
end
|
248
249
|
# these select prompts should just fall down through below, with the extra params no_prompt, use_value
|
249
250
|
elsif option_type['type'] == 'select'
|
250
|
-
value = select_prompt(option_type.merge({'defaultValue' => value, 'defaultInputValue' => input_value}), api_client, option_params, true, nil, false, ignore_empty)
|
251
|
+
value = select_prompt(option_type.merge({'defaultValue' => value, 'defaultInputValue' => input_value}), api_client, option_params, true, nil, false, ignore_empty, options[:edit_mode])
|
251
252
|
elsif option_type['type'] == 'multiSelect'
|
252
253
|
# support value as csv like "thing1, thing2"
|
253
254
|
value_list = value.is_a?(String) ? value.parse_csv.collect {|v| v ? v.to_s.strip : v } : [value].flatten
|
254
255
|
input_value_list = input_value.is_a?(String) ? input_value.parse_csv.collect {|v| v ? v.to_s.strip : v } : [input_value].flatten
|
255
256
|
select_value_list = []
|
256
257
|
value_list.each_with_index do |v, i|
|
257
|
-
select_value_list << select_prompt(option_type.merge({'defaultValue' => v, 'defaultInputValue' => input_value_list[i]}), api_client, option_params, true, nil, false, ignore_empty)
|
258
|
+
select_value_list << select_prompt(option_type.merge({'defaultValue' => v, 'defaultInputValue' => input_value_list[i]}), api_client, option_params, true, nil, false, ignore_empty, options[:edit_mode])
|
258
259
|
end
|
259
260
|
value = select_value_list
|
260
261
|
elsif option_type['type'] == 'typeahead'
|
@@ -302,7 +303,7 @@ module Morpheus
|
|
302
303
|
next
|
303
304
|
end
|
304
305
|
if ['select', 'multiSelect'].include?(option_type['type'])
|
305
|
-
value = select_prompt(option_type, api_client, option_params, true, nil, false, ignore_empty)
|
306
|
+
value = select_prompt(option_type, api_client, option_params, true, nil, false, ignore_empty, options[:edit_mode])
|
306
307
|
value_found = !!value
|
307
308
|
end
|
308
309
|
if ['typeahead', 'multiTypeahead'].include?(option_type['type'])
|
@@ -347,12 +348,12 @@ module Morpheus
|
|
347
348
|
# I suppose the entered value should take precedence
|
348
349
|
# api_params = api_params.merge(options) # this might be good enough
|
349
350
|
# dup it
|
350
|
-
value = select_prompt(option_type, api_client, option_params, options[:no_prompt], nil, paging_enabled, ignore_empty)
|
351
|
+
value = select_prompt(option_type, api_client, option_params, options[:no_prompt], nil, paging_enabled, ignore_empty, options[:edit_mode])
|
351
352
|
if value && option_type['type'] == 'multiSelect'
|
352
353
|
value = [value]
|
353
354
|
recommended_count = (option_type['config'] || {})['recommendedCount'] || 0
|
354
355
|
while self.confirm("Add another #{option_type['fieldLabel']}?", {:default => recommended_count > value.count}) do
|
355
|
-
if addn_value = select_prompt(option_type, api_client, option_params, options[:no_prompt], nil, paging_enabled, ignore_empty)
|
356
|
+
if addn_value = select_prompt(option_type, api_client, option_params, options[:no_prompt], nil, paging_enabled, ignore_empty, options[:edit_mode])
|
356
357
|
value << addn_value
|
357
358
|
else
|
358
359
|
break
|
@@ -476,7 +477,7 @@ module Morpheus
|
|
476
477
|
Thread.current[:_last_select]
|
477
478
|
end
|
478
479
|
|
479
|
-
def self.select_prompt(option_type, api_client, api_params={}, no_prompt=false, use_value=nil, paging_enabled=false, ignore_empty=false)
|
480
|
+
def self.select_prompt(option_type, api_client, api_params={}, no_prompt=false, use_value=nil, paging_enabled=false, ignore_empty=false, edit_mode=false)
|
480
481
|
paging_enabled = false if Morpheus::Cli.windows?
|
481
482
|
field_key = [option_type['fieldContext'], option_type['fieldName']].select {|it| it && it != '' }.join('.')
|
482
483
|
help_field_key = option_type[:help_field_prefix] ? "#{option_type[:help_field_prefix]}.#{field_key}" : field_key
|
@@ -550,6 +551,8 @@ module Morpheus
|
|
550
551
|
print "\n"
|
551
552
|
exit 1
|
552
553
|
end
|
554
|
+
elsif edit_mode
|
555
|
+
# do not use a default value for edit mode
|
553
556
|
# skipSingleOption is no longer supported
|
554
557
|
# elsif !select_options.nil? && select_options.count == 1 && option_type['skipSingleOption'] == true
|
555
558
|
# value_found = true
|
data/lib/morpheus/cli/version.rb
CHANGED
data/lib/morpheus/formatters.rb
CHANGED
@@ -134,7 +134,7 @@ end
|
|
134
134
|
|
135
135
|
def format_duration_milliseconds(milliseconds, format="human", ms_threshold=1000)
|
136
136
|
out = ""
|
137
|
-
milliseconds = milliseconds.abs
|
137
|
+
milliseconds = milliseconds.to_i.abs
|
138
138
|
if ms_threshold && ms_threshold > milliseconds
|
139
139
|
out = "#{milliseconds}ms"
|
140
140
|
else
|
data/lib/morpheus/routes.rb
CHANGED
@@ -100,7 +100,13 @@ module Morpheus::Routes
|
|
100
100
|
],
|
101
101
|
},
|
102
102
|
backups: {
|
103
|
-
|
103
|
+
list: {},
|
104
|
+
show: {},
|
105
|
+
jobs: {},
|
106
|
+
history: [
|
107
|
+
"#!restores",
|
108
|
+
],
|
109
|
+
services: {}
|
104
110
|
},
|
105
111
|
monitoring: {
|
106
112
|
status: {},
|
@@ -162,9 +168,10 @@ module Morpheus::Routes
|
|
162
168
|
|
163
169
|
# lookup a route in the morpheus UI
|
164
170
|
# @param path [String] The input to lookup a route for eg. "dashboard"
|
171
|
+
# @param id [String] ID indicates the show route is needed for a resource for cases where it varies ie. backups
|
165
172
|
# @return full path like "/operations/dashboard"
|
166
|
-
def self.lookup(
|
167
|
-
path =
|
173
|
+
def self.lookup(path, id=nil)
|
174
|
+
path = path.to_s
|
168
175
|
if path.start_with?("/")
|
169
176
|
# absolute path is being looked up
|
170
177
|
return path
|
@@ -174,6 +181,14 @@ module Morpheus::Routes
|
|
174
181
|
|
175
182
|
# map well known aliases
|
176
183
|
case(path.dasherize.pluralize)
|
184
|
+
when "backups"
|
185
|
+
path = id ? "/backups/show" : "/backups/list"
|
186
|
+
when "backup-jobs"
|
187
|
+
path = "/backups/jobs"
|
188
|
+
when "backup-results"
|
189
|
+
path = "/backups/history"
|
190
|
+
when "backup-restores", "restores"
|
191
|
+
path = "/backups/history/#!restores"
|
177
192
|
when "servers","hosts","vms","virtual-machines"
|
178
193
|
# actually should be "/infrastructure/inventory" unless id is passed, show route uses /servers though
|
179
194
|
path = "/infrastructure/servers"
|