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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +1 -1
  3. data/lib/morpheus/api/api_client.rb +8 -4
  4. data/lib/morpheus/api/backup_jobs_interface.rb +4 -0
  5. data/lib/morpheus/api/backup_restores_interface.rb +23 -0
  6. data/lib/morpheus/api/backup_results_interface.rb +28 -0
  7. data/lib/morpheus/api/backups_interface.rb +5 -4
  8. data/lib/morpheus/cli/cli_command.rb +172 -45
  9. data/lib/morpheus/cli/commands/appliance_settings_command.rb +7 -19
  10. data/lib/morpheus/cli/commands/apps.rb +1 -1
  11. data/lib/morpheus/cli/commands/backup_jobs_command.rb +77 -20
  12. data/lib/morpheus/cli/commands/backup_restores_command.rb +144 -0
  13. data/lib/morpheus/cli/commands/backup_results_command.rb +149 -0
  14. data/lib/morpheus/cli/commands/backups_command.rb +214 -93
  15. data/lib/morpheus/cli/commands/hosts.rb +15 -2
  16. data/lib/morpheus/cli/commands/instances.rb +23 -3
  17. data/lib/morpheus/cli/commands/load_balancer_pools.rb +37 -1
  18. data/lib/morpheus/cli/commands/security_groups.rb +58 -37
  19. data/lib/morpheus/cli/commands/service_catalog_command.rb +50 -83
  20. data/lib/morpheus/cli/commands/view.rb +20 -20
  21. data/lib/morpheus/cli/mixins/backups_helper.rb +58 -0
  22. data/lib/morpheus/cli/mixins/print_helper.rb +27 -4
  23. data/lib/morpheus/cli/option_types.rb +10 -7
  24. data/lib/morpheus/cli/version.rb +1 -1
  25. data/lib/morpheus/formatters.rb +1 -1
  26. data/lib/morpheus/routes.rb +18 -3
  27. metadata +6 -8
  28. data/lib/morpheus/api/doc_interface.rb +0 -50
  29. data/lib/morpheus/cli/commands/doc.rb +0 -182
  30. data/test/api/doc_interface_test.rb +0 -35
  31. 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
- zone = find_cloud_by_id(payload['securityGroup']['zoneId'])
325
- # networkServer needed here too? err
326
- if zone['securityServer']
327
- sec_server = @network_security_servers.get(zone['securityServer']['id'])['networkSecurityServer']
328
-
329
- if sec_server['type']
330
- payload['securityGroup'].deep_merge!(Morpheus::Cli::OptionTypes.prompt(sec_server['type']['optionTypes'], options[:options], @api_client, {zoneId: zone['id']}))
331
- end
332
- end
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
- # Custom Options prompt
630
- # securityServer is no longer used, it has been replaced by networkServer,
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
- payloads = parse_payloads(options, update_cart_object_key) do |payload|
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
- process_payloads(payloads, options) do |payload|
570
- @service_catalog_interface.setopts(options)
571
- if options[:dry_run]
572
- print_dry_run @service_catalog_interface.dry.update_cart(payload)
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
- build_standard_update_options(opts, options, [:payloads, :sigdig])
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
- payloads = parse_payloads(options, add_item_object_key) do |payload|
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
- if options[:validate_only]
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
- render_response(json_response, options) do
727
- if options[:validate_only]
728
- if json_response['success']
729
- print_h2 "Validated Cart Item", [], options
730
- cart_item_columns = {
731
- "Type" => lambda {|it| it['type']['name'] rescue '' },
732
- #"Qty" => lambda {|it| it['quantity'] },
733
- "Price" => lambda {|it| it['price'] ? format_money(it['price'] , it['currency'], {sigdig:options[:sigdig] || default_sigdig}) : "No pricing configured" },
734
- "Status" => lambda {|it|
735
- status_string = format_catalog_item_status(it)
736
- if it['errorMessage'].to_s != ""
737
- status_string << " - #{it['errorMessage']}"
738
- end
739
- status_string
740
- },
741
- #"Config" => lambda {|it| truncate_string(format_name_values(it['config']), 50) }
742
- }
743
- print as_pretty_table([cart_item], cart_item_columns.upcase_keys!)
744
- print reset, "\n"
745
- print_green_success(json_response['msg'] || "Item is valid")
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
- print_green_success "Added item to cart"
753
- get_cart([] + (options[:remote] ? ["-r",options[:remote]] : []))
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
- payloads = parse_payloads(options, order_object_key) do |payload|
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
- if options[:validate_only]
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
- render_response(json_response, options) do
1096
- if options[:validate_only]
1097
- if json_response['success']
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
- # verify_args!(args:args, optparse:optparse, min: 0, max: 2)
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
- path, *ids = args
53
+ # input, *ids = args
54
+ input = args[0]
55
+ id = args[1]
54
56
  # default to index page "/"
55
- path = 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(s) to path if passed
73
- if ids.size > 0
74
- # convert names to ids
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
- ids = ids.collect do |id|
78
- if id.to_s !~ /\A\d{1,}\Z/
79
- # assume the last part of path is the type
80
- record_type = path.split("/").last
81
- record_type.sub!('#!', '')
82
- record = find_by_name(record_type, id)
83
- if record.nil?
84
- raise_command_error("[id] is invalid. No #{record_type} found for '#{id}'", args, optparse)
85
- end
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}/" + ids.join("/")
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
- print output
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
@@ -1,6 +1,6 @@
1
1
 
2
2
  module Morpheus
3
3
  module Cli
4
- VERSION = "6.1.1"
4
+ VERSION = "6.2.0"
5
5
  end
6
6
  end
@@ -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.to_i
137
+ milliseconds = milliseconds.to_i.abs
138
138
  if ms_threshold && ms_threshold > milliseconds
139
139
  out = "#{milliseconds}ms"
140
140
  else
@@ -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(input)
167
- path = input.to_s
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"