morpheus-cli 8.0.0 → 8.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 49afeaaa11ed794659cc20a46d3fc145990269dc96f1394a1cd95160bbc2f786
4
- data.tar.gz: cdcf88755b1638f3d1816dde28dcfea8f8c6be27b9300bdfc3c6e4798d3937dd
3
+ metadata.gz: 06ea65156f0a57e7af971494347aae4844608a6cb877087dc9394a127841a75e
4
+ data.tar.gz: a93ddec1dc9e647e23a111a168126dbfc3b683d04844731f5bc53a461305b140
5
5
  SHA512:
6
- metadata.gz: 56f27f9ab0323061846e4b3df32d570efff1b944083fe27700a9d729540ad605a1a39a5c2005cc5a0e19880d5f039a7aa3dbc12ff2c31c9b511426ec736f83a7
7
- data.tar.gz: 7d98ac1e35e7929f3f73ff3968d3a2d1221629d1a2c07d7cdd916b2aa184ca2d30041eaeaf2ac5d53c653650a0a36e0c39a33bebbb9bf02bdcf3e4f84e5173c8
6
+ metadata.gz: 9ca9b136bd006211eed46cd89103c2e7af9157d8111595f4e90b09a7a1e014570405256d64401c78311503e3d4b0aaecf5690526b2014985d79c515e0093eba3
7
+ data.tar.gz: f1355c6ec99b7a05e068e215ba4581a75a712bef545b661a04e7a31ad0b1711071d7f65c316e938171da60d66d0a297898068f5efb70e750a7dc987b2f12014e
data/Dockerfile CHANGED
@@ -1,5 +1,5 @@
1
1
  FROM ruby:2.7.5
2
2
 
3
- RUN gem install morpheus-cli -v 8.0.0
3
+ RUN gem install morpheus-cli -v 8.0.2
4
4
 
5
5
  ENTRYPOINT ["morpheus"]
@@ -25,6 +25,13 @@ class Morpheus::ApplianceSettingsInterface < Morpheus::APIClient
25
25
  execute(opts)
26
26
  end
27
27
 
28
+ def locales(params={})
29
+ url = "#{base_path}/locales"
30
+ headers = { params: params, authorization: "Bearer #{@access_token}" }
31
+ opts = {method: :get, url: url, headers: headers}
32
+ execute(opts)
33
+ end
34
+
28
35
  def maintenance(params={}, payload={})
29
36
  url = "#{base_path}/maintenance"
30
37
  headers = { params: params, authorization: "Bearer #{@access_token}" }
@@ -44,6 +44,12 @@ class Morpheus::VirtualImagesInterface < Morpheus::APIClient
44
44
  execute(method: :put, url: url, headers: headers, payload: payload.to_json)
45
45
  end
46
46
 
47
+ def convert(id, payload)
48
+ url = "#{@base_url}/api/virtual-images/#{id}/convert"
49
+ headers = { :authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
50
+ execute(method: :post, url: url, headers: headers, payload: payload.to_json)
51
+ end
52
+
47
53
  def destroy(id, params={})
48
54
  url = "#{@base_url}/api/virtual-images/#{id}"
49
55
  headers = { :authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
@@ -1278,7 +1278,7 @@ module Morpheus
1278
1278
  elsif options[:remote_url]
1279
1279
  credentials = Morpheus::Cli::Credentials.new(@appliance_name, @appliance_url)
1280
1280
  unless options[:skip_login]
1281
- @wallet = credentials.request_credentials(options, @do_save_credentials)
1281
+ @wallet = credentials.request_credentials(options.merge({dry_run:false}), @do_save_credentials)
1282
1282
  end
1283
1283
  else
1284
1284
  credentials = Morpheus::Cli::Credentials.new(@appliance_name, @appliance_url)
@@ -1293,7 +1293,7 @@ module Morpheus
1293
1293
 
1294
1294
  if @wallet.nil? || @wallet['access_token'].nil?
1295
1295
  unless options[:skip_login]
1296
- @wallet = credentials.request_credentials(options, @do_save_credentials)
1296
+ @wallet = credentials.request_credentials(options.merge({dry_run:false}), @do_save_credentials)
1297
1297
  end
1298
1298
  end
1299
1299
 
@@ -1767,7 +1767,22 @@ module Morpheus
1767
1767
  output = records.collect { |record|
1768
1768
  options[:select_fields].collect { |field|
1769
1769
  value = get_object_value(record, field)
1770
- value.is_a?(String) ? value : JSON.fast_generate(value)
1770
+ if value.is_a?(String)
1771
+ value
1772
+ else
1773
+ if options[:json]
1774
+ as_json(value, options)
1775
+ elsif options[:yaml]
1776
+ output = as_yaml(value, options)
1777
+ elsif options[:csv]
1778
+ as_csv(value, nil, options)
1779
+ else
1780
+ # default behavior
1781
+ # value.is_a?(String) ? value : as_json(value, options)
1782
+ do_pretty = options.key?(:pretty_json) ? options[:pretty_json] : false # or true?
1783
+ do_pretty ? JSON.pretty_generate(value) : JSON.fast_generate(value)
1784
+ end
1785
+ end
1771
1786
  }.join(options[:delim] || ",")
1772
1787
  }.join(options[:newline] || "\n")
1773
1788
  elsif options[:json]
@@ -7,7 +7,7 @@ class Morpheus::Cli::ApplianceSettingsCommand
7
7
  set_command_name :'appliance-settings'
8
8
 
9
9
  register_subcommands :get, :update, :toggle_maintenance, :'reindex'
10
-
10
+
11
11
  set_default_subcommand :get
12
12
 
13
13
  def connect(opts)
@@ -33,7 +33,7 @@ class Morpheus::Cli::ApplianceSettingsCommand
33
33
  raise_command_error "wrong number of arguments, expected 0 and got (#{args.count}) #{args}\n#{optparse}"
34
34
  return 1
35
35
  end
36
-
36
+
37
37
  begin
38
38
  @appliance_settings_interface.setopts(options)
39
39
 
@@ -220,6 +220,25 @@ class Morpheus::Cli::ApplianceSettingsCommand
220
220
  opts.on("--stats-retainment-period DAYS", Integer, "Stats retainment period. The number of days stats should be available. Can be 30, 60, or 90.") do |val|
221
221
  params['statsRetainmentPeriod'] = val.to_i
222
222
  end
223
+ opts.on("--cloud-sync-interval-seconds SECONDS", Integer, "Cloud sync interval in seconds") do |val|
224
+ params['cloudSyncIntervalSeconds'] = val.to_i
225
+ end
226
+ opts.on("--cluster-sync-interval-seconds SECONDS", Integer, "Cluster sync interval in seconds") do |val|
227
+ params['clusterSyncIntervalSeconds'] = val.to_i
228
+ end
229
+ opts.on("--usage-retainment-period DAYS", Integer, "Retainment period for usage records") do |val|
230
+ params['usageRetainmentPeriod'] = val.to_i
231
+ end
232
+ opts.on("--invoice-retainment-period DAYS", Integer, "Retainment period for invoice records") do |val|
233
+ params['invoiceRetainmentPeriod'] = val.to_i
234
+ end
235
+ opts.on("--max-option-list-size NUMBER", Integer, "Max option list size (x10^3)") do |val|
236
+ params['maxOptionListSize'] = val.to_i
237
+ end
238
+ opts.on("--default-locale STRING", String, "Default locale for the appliance") do |val|
239
+ params['defaultLocale'] = val == 'null' ? nil : val
240
+ end
241
+
223
242
  build_common_options(opts, options, [:json, :payload, :dry_run, :quiet, :remote])
224
243
  end
225
244
 
@@ -506,7 +506,7 @@ class Morpheus::Cli::Apps
506
506
  # prompt for cloudFormation config
507
507
  # todo
508
508
  else
509
- print yellow, "Unknown template type: #{template_type})", "\n"
509
+ print yellow, "Unknown template type: #{blueprint_type})", "\n"
510
510
  end
511
511
  end
512
512
  end
@@ -1183,7 +1183,7 @@ EOT
1183
1183
 
1184
1184
  # Details (zoneType.optionTypes)
1185
1185
 
1186
- if cloud_type && cloud_type['optionTypes']
1186
+ if cloud_type && cloud_type['optionTypes'] && cloud_type['code'] != 'standard'
1187
1187
  if !cloud_type['optionTypes'].find {|opt| opt['type'] == 'credential'}
1188
1188
  tmp_option_types << {'fieldName' => 'type', 'fieldLabel' => 'Credentials', 'type' => 'credential', 'optionSource' => 'credentials', 'required' => true, 'defaultValue' => 'local', 'config' => {'credentialTypes' => get_cloud_type_credential_types(cloud_type['code'])}, 'displayOrder' => 7}
1189
1189
  cloud_type['optionTypes'].select {|opt| ['username', 'password', 'serviceUsername', 'servicePassword'].include?(opt['fieldName'])}.each {|opt| opt['localCredential'] = true}
@@ -6,6 +6,7 @@ class Morpheus::Cli::Clusters
6
6
  include Morpheus::Cli::ProcessesHelper
7
7
  include Morpheus::Cli::WhoamiHelper
8
8
  include Morpheus::Cli::AccountsHelper
9
+ include Morpheus::Cli::ExecutionRequestHelper
9
10
 
10
11
  register_subcommands :list, :count, :get, :view, :add, :update, :remove, :logs, :history, {:'history-details' => :history_details}, {:'history-event' => :history_event_details}
11
12
  register_subcommands :list_workers, :add_worker, :remove_worker, :update_worker_count
@@ -607,7 +608,7 @@ class Morpheus::Cli::Clusters
607
608
  end
608
609
 
609
610
  # Controller type
610
- server_types = @server_types_interface.list({computeTypeId: cluster_type['controllerTypes'].first['id'], zoneTypeId: cloud['zoneType']['id'], useZoneProvisionTypes: true})['serverTypes'].reject {|it| it['provisionType']['code'] == 'manual'}
611
+ server_types = @server_types_interface.list({computeTypeId: cluster_type['controllerTypes'].first['id'], zoneTypeId: cloud['zoneType']['id'], useZoneProvisionTypes: true})['serverTypes'].reject {|it| it['provisionType']['code'] == 'manual'} unless ['kubernetes-cluster', 'mvm-cluster'].include?(cluster_type_code)
611
612
  controller_provision_type = nil
612
613
  resource_pool = nil
613
614
 
@@ -802,6 +803,9 @@ class Morpheus::Cli::Clusters
802
803
  opts.on('--managed [on|off]', String, "Can be used to enable / disable managed cluster. Default is on") do |val|
803
804
  options[:managed] = val.to_s == 'on' || val.to_s == 'true' || val.to_s == '1' || val.to_s == ''
804
805
  end
806
+ opts.on('--useAgent [on|off]', String, "Use the Agent to relay communications for the Kubernetes API instead of direct.") do |val|
807
+ options[:useAgent] = val.to_s
808
+ end
805
809
  opts.on('--autoRecoverPowerState [on|off]', String, "Automatically Power On VMs") do |val|
806
810
  options[:autoRecoverPowerState] = val.to_s == 'on' || val.to_s == 'true' || val.to_s == '1' || val.to_s == ''
807
811
  end
@@ -855,6 +859,7 @@ class Morpheus::Cli::Clusters
855
859
  cluster_payload['refresh'] = options[:refresh] if options[:refresh] == true
856
860
  cluster_payload['tenant'] = options[:tenant] if !options[:tenant].nil?
857
861
  cluster_payload['integrations'] = options[:integrations] if !options[:integrations].nil?
862
+ cluster_payload['useAgent'] = options[:useAgent] if !options[:useAgent].nil?
858
863
  payload = {"cluster" => cluster_payload}
859
864
  end
860
865
 
@@ -3004,6 +3009,12 @@ class Morpheus::Cli::Clusters
3004
3009
  options = {}
3005
3010
  optparse = Morpheus::Cli::OptionParser.new do |opts|
3006
3011
  opts.banner = subcommand_usage( "[cluster] [options]")
3012
+ opts.on('--refresh [SECONDS]', String, "Refresh until execution is complete. Default interval is #{default_refresh_interval} seconds.") do |val|
3013
+ options[:refresh_interval] = val.to_s.empty? ? default_refresh_interval : val.to_f
3014
+ end
3015
+ opts.on(nil, '--no-refresh', "Do not refresh" ) do
3016
+ options[:no_refresh] = true
3017
+ end
3007
3018
  build_option_type_options(opts, options, add_datastore_option_types)
3008
3019
  build_common_options(opts, options, [:options, :payload, :json, :dry_run, :remote])
3009
3020
  opts.footer = "Add datastore to a cluster.\n" +
@@ -3059,11 +3070,19 @@ class Morpheus::Cli::Clusters
3059
3070
  json_response = @clusters_interface.create_datastore(cluster['id'], payload)
3060
3071
  if options[:json]
3061
3072
  puts as_json(json_response)
3062
- elsif json_response['success']
3063
- if json_response['msg'] == nil
3064
- print_green_success "Added datastore to cluster #{cluster['name']}"
3073
+ else
3074
+ if json_response['success']
3075
+ if json_response['msg'] == nil
3076
+ print_green_success "Adding datastore to cluster #{cluster['name']}"
3077
+ else
3078
+ print_green_success json_response['msg']
3079
+ end
3080
+ execution_id = json_response['executionId']
3081
+ if !options[:no_refresh] && execution_id
3082
+ wait_for_execution_request(json_response['executionId'], options.merge({waiting_status:['new', 'pending', 'executing']}))
3083
+ end
3065
3084
  else
3066
- print_green_success json_response['msg']
3085
+ print_red_alert "Failed to create cluster datastore #{json_response['msg']}"
3067
3086
  end
3068
3087
  end
3069
3088
  return 0
@@ -3074,6 +3093,7 @@ class Morpheus::Cli::Clusters
3074
3093
  end
3075
3094
 
3076
3095
  def remove_datastore(args)
3096
+ default_refresh_interval = 5
3077
3097
  params = {}
3078
3098
  options = {}
3079
3099
  optparse = Morpheus::Cli::OptionParser.new do |opts|
@@ -3081,6 +3101,12 @@ class Morpheus::Cli::Clusters
3081
3101
  opts.on( '-f', '--force', "Force Delete" ) do
3082
3102
  params[:force] = 'on'
3083
3103
  end
3104
+ opts.on('--refresh [SECONDS]', String, "Refresh until execution is complete. Default interval is #{default_refresh_interval} seconds.") do |val|
3105
+ options[:refresh_interval] = val.to_s.empty? ? default_refresh_interval : val.to_f
3106
+ end
3107
+ opts.on(nil, '--no-refresh', "Do not refresh" ) do
3108
+ options[:no_refresh] = true
3109
+ end
3084
3110
  build_standard_remove_options(opts, options)
3085
3111
  opts.footer = "Delete a datastore from a cluster.\n" +
3086
3112
  "[cluster] is required. This is the name or id of an existing cluster.\n" +
@@ -3114,12 +3140,18 @@ class Morpheus::Cli::Clusters
3114
3140
  return
3115
3141
  end
3116
3142
  json_response = @clusters_interface.destroy_datastore(cluster['id'], datastore['id'], params)
3117
- render_response(json_response, options) do
3118
- msg = "Datastore #{datastore['name']} is being removed from cluster #{cluster['name']}..."
3119
- if json_response['msg']
3120
- msg = json_response['msg']
3143
+ if options[:json]
3144
+ puts as_json(json_response)
3145
+ else
3146
+ if json_response['success']
3147
+ print_green_success "Datastore #{datastore['name']} is being removed from cluster #{cluster['name']}"
3148
+ execution_id = json_response['executionId']
3149
+ if !options[:no_refresh] && execution_id
3150
+ wait_for_execution_request(execution_id, options.merge({waiting_status:['new', 'pending', 'executing']}))
3151
+ end
3152
+ else
3153
+ print_red_alert "Failed to remove cluster datastore #{json_response['msg']}"
3121
3154
  end
3122
- print_green_success msg
3123
3155
  end
3124
3156
  return 0, nil
3125
3157
  end
@@ -4535,7 +4567,12 @@ class Morpheus::Cli::Clusters
4535
4567
  elsif resource_pool_options.count > 1 && !options[:no_prompt]
4536
4568
  resource_pool_id = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'resourcePool', 'type' => 'select', 'fieldLabel' => 'Resource Pool', 'selectOptions' => resource_pool_options, 'required' => true, 'skipSingleOption' => true, 'description' => 'Select resource pool.'}],options[:options],api_client, {})['resourcePool']
4537
4569
  else
4538
- resource_pool_id = resource_pool_options.first['id']
4570
+ first_option = resource_pool_options.find {|it| !it['id'].nil? }
4571
+ if first_option.nil?
4572
+ print_red_alert "Cloud #{cloud['name']} has no available resource pools"
4573
+ exit 1
4574
+ end
4575
+ resource_pool_id = first_option['id']
4539
4576
  end
4540
4577
  if resource_pool_id.to_s["poolGroup-"]
4541
4578
  resource_pool = @resource_pool_groups_interface.get(resource_pool_id)['resourcePoolGroup']
@@ -1337,14 +1337,20 @@ class Morpheus::Cli::Hosts
1337
1337
  payload[:server][:plan] = {id: service_plan["id"]}
1338
1338
 
1339
1339
  # fetch volumes
1340
- volumes_response = @servers_interface.volumes(server['id'])
1341
- current_volumes = volumes_response['volumes'].sort {|x,y| x['displayOrder'] <=> y['displayOrder'] }
1340
+ current_volumes = nil
1341
+ if server['volumes']
1342
+ current_volumes = server['volumes'].sort {|x,y| x['displayOrder'] <=> y['displayOrder'] }
1343
+ else
1344
+ volumes_response = @servers_interface.volumes(server['id'])
1345
+ current_volumes = volumes_response['volumes'].sort {|x,y| x['displayOrder'] <=> y['displayOrder'] }
1346
+ end
1342
1347
 
1343
1348
  # prompt for volumes
1344
1349
  vol_options = options
1345
1350
  vol_options['siteId'] = group_id
1346
1351
  vol_options['zoneId'] = cloud_id
1347
- volumes = prompt_resize_volumes(current_volumes, service_plan, provision_type, vol_options)
1352
+ vol_options['resourcePoolId'] = server['resourcePool']['id'] if server['resourcePool']
1353
+ volumes = prompt_resize_volumes(current_volumes, service_plan, provision_type, vol_options, server)
1348
1354
  if !volumes.empty?
1349
1355
  payload[:volumes] = volumes
1350
1356
  end
@@ -2756,7 +2756,10 @@ class Morpheus::Cli::Instances
2756
2756
  connect(options)
2757
2757
  instance = find_instance_by_name_or_id(args[0])
2758
2758
  return 1, "instance not found" if instance.nil?
2759
-
2759
+ # need to load full instance details in case fetched by name
2760
+ if instance['containerDetails'].nil?
2761
+ instance = find_instance_by_id(instance['id'])
2762
+ end
2760
2763
  payload = {}
2761
2764
  if options[:payload]
2762
2765
  payload = options[:payload]
@@ -2811,13 +2814,24 @@ class Morpheus::Cli::Instances
2811
2814
  payload["instance"]["plan"] = {"id" => service_plan["id"]}
2812
2815
 
2813
2816
  volumes_response = @instances_interface.volumes(instance['id'])
2814
- current_volumes = volumes_response['volumes'].sort {|x,y| x['displayOrder'] <=> y['displayOrder'] }
2817
+ current_volumes = nil
2815
2818
 
2816
2819
  # prompt for volumes
2817
2820
  vol_options = options
2818
2821
  vol_options['siteId'] = group_id
2819
2822
  vol_options['zoneId'] = cloud_id
2820
- volumes = prompt_resize_volumes(current_volumes, service_plan, provision_type, vol_options)
2823
+ vol_options['resourcePoolId'] = resource_pool_id # server['resourcePool']['id'] if server['resourcePool']
2824
+ server = instance['containerDetails'][0]['server'] rescue nil
2825
+ if server.nil?
2826
+ Morpheus::Logging::DarkPrinter.puts "Failed to load server info"
2827
+ volumes_response = @instances_interface.volumes(instance['id'])
2828
+ current_volumes = volumes_response['volumes'].sort {|x,y| x['displayOrder'] <=> y['displayOrder'] }
2829
+ else
2830
+ # or just use instance['volumes'] ?
2831
+ current_volumes = server['volumes'].sort {|x,y| x['displayOrder'] <=> y['displayOrder'] }
2832
+ end
2833
+ vol_options['resourcePoolId'] = server['resourcePool']['id'] if server && server['resourcePool']
2834
+ volumes = prompt_resize_volumes(current_volumes, service_plan, provision_type, vol_options, server)
2821
2835
  if !volumes.empty?
2822
2836
  payload["volumes"] = volumes
2823
2837
  end
@@ -845,7 +845,7 @@ class Morpheus::Cli::LibraryClusterLayoutsCommand
845
845
  end
846
846
 
847
847
  def find_layout_by_name(name)
848
- layouts = @library_cluster_layouts_interface.list(instance_type_id, {name: name.to_s})['layouts']
848
+ layouts = @library_cluster_layouts_interface.list({name: name.to_s})['layouts']
849
849
  if layouts.empty?
850
850
  print_red_alert "Cluster layout not found by name #{name}"
851
851
  return nil
@@ -635,8 +635,8 @@ class Morpheus::Cli::Shell
635
635
  # @return Hash like {:commands => [], :command_count => total}
636
636
  def load_history_commands(options={})
637
637
  phrase = options[:phrase]
638
- sort_key = options[:sort] ? options[:sort].to_sym : nil
639
-
638
+ #sort_key = options[:sort] ? options[:sort].to_sym : nil
639
+ #direction = options[:direction] # default sort is reversed to get newest first
640
640
  offset = options[:offset].to_i > 0 ? options[:offset].to_i : 0
641
641
  max = options[:max].to_i > 0 ? options[:max].to_i : 25
642
642
 
@@ -648,40 +648,21 @@ class Morpheus::Cli::Shell
648
648
  # collect records as [{:number => 1, :command => "instances list"}, etc]
649
649
  history_records = []
650
650
  history_count = 0
651
-
652
- # sort is a bit different for this command, the default sort is by number
653
- # it sorts oldest -> newest, but shows the very last page by default.
654
- if options[:sort] && ![:number, :command].include?(options[:sort])
655
- sort_key = :number # nil
656
- end
657
651
 
658
- if options[:phrase] || sort_key || options[:direction] || options[:offset]
659
- # this could be a large object...need to index our shell_history file lol
660
- sort_key ||= :number
661
- history_records = @history.keys.collect { |k| {number: k, command: @history[k]} }
662
- if options[:direction] == 'desc'
663
- history_records = history_records.sort {|x,y| y[sort_key] <=> x[sort_key] }
664
- else
665
- history_records = history_records.sort {|x,y| x[sort_key] <=> y[sort_key] }
666
- end
667
- if phrase
668
- history_records = history_records.select {|it| it[:command].include?(phrase) || it[:number].to_s == phrase }
669
- end
670
- command_count = history_records.size
671
- history_records = history_records[offset..(max-1)]
672
- else
673
- # default should try to be as fast as possible..
674
- # no searching or sorting, default order by :number works
675
- if offset != 0
676
- cmd_numbers = @history.keys.last(max + offset).first(max)
677
- else
678
- cmd_numbers = @history.keys.last(max)
679
- end
680
- history_records = cmd_numbers.collect { |k| {number: k, command: @history[k]} }
681
- command_count = @history.size
652
+ # only go so far back in command history, 1 million commands
653
+ # this could be a large object...need to index our shell_history file lol
654
+ # todo: this max needs to be done in load_history_from_log_file()
655
+ history_keys = @history.keys.last(1000000).reverse
656
+ # filter by phrase
657
+ if phrase
658
+ history_keys = history_keys.select {|k| (@history[k] && @history[k].include?(phrase)) || k.to_s == phrase }
682
659
  end
660
+ # no offset, just max
661
+ history_records = history_keys.first(max).collect { |k| {number: k, command: @history[k]} }
662
+ command_count = history_keys.size
663
+
683
664
  meta = {size:history_records.size, total:command_count.to_i, max:max, offset:offset}
684
- return {:commands => history_records, :command_count => command_count, :meta => meta}
665
+ return {commands: history_records, command_count: command_count, meta: meta}
685
666
  end
686
667
 
687
668
  def print_history(options={})
@@ -696,16 +677,14 @@ class Morpheus::Cli::Shell
696
677
  end
697
678
  else
698
679
  print cyan
680
+ # by default show old->new as the shell history should bash history
681
+ history_records.reverse! unless options[:direction] == "desc"
699
682
  history_records.each do |history_record|
700
683
  puts "#{history_record[:number].to_s.rjust(3, ' ')} #{history_record[:command]}"
701
684
  end
702
685
  if options[:show_pagination]
703
- if options[:phrase] || options[:sort] || options[:direction] || options[:offset]
704
- print_results_pagination(history_result[:meta])
705
- else
706
- # default order is weird, it's the last page of results, 1-25 is misleading and showing the indexes is stranger
707
- print_results_pagination(history_result[:meta], {:message =>"Viewing most recent %{size} of %{total} %{label}"})
708
- end
686
+ pagination_msg = options[:phrase] ? "Viewing most recent %{size} of %{total} commands matching '#{options[:phrase]}'" : "Viewing most recent %{size} of %{total} commands"
687
+ print_results_pagination(history_result[:meta], {:message =>pagination_msg})
709
688
  print reset, "\n"
710
689
  else
711
690
  print reset
@@ -6,7 +6,7 @@ class Morpheus::Cli::VirtualImages
6
6
  include Morpheus::Cli::CliCommand
7
7
  include Morpheus::Cli::ProvisioningHelper
8
8
 
9
- register_subcommands :list, :get, :add, :add_file, :remove_file, :update, :remove, :types => :virtual_image_types
9
+ register_subcommands :list, :get, :add, :add_file, :remove_file, :update, :remove, :convert, :types => :virtual_image_types
10
10
  register_subcommands :list_locations, :get_location, :remove_location
11
11
 
12
12
  # def initialize()
@@ -172,11 +172,8 @@ EOT
172
172
  id
173
173
  else
174
174
  image = find_virtual_image_by_name_or_id(id)
175
- if image
176
- image['id']
177
- else
178
- raise_command_error "virtual image not found for name '#{id}'"
179
- end
175
+ return 1, "virtual image not found for name '#{id}'" if image.nil?
176
+ image['id']
180
177
  end
181
178
  end
182
179
  return run_command_for_each_arg(id_list) do |arg|
@@ -302,7 +299,7 @@ EOT
302
299
  options = {}
303
300
  tenants_list = nil
304
301
  optparse = Morpheus::Cli::OptionParser.new do |opts|
305
- opts.banner = subcommand_usage("[name] [options]")
302
+ opts.banner = subcommand_usage("[image] [options]")
306
303
  opts.on('--tenants LIST', Array, "Tenant Access, comma separated list of account IDs") do |list|
307
304
  if list.size == 1 && list[0] == 'null' # hacky way to clear it
308
305
  tenants_list = []
@@ -324,7 +321,7 @@ EOT
324
321
  end
325
322
  build_common_options(opts, options, [:options, :payload, :json, :dry_run, :remote])
326
323
  opts.footer = "Update a virtual image." + "\n" +
327
- "[name] is required. This is the name or id of a virtual image."
324
+ "[image] is required. This is the name or id of a virtual image."
328
325
  end
329
326
  optparse.parse!(args)
330
327
  verify_args!(args:args, optparse:optparse, count:1)
@@ -377,6 +374,68 @@ EOT
377
374
 
378
375
  end
379
376
 
377
+ def convert(args)
378
+ image_name = args[0]
379
+ options = {}
380
+ # storageProviderId, format, name
381
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
382
+ opts.banner = subcommand_usage("[image] [options]")
383
+ # opts.on('-n', '--name NAME', String, "Name (optional) of the new converted image. Default is name of the original image.") do |val|
384
+ # options[:options]['name'] = val
385
+ # end
386
+ # opts.on('-f', '--format FORMAT', String, "Format (optional). Default is 'qcow2'") do |val|
387
+ # options[:options]['format'] = val
388
+ # end
389
+ # opts.on('--storageProvider VALUE', String, "Storage Provider ID (optional). Default is storage provider of the original image.") do |val|
390
+ # options[:options]['storageProvider'] = val.to_s
391
+ # end
392
+ build_option_type_options(opts, options, convert_virtual_image_option_types)
393
+ build_common_options(opts, options, [:options, :payload, :json, :dry_run, :remote])
394
+ opts.footer = "Convert a virtual image to a new format." + "\n" +
395
+ "[image] is required. This is the name or id of a virtual image."
396
+ end
397
+ optparse.parse!(args)
398
+ verify_args!(args:args, optparse:optparse, min:1, max:4)
399
+
400
+ connect(options)
401
+
402
+ virtual_image = find_virtual_image_by_name_or_id(image_name)
403
+ return 1, "Virtual image not found for #{image_name}" if virtual_image.nil?
404
+
405
+ passed_options = parse_passed_options(options)
406
+ payload = nil
407
+ if options[:payload]
408
+ payload = options[:payload]
409
+ payload.deep_merge!({virtual_image_object_key => passed_options}) unless passed_options.empty?
410
+ else
411
+ virtual_image_payload = passed_options
412
+ source_format = virtual_image['imageType']
413
+ v_prompt = Morpheus::Cli::OptionTypes.prompt(convert_virtual_image_option_types(source_format), options[:options], @api_client, {'virtualImageId': virtual_image['id']})
414
+ v_prompt.deep_compact!
415
+ virtual_image_payload.deep_merge!(v_prompt)
416
+ # support "storageProviderId" too
417
+ if virtual_image_payload['storageProviderId']
418
+ virtual_image_payload['storageProvider'] = virtual_image_payload.delete('storageProviderId')
419
+ end
420
+ # convert "storageProvider":1 to "storageProvider": {"id":1}
421
+ if virtual_image_payload['storageProvider'] && !virtual_image_payload['storageProvider'].is_a?(Hash)
422
+ virtual_image_payload['storageProvider'] = {'id' => virtual_image_payload['storageProvider'].to_i }
423
+ end
424
+ payload = virtual_image_payload
425
+ end
426
+ @virtual_images_interface.setopts(options)
427
+ if options[:dry_run]
428
+ print_dry_run @virtual_images_interface.dry.convert(virtual_image['id'], payload)
429
+ return
430
+ end
431
+ json_response = @virtual_images_interface.convert(virtual_image['id'], payload)
432
+ render_response(json_response, options, 'virtualImage') do
433
+ print_green_success "Updated virtual image #{virtual_image['name']}"
434
+ _get(virtual_image["id"], {}, options)
435
+ end
436
+ return 0, nil
437
+ end
438
+
380
439
  def virtual_image_types(args)
381
440
  options = {}
382
441
  optparse = Morpheus::Cli::OptionParser.new do |opts|
@@ -621,7 +680,7 @@ EOT
621
680
  do_gzip = false
622
681
  options = {}
623
682
  optparse = Morpheus::Cli::OptionParser.new do |opts|
624
- opts.banner = subcommand_usage("[name] [filepath]")
683
+ opts.banner = subcommand_usage("[image] [filepath]")
625
684
  opts.on('--filename FILENAME', String, "Filename for uploaded file. Derived from [filepath] by default." ) do |val|
626
685
  file_name = val
627
686
  end
@@ -633,7 +692,7 @@ EOT
633
692
  end
634
693
  build_common_options(opts, options, [:json, :dry_run, :quiet, :remote])
635
694
  opts.footer = "Upload a virtual image file." + "\n" +
636
- "[name] is required. This is the name or id of a virtual image." + "\n" +
695
+ "[image] is required. This is the name or id of a virtual image." + "\n" +
637
696
  "[filepath] or --url is required. This is location of the virtual image file."
638
697
  end
639
698
  optparse.parse!(args)
@@ -700,7 +759,7 @@ EOT
700
759
  def remove_file(args)
701
760
  options = {}
702
761
  optparse = Morpheus::Cli::OptionParser.new do |opts|
703
- opts.banner = subcommand_usage("[name] [filename]")
762
+ opts.banner = subcommand_usage("[image] [filename]")
704
763
  build_common_options(opts, options, [:auto_confirm, :json, :dry_run, :remote])
705
764
  end
706
765
  optparse.parse!(args)
@@ -940,11 +999,20 @@ EOT
940
999
 
941
1000
  def find_virtual_image_by_name(name)
942
1001
  json_results = @virtual_images_interface.list({name: name.to_s})
943
- if json_results['virtualImages'].empty?
1002
+ records = json_results['virtualImages']
1003
+ if records.empty?
944
1004
  print_red_alert "Virtual Image not found by name #{name}"
945
1005
  return nil
1006
+ elsif records.size > 1
1007
+ print_red_alert "More than one Virtual Image found by name '#{name}'"
1008
+ print_error "\n"
1009
+ puts_error as_pretty_table(records, [:id, :name], {color:red})
1010
+ print_red_alert "Try using ID instead"
1011
+ print_error reset,"\n"
1012
+ return nil
1013
+ else
1014
+ virtual_image = records[0]
946
1015
  end
947
- virtual_image = json_results['virtualImages'][0]
948
1016
  return virtual_image
949
1017
  end
950
1018
 
@@ -1024,6 +1092,42 @@ EOT
1024
1092
  list
1025
1093
  end
1026
1094
 
1095
+ def convert_virtual_image_option_types(source_format=nil)
1096
+ [
1097
+ {'shorthand' => '-f', 'fieldName' => 'format', 'type' => 'select', 'fieldLabel' => 'Format', 'selectOptions' => get_convert_image_formats(source_format), 'required' => true, 'defaultValue' => (source_format == 'qcow2' ? nil : 'qcow2')},
1098
+ {'shorthand' => '-b', 'fieldName' => 'storageProviderId', 'type' => 'select', 'fieldLabel' => 'Bucket', 'optionSource' => 'storageProviders', 'required' => false, 'description' => 'Select Storage Provider Bucket or File Share.'},
1099
+ {'shorthand' => '-n', 'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => false}
1100
+ ]
1101
+ end
1102
+
1103
+ def convert_source_types
1104
+ ["raw","qcow2","vmdk",'vmware', "vhd","ovf"]
1105
+ end
1106
+
1107
+ def convert_destination_types
1108
+ #["raw", "qcow2", "vmdk",'vmware',"vhd","ovf"]
1109
+ convert_source_types
1110
+ end
1111
+
1112
+ def get_convert_image_formats(source_format=nil)
1113
+ categories = [
1114
+ {'name' => 'QCOW2', 'value' => 'qcow2'},
1115
+ {'name' => 'VMDK', 'value' => 'ovf'},
1116
+ {'name' => 'VHD', 'value' => 'vhd'},
1117
+ {'name' => 'Raw', 'value' => 'raw'}
1118
+ ]
1119
+ # if source format (imageType) is not in the list, then no options
1120
+ # else reject the current value
1121
+ if source_format
1122
+ if !convert_source_types.include?(source_format.to_s.downcase)
1123
+ categories = []
1124
+ else
1125
+ categories = categories.select {|it| it['value'] != source_format.to_s.downcase }
1126
+ end
1127
+ end
1128
+ return categories
1129
+ end
1130
+
1027
1131
  def format_tenants(accounts)
1028
1132
  if accounts && accounts.size > 0
1029
1133
  accounts = accounts.sort {|a,b| a['name'] <=> b['name'] }.uniq {|it| it['id'] }