morpheus-cli 6.1.1 → 6.2.0

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.
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"