morpheus-cli 6.3.2 → 6.3.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0190e52b071314ef69df636c03de5f81f01dabe1e8314ea96d52d6877b95f0b2'
4
- data.tar.gz: '098aaf12c061b7bd684956750d14c5f6093e3b8a6dd66d558d510834af6ebcd0'
3
+ metadata.gz: 044ab9d85780cd6e8c58298dd7eea8c389093c0c40f97e444c1050ecb9447d65
4
+ data.tar.gz: 54d733790244eea47f2310d0211dc4173ff26a89b9cde962cc64627f1ea8a68a
5
5
  SHA512:
6
- metadata.gz: '0853e9c37a2ac7f3b96d4b581c223ed0f729eaf299698415dfa821199b83269afb92fe55ee081b7d78c2bc8cea40ea98852172d6c56a4fce85bea4ac8af18e8b'
7
- data.tar.gz: a5bf1ce5b619a150debf9b6558bc758208e1bd31c30fbfdbe3d3fc8d9bc1ea752294b49462e9f42dacf7e4c8a14fe9f4d191b565662fd1334a586b348a5b60e5
6
+ metadata.gz: 527cceb963b7b673ec478a1717cf39b0481a295a59026430944621d6e9133044e6296107d90c336750ff5413599032379e0d012285a23a63e1ecd1287558a863
7
+ data.tar.gz: a160e2dc2676bcaaded2318268f72df801b9c6514293ba6c1ce59c54cc6803221e04885066d961bdb78dbcfa49855236f79ab24727685d35442e40dda5933ad5
data/Dockerfile CHANGED
@@ -1,5 +1,5 @@
1
1
  FROM ruby:2.7.5
2
2
 
3
- RUN gem install morpheus-cli -v 6.3.2
3
+ RUN gem install morpheus-cli -v 6.3.4
4
4
 
5
5
  ENTRYPOINT ["morpheus"]
@@ -894,6 +894,10 @@ class Morpheus::APIClient
894
894
  Morpheus::BackupServiceTypesInterface.new(common_interface_options).setopts(@options)
895
895
  end
896
896
 
897
+ def backup_types
898
+ Morpheus::BackupTypesInterface.new(common_interface_options).setopts(@options)
899
+ end
900
+
897
901
  def catalog_item_types
898
902
  Morpheus::CatalogItemTypesInterface.new(common_interface_options).setopts(@options)
899
903
  end
@@ -15,6 +15,10 @@ class Morpheus::BackupRestoresInterface < Morpheus::APIClient
15
15
  execute(method: :get, url: "#{base_path}/#{CGI::escape(id.to_s)}", params: params, headers: headers)
16
16
  end
17
17
 
18
+ def create(payload, params={}, headers={})
19
+ execute(method: :post, url: "#{base_path}", params: params, payload: payload, headers: headers)
20
+ end
21
+
18
22
  def destroy(id, params = {}, headers={})
19
23
  validate_id!(id)
20
24
  execute(method: :delete, url: "#{base_path}/#{CGI::escape(id.to_s)}", params: params, headers: headers)
@@ -0,0 +1,9 @@
1
+ require 'morpheus/api/read_interface'
2
+
3
+ class Morpheus::BackupTypesInterface < Morpheus::ReadInterface
4
+
5
+ def base_path
6
+ "/api/backup-types"
7
+ end
8
+
9
+ end
@@ -200,6 +200,9 @@ module Morpheus
200
200
  # value_label = 'SELECT'
201
201
  # elsif option['type'] == 'select'
202
202
  end
203
+ if option_type['optionalValue']
204
+ value_label = "[#{value_label}]"
205
+ end
203
206
  full_option = "--#{full_field_name} #{value_label}"
204
207
  # switch is an alias for the full option name, fieldName is the default
205
208
  if option_type['switch']
@@ -563,7 +566,7 @@ module Morpheus
563
566
  # added with :payload too... just need it here to avoid unknown key error
564
567
  # todo: remove this when every command supporting :payload is updated to use parse_payload(options) and execute_api(options)
565
568
  when :list
566
- opts.on( '-m', '--max MAX', "Max Results (use -1 for all results)" ) do |val|
569
+ opts.on( '-m', '--max MAX', "Max Results" ) do |val|
567
570
  # api supports max=-1 for all at the moment..
568
571
  if val.to_s == "all" || val.to_s == "-1"
569
572
  options[:max] = "-1"
@@ -0,0 +1,53 @@
1
+ require 'morpheus/cli/cli_command'
2
+
3
+ class Morpheus::Cli::BackupTypes
4
+ include Morpheus::Cli::CliCommand
5
+ include Morpheus::Cli::RestCommand
6
+
7
+ set_command_description "View backup types."
8
+ set_command_name :'backup-types'
9
+ register_subcommands :list, :get
10
+ register_interfaces :backup_types
11
+
12
+ # This is a hidden command, could move to backup list-types and backup get-type
13
+ set_command_hidden
14
+
15
+ protected
16
+
17
+ def backup_type_list_column_definitions(options)
18
+ {
19
+ "ID" => 'id',
20
+ "Name" => 'name',
21
+ "Code" => 'code',
22
+ # "Active" => lambda {|it| format_boolean it['active'] },
23
+ # "Provider Code" => 'providerCode',
24
+ }
25
+ end
26
+
27
+ def backup_type_column_definitions(options)
28
+ {
29
+ "ID" => 'id',
30
+ "Name" => 'name',
31
+ "Code" => 'code',
32
+ #"Backup Format" => 'backupFormat',
33
+ "Active" => lambda {|it| format_boolean it['active'] },
34
+ # "Container Type" => 'containerType',
35
+ # "Container Format" => 'containerFormat',
36
+ # "Container Category" => 'containerCategory',
37
+ "Restore Type" => 'restoreType',
38
+ # "Has Stream To Store" => lambda {|it| format_boolean it['hasStreamToStore'] },
39
+ # "Has Copy To Store" => lambda {|it| format_boolean it['hasCopyToStore'] },
40
+ "Download" => lambda {|it| format_boolean it['downloadEnabled'] },
41
+ # "Download From Store Only" => lambda {|it| format_boolean it['downloadFromStoreOnly'] },
42
+ # "Copy To Store" => lambda {|it| format_boolean it['copyToStore'] },
43
+ "Restore Existing" => lambda {|it| format_boolean it['restoreExistingEnabled'] },
44
+ "Restore New" => lambda {|it| format_boolean it['restoreNewEnabled'] },
45
+ # "Restore New Mode" => 'restoreNewMode',
46
+ # "Prune Results On Restore Existing" => lambda {|it| format_boolean it['pruneResultsOnRestoreExisting'] },
47
+ # "Restrict Targets" => lambda {|it| format_boolean it['restrictTargets'] },
48
+ # "Provider Code" => 'providerCode',
49
+ "Plugin" => lambda {|it| format_boolean it['isPlugin'] },
50
+ "Embedded" => lambda {|it| format_boolean it['isEmbedded'] },
51
+ }
52
+ end
53
+ end
@@ -9,7 +9,7 @@ class Morpheus::Cli::BackupsCommand
9
9
 
10
10
  set_command_description "View and manage backups"
11
11
  set_command_name :'backups'
12
- register_subcommands :list, :get, :add, :update, :remove, :execute #, :restore
12
+ register_subcommands :list, :get, :add, :update, :remove, :execute, :restore
13
13
  register_subcommands :list_jobs, :get_job, :add_job, :update_job, :remove_job, :execute_job
14
14
  register_subcommands :list_results, :get_result, :remove_result
15
15
  register_subcommands :list_restores, :get_restore, :remove_restore
@@ -45,7 +45,7 @@ class Morpheus::Cli::BackupsCommand
45
45
  end
46
46
  params.merge!(parse_list_options(options))
47
47
  parse_options(options, params)
48
- execute_api(@backups_interface, :list, [], options, 'backup') do |json_response|
48
+ execute_api(@backups_interface, :list, [], options, 'backups') do |json_response|
49
49
  backups = json_response['backups']
50
50
  print_h1 "Morpheus Backups", parse_list_subtitles(options), options
51
51
  if backups.empty?
@@ -315,19 +315,18 @@ EOT
315
315
  end
316
316
 
317
317
  def restore(args)
318
- raise "Not Yet Implemented"
319
318
  options = {}
320
319
  params = {}
321
320
  payload = {}
322
321
  optparse = Morpheus::Cli::OptionParser.new do |opts|
323
322
  opts.banner = subcommand_usage("[backup] [result] [options]")
324
- build_standard_post_options(opts, options)
323
+ build_standard_post_options(opts, options, [:auto_confirm])
325
324
  opts.on('--result ID', String, "Backup Result ID that is being restored") do |val|
326
325
  options[:options]['backupResultId'] = val
327
326
  end
328
327
  opts.on('--restore-instance existing|new', String, "Instance being targeted for the restore, existing to restore the current instance or new to create a new instance. The current instance is targeted by default.") do |val|
329
- # restoreInstanceSelect=current|new and the flag on the restore object is called 'restoreToNew'
330
- options[:options]['restoreInstanceSelect'] = val
328
+ # restoreInstance=existing|new and the flag on the restore object is called 'restoreToNew'
329
+ options[:options]['restoreInstance'] = val
331
330
  end
332
331
  opts.footer = <<-EOT
333
332
  Restore a backup, replacing the existing target with the specified backup result.
@@ -350,32 +349,71 @@ EOT
350
349
  available_backups = @backups_interface.list({max:10000})['backups'].collect {|it| {'name' => it['name'], 'value' => it['id']}}
351
350
  backup_id = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'backupId', 'fieldLabel' => 'Backup', 'type' => 'select', 'selectOptions' => available_backups, 'required' => true}], options[:options], @api_client)['backupId']
352
351
  backup = find_backup_by_name_or_id(backup_id)
353
- return 1 if backup.nil?
352
+ return 1 if backup.nil?
354
353
  end
355
354
  end
356
355
  # Prompt for backup result
357
356
  if backup_result.nil?
358
-
359
- # Instance
360
- available_backup_results = @backups_interface.list({backupId: backup['id'], max:10000})['results'].collect {|it| {'name' => it['name'], 'value' => it['id']}}
361
- params['backupResultId'] = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'backupResultId', 'fieldLabel' => 'Backup Result', 'type' => 'select', 'selectOptions' => available_backup_results, 'required' => true}], options[:options], @api_client)['backupResultId']
362
- # Name
363
- params['name'] = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true, 'description' => 'Backup Name'}], options[:options], @api_client)['name']
357
+ #available_backup_results = @backup_results_interface.list({backupId: backup['id'], status: ['success', 'succeeded'], max:10000})['results'].collect {|it| {format_backup_result_option_name(it), 'value' => it['id']}}
358
+ available_backup_results = @backup_results_interface.list({backupId: backup['id'], max:10000})['results'].select {|it| it['status'].to_s.downcase == 'succeeded' || it['status'].to_s.downcase == 'success' }.collect {|it| {'name' => format_backup_result_option_name(it), 'value' => it['id']} }
359
+ params['backupResultId'] = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'backupResultId', 'fieldLabel' => 'Backup Result', 'type' => 'select', 'selectOptions' => available_backup_results, 'required' => true}], options[:options], @api_client)['backupResultId']
360
+ backup_result = @backup_results_interface.get(params['backupResultId'].to_i)['result']
364
361
  end
365
-
366
362
  parse_payload(options, 'restore') do |payload|
367
363
  # Prompt for restore configuration
368
- # We should probably require identifying the instance by name or id too, just to be safe.
364
+ # todo: These options should be based on backup type
365
+ # Look at backup_type['restoreExistingEnabled'] and backup_type['restoreNewEnabled']
369
366
  # Target Instance
370
- if backup_result['instance']
371
- params['restoreInstanceSelect'] = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true, 'description' => 'Instance being targeted for the restore, existing to restore the current instance or new to create a new instance. By default the existing instance is restored.'}], options[:options], @api_client)['restoreInstanceSelect']
367
+ #if backup_result['instanceId']
368
+ if backup['locationType'] == 'instance'
369
+ instance = backup['instance']
370
+ # could actually fetch the instance.., only need name and id right now though.
371
+ raise_command_error "Backup instance not found" if instance.nil?
372
+ params['restoreInstance'] = prompt_value({'fieldName' => 'restoreInstance', 'fieldLabel' => 'Restore Instance', 'type' => 'select', 'selectOptions' => [{'name' => 'Current Instance', 'value' => 'existing'}, {'name' => 'New Instance', 'value' => 'new'}], 'defaultValue' => 'existing', 'required' => true, 'description' => 'Restore the current instance or a new instance?'}, options)
373
+ if params['restoreInstance'] == 'new'
374
+ # new instance
375
+ config_map = prompt_restore_instance_config(options)
376
+ params['instanceConfig'] = config_map
377
+ else
378
+ # existing instance
379
+ # confirm the instance
380
+ keep_prompting = !options[:no_prompt]
381
+ while keep_prompting
382
+ instance_id = prompt_value({'fieldName' => 'instanceId', 'fieldLabel' => 'Confirm Instance ID', 'type' => 'text', 'required' => true, 'description' => "Enter the current instance ID to confirm that you wish to restore it."}, options)
383
+ if instance_id && instance_id.to_i == instance['id']
384
+ params['instanceId'] = instance_id.to_i
385
+ keep_prompting = false
386
+ elsif instance_id.to_s.downcase == instance['name'].to_s.downcase # allow matching on name too
387
+ params['instanceId'] = instance['id']
388
+ keep_prompting = false
389
+ else
390
+ print_red_alert "The value '#{instance_id}' does not match the existing instance #{instance['name']} [#{instance['id'] rescue ''}]. Please try again."
391
+ end
392
+ end
393
+ end
394
+ elsif backup['locationType'] == 'server'
395
+ # prompt for server type backup restore
396
+ elsif backup['locationType'] == 'storage'
397
+ # prompt for storage type backup restore
398
+ else
399
+ print yellow, "Backup location type is unknown: #{backup['locationType']}",reset,"\n"
372
400
  end
373
- payload['backup'].deep_merge!(params)
401
+
402
+ payload['restore'].deep_merge!(params)
374
403
  end
375
404
 
376
- print cyan,"#{bold}WARNING!#{reset}#{cyan} Restoring a backup will erase all data when restored to an existing instance.",reset,"\n"
377
- confirm!("Are you sure you want to restore the backup result ID: #{backup_result['id']} Name: #{backup_result['backup']['name'] rescue ''} Date: (#{format_local_dt(backup_result['dateCreated'])})?", options)
378
- execute_api(@backups_interface, :restore, [backup['id']], options, 'backup') do |json_response|
405
+ if params['restoreInstance'] != 'new'
406
+ if backup['instance']
407
+ print cyan,"You have selected to restore the existing instance #{backup['instance']['name'] rescue ''} [#{backup['instance']['id'] rescue ''}] with the backup result #{format_backup_result_option_name(backup_result)} [#{backup_result['id']}]",reset,"\n"
408
+ end
409
+ if backup['sourceProviderId']
410
+ print yellow,"#{bold}WARNING!#{reset}#{yellow} Restoring a backup will overwite objects when restored to an existing object store.",reset,"\n"
411
+ else
412
+ print yellow,"#{bold}WARNING!#{reset}#{yellow} Restoring a backup will erase all data when restored to an existing instance.",reset,"\n"
413
+ end
414
+ end
415
+ confirm!("Are you sure you want to restore the backup result?", options)
416
+ execute_api(@backup_restores_interface, :create, [], options, 'restore') do |json_response|
379
417
  print_green_success "Restoring backup result ID: #{backup_result['id']} Name: #{backup_result['backup']['name'] rescue ''} Date: (#{format_local_dt(backup_result['dateCreated'])}"
380
418
  # should get the restore maybe, or could even support refreshing until it is complete...
381
419
  # restore = json_response["restore"]
@@ -506,4 +544,45 @@ EOT
506
544
  ]
507
545
  end
508
546
 
547
+ def format_backup_result_option_name(result)
548
+ "#{result['backup']['name']} (#{format_local_dt(result['startDate'])})"
549
+ end
550
+
551
+ # prompt for an instance config (vdiPool.instanceConfig)
552
+ def prompt_restore_instance_config(options)
553
+ # use config if user passed one in..
554
+ scope_context = 'instanceConfig'
555
+ scoped_instance_config = {}
556
+ if options[:options][scope_context].is_a?(Hash)
557
+ scoped_instance_config = options[:options][scope_context]
558
+ end
559
+
560
+ # now configure an instance like normal, use the config as default options with :always_prompt
561
+ instance_prompt_options = {}
562
+ # instance_prompt_options[:group] = group ? group['id'] : nil
563
+ # #instance_prompt_options[:cloud] = cloud ? cloud['name'] : nil
564
+ # instance_prompt_options[:default_cloud] = cloud ? cloud['name'] : nil
565
+ # instance_prompt_options[:environment] = selected_environment ? selected_environment['code'] : nil
566
+ # instance_prompt_options[:default_security_groups] = scoped_instance_config['securityGroups'] ? scoped_instance_config['securityGroups'] : nil
567
+
568
+ instance_prompt_options[:no_prompt] = options[:no_prompt]
569
+ #instance_prompt_options[:always_prompt] = options[:no_prompt] != true # options[:always_prompt]
570
+ instance_prompt_options[:options] = scoped_instance_config
571
+ #instance_prompt_options[:options][:always_prompt] = instance_prompt_options[:no_prompt] != true
572
+ instance_prompt_options[:options][:no_prompt] = instance_prompt_options[:no_prompt]
573
+
574
+ #instance_prompt_options[:name_required] = true
575
+ # instance_prompt_options[:instance_type_code] = instance_type_code
576
+ # todo: an effort to render more useful help eg. -O Web.0.instance.name
577
+ help_field_prefix = scope_context
578
+ instance_prompt_options[:help_field_prefix] = help_field_prefix
579
+ instance_prompt_options[:options][:help_field_prefix] = help_field_prefix
580
+ # instance_prompt_options[:locked_fields] = scoped_instance_config['lockedFields']
581
+ # instance_prompt_options[:for_app] = true
582
+ instance_prompt_options[:select_datastore] = true
583
+ instance_prompt_options[:name_required] = true
584
+ # this provisioning helper method handles all (most) of the parsing and prompting
585
+ instance_config_payload = prompt_new_instance(instance_prompt_options)
586
+ return instance_config_payload
587
+ end
509
588
  end
@@ -456,32 +456,6 @@ EOT
456
456
  opts.on('-l', '--labels [LIST]', String, "Labels") do |val|
457
457
  options[:options]['labels'] = parse_labels(val)
458
458
  end
459
- opts.on('--logo FILE', String, "Upload a custom logo icon") do |val|
460
- filename = val
461
- logo_file = nil
462
- if filename == 'null'
463
- logo_file = 'null' # clear it
464
- else
465
- filename = File.expand_path(filename)
466
- if !File.exist?(filename)
467
- raise_command_error "File not found: #{filename}"
468
- end
469
- logo_file = File.new(filename, 'rb')
470
- end
471
- end
472
- opts.on('--dark-logo FILE', String, "Upload a custom dark logo icon") do |val|
473
- filename = val
474
- dark_logo_file = nil
475
- if filename == 'null'
476
- dark_logo_file = 'null' # clear it
477
- else
478
- filename = File.expand_path(filename)
479
- if !File.exist?(filename)
480
- raise_command_error "File not found: #{filename}"
481
- end
482
- dark_logo_file = File.new(filename, 'rb')
483
- end
484
- end
485
459
  opts.on('--config-file FILE', String, "Config from a local JSON or YAML file") do |val|
486
460
  options[:config_file] = val.to_s
487
461
  file_content = nil
@@ -41,6 +41,12 @@ class Morpheus::Cli::Clouds
41
41
  opts.on( '-t', '--type TYPE', "Cloud Type" ) do |val|
42
42
  options[:zone_type] = val
43
43
  end
44
+ opts.on('-l', '--labels LABEL', String, "Filter by labels, can match any of the values") do |val|
45
+ add_query_parameter(params, 'labels', parse_labels(val))
46
+ end
47
+ opts.on('--all-labels LABEL', String, "Filter by labels, must match all of the values") do |val|
48
+ add_query_parameter(params, 'allLabels', parse_labels(val))
49
+ end
44
50
  build_standard_list_options(opts, options)
45
51
  opts.footer = "List clouds."
46
52
  end
@@ -179,6 +185,7 @@ class Morpheus::Cli::Clouds
179
185
  "Type" => lambda {|it| it['zoneType'] ? it['zoneType']['name'] : '' },
180
186
  "Code" => 'code',
181
187
  "Location" => 'location',
188
+ "Labels" => lambda {|it| format_list(it['labels'], '') rescue '' },
182
189
  "Region Code" => 'regionCode',
183
190
  "Visibility" => lambda {|it| it['visibility'].to_s.capitalize },
184
191
  "Groups" => lambda {|it| it['groups'].collect {|g| g.instance_of?(Hash) ? g['name'] : g.to_s }.join(', ') },
@@ -229,6 +236,9 @@ class Morpheus::Cli::Clouds
229
236
  opts.on('--credential VALUE', String, "Credential ID or \"local\"" ) do |val|
230
237
  options[:options]['credential'] = val
231
238
  end
239
+ opts.on('-l', '--labels [LIST]', String, "Labels") do |val|
240
+ options[:options]['labels'] = parse_labels(val)
241
+ end
232
242
 
233
243
  build_common_options(opts, options, [:options, :payload, :json, :dry_run, :remote])
234
244
  end
@@ -342,6 +352,9 @@ class Morpheus::Cli::Clouds
342
352
  # opts.on( '-d', '--description DESCRIPTION', "Description (optional)" ) do |desc|
343
353
  # params[:description] = desc
344
354
  # end
355
+ opts.on('-l', '--labels [LIST]', String, "Labels") do |val|
356
+ options[:options]['labels'] = parse_labels(val)
357
+ end
345
358
  opts.on('--costing-mode VALUE', String, "Costing Mode can be off, costing, or full. Default is off." ) do |val|
346
359
  options[:options]['costingMode'] = val
347
360
  end
@@ -400,6 +413,9 @@ class Morpheus::Cli::Clouds
400
413
  if params['zone'].is_a?(Hash)
401
414
  cloud_payload.merge!(params.delete('zone'))
402
415
  end
416
+ if params.key?('labels')
417
+ params['labels'] = parse_labels(params['labels'])
418
+ end
403
419
  cloud_payload.merge!(params)
404
420
  payload = {zone: cloud_payload}
405
421
  end
@@ -1141,6 +1157,7 @@ EOT
1141
1157
  "ID" => 'id',
1142
1158
  "Name" => 'name',
1143
1159
  "Type" => lambda {|it| it['zoneType'] ? it['zoneType']['name'] : '' },
1160
+ "Labels" => lambda {|it| format_list(it['labels'], '', 3) rescue '' },
1144
1161
  "Location" => 'location',
1145
1162
  "Region Code" => lambda {|it| it['regionCode'] },
1146
1163
  "Groups" => lambda {|it| (it['groups'] || []).collect {|g| g.instance_of?(Hash) ? g['name'] : g.to_s }.join(', ') },
@@ -1155,10 +1172,11 @@ EOT
1155
1172
  #{'fieldName' => 'zoneType.code', 'fieldLabel' => 'Image Type', 'type' => 'select', 'selectOptions' => cloud_types_for_dropdown, 'required' => true, 'description' => 'Cloud Type.', 'displayOrder' => 0},
1156
1173
  {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true, 'displayOrder' => 1},
1157
1174
  {'fieldName' => 'code', 'fieldLabel' => 'Code', 'type' => 'text', 'required' => false, 'displayOrder' => 2},
1158
- {'fieldName' => 'location', 'fieldLabel' => 'Location', 'type' => 'text', 'required' => false, 'displayOrder' => 3},
1159
- {'fieldName' => 'visibility', 'fieldLabel' => 'Visibility', 'type' => 'select', 'selectOptions' => [{'name' => 'Private', 'value' => 'private'},{'name' => 'Public', 'value' => 'public'}], 'required' => false, 'description' => 'Visibility', 'category' => 'permissions', 'defaultValue' => 'private', 'displayOrder' => 4},
1160
- {'fieldName' => 'enabled', 'fieldLabel' => 'Enabled', 'type' => 'checkbox', 'required' => false, 'defaultValue' => true, 'displayOrder' => 5},
1161
- {'fieldName' => 'autoRecoverPowerState', 'fieldLabel' => 'Automatically Power On VMs', 'type' => 'checkbox', 'required' => false, 'defaultValue' => false, 'displayOrder' => 6}
1175
+ {'shorthand' => '-l', 'optionalValue' => true, 'fieldName' => 'labels', 'fieldLabel' => 'Labels', 'type' => 'text', 'required' => false, 'processValue' => lambda {|val| parse_labels(val) }, 'displayOrder' => 3},
1176
+ {'fieldName' => 'location', 'fieldLabel' => 'Location', 'type' => 'text', 'required' => false, 'displayOrder' => 4},
1177
+ {'fieldName' => 'visibility', 'fieldLabel' => 'Visibility', 'type' => 'select', 'selectOptions' => [{'name' => 'Private', 'value' => 'private'},{'name' => 'Public', 'value' => 'public'}], 'required' => false, 'description' => 'Visibility', 'category' => 'permissions', 'defaultValue' => 'private', 'displayOrder' => 5},
1178
+ {'fieldName' => 'enabled', 'fieldLabel' => 'Enabled', 'type' => 'checkbox', 'required' => false, 'defaultValue' => true, 'displayOrder' => 6},
1179
+ {'fieldName' => 'autoRecoverPowerState', 'fieldLabel' => 'Automatically Power On VMs', 'type' => 'checkbox', 'required' => false, 'defaultValue' => false, 'displayOrder' => 7}
1162
1180
  ]
1163
1181
 
1164
1182
  # TODO: Account
@@ -33,7 +33,7 @@ class Morpheus::Cli::CurlCommand
33
33
  raise ::OptionParser::InvalidOption.new("Failed to parse payload as JSON. Error: #{ex.message}")
34
34
  end
35
35
  end
36
- opts.on('--absolute', "Absolute path, value can be used to prevent automatic using the automatic /api/ path prefix to the path by default.") do
36
+ opts.on('--absolute', "Absolute path, skip the addition of path prefix '/api/'") do
37
37
  options[:absolute_path] = true
38
38
  end
39
39
  opts.on('--inspect', "Inspect response, prints headers. By default only the body is printed.") do
@@ -5,6 +5,7 @@ class Morpheus::Cli::DashboardCommand
5
5
  include Morpheus::Cli::ProvisioningHelper
6
6
  set_command_name :dashboard
7
7
  set_command_description "View Morpheus Dashboard"
8
+ set_command_hidden
8
9
 
9
10
  def initialize()
10
11
  # @appliance_name, @appliance_url = Morpheus::Cli::Remote.active_appliance
@@ -16,7 +17,7 @@ class Morpheus::Cli::DashboardCommand
16
17
  end
17
18
 
18
19
  def usage
19
- "Usage: morpheus #{command_name}"
20
+ "#{yellow}API for this command has been removed starting with appliance version 6.2.6\n#{reset}Usage: morpheus #{command_name}"
20
21
  end
21
22
 
22
23
  def handle(args)
@@ -31,6 +31,12 @@ class Morpheus::Cli::Groups
31
31
  params = {}
32
32
  optparse = Morpheus::Cli::OptionParser.new do|opts|
33
33
  opts.banner = subcommand_usage()
34
+ opts.on('-l', '--labels LABEL', String, "Filter by labels, can match any of the values") do |val|
35
+ add_query_parameter(params, 'labels', parse_labels(val))
36
+ end
37
+ opts.on('--all-labels LABEL', String, "Filter by labels, must match all of the values") do |val|
38
+ add_query_parameter(params, 'allLabels', parse_labels(val))
39
+ end
34
40
  build_standard_list_options(opts, options)
35
41
  opts.footer = "List groups."
36
42
  end
@@ -100,6 +106,14 @@ EOT
100
106
  return 0 if render_result
101
107
 
102
108
  group = json_response['group']
109
+ group_stats = group['stats']
110
+ # serverCounts moved to zone.stats.serverCounts
111
+ server_counts = nil
112
+ instance_counts = nil
113
+ if group_stats
114
+ instance_counts = group_stats['instanceCounts']
115
+ server_counts = group_stats['serverCounts']
116
+ end
103
117
  is_active = @active_group_id && (@active_group_id == group['id'])
104
118
  print_h1 "Group Details"
105
119
  print cyan
@@ -108,10 +122,28 @@ EOT
108
122
  "Name" => 'name',
109
123
  "Code" => 'code',
110
124
  "Location" => 'location',
125
+ "Labels" => lambda {|it| format_list(it['labels'], '') rescue '' },
111
126
  "Clouds" => lambda {|it| it['zones'].collect {|z| z['name'] }.join(', ') },
112
- "Hosts" => 'serverCount'
127
+ #"Instances" => lambda {|it| it['stats']['instanceCounts']['all'] rescue '' },
128
+ # "Hosts" => lambda {|it| it['stats']['serverCounts']['host'] rescue it['serverCount'] },
129
+ # "VMs" => lambda {|it| it['stats']['serverCounts']['vm'] rescue '' },
130
+ # "Bare Metal" => lambda {|it| it['stats']['serverCounts']['baremetal'] rescue '' },
113
131
  }
114
132
  print_description_list(description_cols, group)
133
+
134
+ if server_counts
135
+ print_h2 "Group Stats"
136
+ print cyan
137
+ print "Clouds: #{group['zones'].size}".center(20)
138
+ print "Instances: #{instance_counts['all']}".center(20) if instance_counts
139
+ print "Hosts: #{server_counts['host']}".center(20)
140
+ #print "Container Hosts: #{server_counts['containerHost']}".center(20)
141
+ #print "Hypervisors: #{server_counts['hypervisor']}".center(20)
142
+ print "Virtual Machines: #{server_counts['vm']}".center(20)
143
+ print "Bare Metal: #{server_counts['baremetal']}".center(20)
144
+ #print "Unmanaged: #{server_counts['unmanaged']}".center(20)
145
+ print "\n"
146
+ end
115
147
  # puts "ID: #{group['id']}"
116
148
  # puts "Name: #{group['name']}"
117
149
  # puts "Code: #{group['code']}"
@@ -211,10 +243,12 @@ EOT
211
243
  params = options[:options] || {}
212
244
 
213
245
  if params.empty?
214
- puts optparse
215
- exit 1
246
+ raise_command_error "Specify at least one option to update.\n#{optparse}"
216
247
  end
217
248
 
249
+ if params.key?('labels')
250
+ params['labels'] = parse_labels(params['labels'])
251
+ end
218
252
  group_payload.merge!(params)
219
253
 
220
254
  payload = {group: group_payload}
@@ -638,18 +672,26 @@ EOT
638
672
  {
639
673
  id: active_group ? (((@active_group_id && (@active_group_id == group['id'])) ? "=> " : " ") + group['id'].to_s) : group['id'],
640
674
  name: group['name'],
675
+ labels: group['labels'],
641
676
  location: group['location'],
642
677
  cloud_count: group['zones'] ? group['zones'].size : 0,
643
- server_count: group['serverCount']
678
+ instance_count: (group['stats']['instanceCounts']['all'] rescue ''),
679
+ host_count: (group['stats']['serverCounts']['host'] rescue group['serverCount']),
680
+ vm_count: (group['stats']['serverCounts']['vm'] rescue ''),
681
+ baremetal_count: (group['stats']['serverCounts']['baremetal'] rescue '')
644
682
  }
645
683
  end
646
684
  columns = [
647
685
  #{:active => {:display_name => ""}},
648
686
  {:id => {:display_name => (active_group ? " ID" : "ID")}},
649
- {:name => {:width => 16}},
687
+ {:name => {:width => 64}},
650
688
  {:location => {:width => 32}},
689
+ {:labels => {:display_method => lambda {|it| format_list(it[:labels], '', 3) rescue '' }}},
651
690
  {:cloud_count => {:display_name => "CLOUDS"}},
652
- {:server_count => {:display_name => "HOSTS"}}
691
+ {:instance_count => {:display_name => "INSTANCES"}},
692
+ {:host_count => {:display_name => "HOSTS"}},
693
+ {:vm_count => {:display_name => "VMS"}},
694
+ {:baremetal_count => {:display_name => "BARE METAL"}},
653
695
  ]
654
696
  print as_pretty_table(rows, columns, opts)
655
697
  end
@@ -658,7 +700,8 @@ EOT
658
700
  tmp_option_types = [
659
701
  {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true, 'displayOrder' => 1},
660
702
  {'fieldName' => 'code', 'fieldLabel' => 'Code', 'type' => 'text', 'required' => false, 'displayOrder' => 2},
661
- {'fieldName' => 'location', 'fieldLabel' => 'Location', 'type' => 'text', 'required' => false, 'displayOrder' => 3}
703
+ {'shorthand' => '-l', 'optionalValue' => true, 'fieldName' => 'labels', 'fieldLabel' => 'Labels', 'type' => 'text', 'required' => false, 'processValue' => lambda {|val| parse_labels(val) }, 'displayOrder' => 3},
704
+ {'fieldName' => 'location', 'fieldLabel' => 'Location', 'type' => 'text', 'required' => false, 'displayOrder' => 4},
662
705
  ]
663
706
 
664
707
  # Advanced Options
@@ -323,7 +323,7 @@ EOT
323
323
  {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true, 'description' => 'Form name'},
324
324
  {'fieldName' => 'code', 'fieldLabel' => 'Code', 'type' => 'text', 'required' => true, 'description' => 'Unique form code'},
325
325
  {'fieldName' => 'description', 'fieldLabel' => 'Description', 'type' => 'text'},
326
- {'shorthand' => '-l', 'fieldName' => 'labels', 'fieldLabel' => 'Labels', 'type' => 'text', 'required' => false, 'noPrompt' => true, 'processValue' => lambda {|val| parse_labels(val) }},
326
+ {'shorthand' => '-l', 'optionalValue' => true, 'fieldName' => 'labels', 'fieldLabel' => 'Labels', 'type' => 'text', 'required' => false, 'noPrompt' => true, 'processValue' => lambda {|val| parse_labels(val) }},
327
327
  ]
328
328
  end
329
329
 
@@ -431,7 +431,7 @@ class Morpheus::Cli::LibraryOptionListsCommand
431
431
  # rest
432
432
  {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true, 'displayOrder' => 1},
433
433
  {'fieldName' => 'description', 'fieldLabel' => 'Description', 'type' => 'text', 'displayOrder' => 2},
434
- {'shorthand' => '-l', 'fieldName' => 'labels', 'fieldLabel' => 'Labels', 'type' => 'text', 'required' => false, 'noPrompt' => true, 'processValue' => lambda {|val| parse_labels(val) }, 'displayOrder' => 3},
434
+ {'shorthand' => '-l', 'optionalValue' => true, 'fieldName' => 'labels', 'fieldLabel' => 'Labels', 'type' => 'text', 'required' => false, 'noPrompt' => true, 'processValue' => lambda {|val| parse_labels(val) }, 'displayOrder' => 3},
435
435
  {'code' => 'optionTypeList.type', 'fieldName' => 'type', 'fieldLabel' => 'Type', 'type' => 'select', 'selectOptions' => get_available_option_list_types, 'defaultValue' => 'rest', 'required' => true, 'description' => 'Option List Type. eg. rest, api, ldap, manual', 'displayOrder' => 4},
436
436
  {'fieldName' => 'visibility', 'fieldLabel' => 'Visibility', 'type' => 'select', 'selectOptions' => [{'name' => 'Private', 'value' => 'private'}, {'name' => 'Public', 'value' => 'public'}], 'defaultValue' => 'private', 'displayOrder' => 5},
437
437
  {'dependsOnCode' => 'optionTypeList.type:rest', 'fieldName' => 'sourceUrl', 'fieldLabel' => 'Source Url', 'type' => 'text', 'required' => true, 'description' => "A REST URL can be used to fetch list data and is cached in the appliance database.", 'displayOrder' => 6},
@@ -302,7 +302,7 @@ class Morpheus::Cli::LibraryOptionTypesCommand
302
302
  [
303
303
  {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true, 'displayOrder' => 1},
304
304
  {'fieldName' => 'description', 'fieldLabel' => 'Description', 'type' => 'text', 'displayOrder' => 2},
305
- {'shorthand' => '-l', 'fieldName' => 'labels', 'fieldLabel' => 'Labels', 'type' => 'text', 'required' => false, 'noPrompt' => true, 'processValue' => lambda {|val| parse_labels(val) }, 'displayOrder' => 3},
305
+ {'shorthand' => '-l', 'optionalValue' => true, 'fieldName' => 'labels', 'fieldLabel' => 'Labels', 'type' => 'text', 'required' => false, 'noPrompt' => true, 'processValue' => lambda {|val| parse_labels(val) }, 'displayOrder' => 3},
306
306
  {'fieldName' => 'fieldName', 'fieldLabel' => 'Field Name', 'type' => 'text', 'required' => true, 'description' => 'This is the input property that the value gets assigned to.', 'displayOrder' => 4},
307
307
  {'code' => 'optionType.type', 'fieldName' => 'type', 'fieldLabel' => 'Type', 'type' => 'select', 'selectOptions' => [{'name' => 'Text', 'value' => 'text'}, {'name' => 'Password', 'value' => 'password'}, {'name' => 'Number', 'value' => 'number'}, {'name' => 'Checkbox', 'value' => 'checkbox'}, {'name' => 'Select', 'value' => 'select'}, {'name' => 'Hidden', 'value' => 'hidden'}], 'defaultValue' => 'text', 'required' => true, 'displayOrder' => 5},
308
308
  {'fieldName' => 'optionList', 'fieldLabel' => 'Option List', 'type' => 'select', 'optionSource' => 'optionTypeLists', 'required' => true, 'dependsOnCode' => 'optionType.type:select', 'description' => "The Option List to be the source of options when type is 'select'.", 'displayOrder' => 6},
@@ -132,7 +132,7 @@ class Morpheus::Cli::NetworksCommand
132
132
  pool: subnet['pool'] ? subnet['pool']['name'] : '',
133
133
  dhcp: subnet['dhcpServer'] ? 'Yes' : 'No',
134
134
  visibility: subnet['visibility'].to_s.capitalize,
135
- active: format_boolean(network['active']),
135
+ active: format_boolean(subnet['active']),
136
136
  tenants: subnet['tenants'] ? subnet['tenants'].collect {|it| it['name'] }.uniq.join(', ') : ''
137
137
  }
138
138
  rows << subnet_row
@@ -57,7 +57,7 @@ class Morpheus::Cli::SecurityPackagesCommand
57
57
  api_client.security_package_types.list({max:10000})['securityPackageTypes'].collect { |it| {"name" => it["name"], "value" => it["code"]} }
58
58
  }, 'required' => true, 'defaultValue' => 'SCAP Package'},
59
59
  {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true},
60
- {'shorthand' => '-l', 'fieldName' => 'labels', 'fieldLabel' => 'Labels', 'type' => 'text', 'required' => false, 'noPrompt' => true, 'processValue' => lambda {|val| parse_labels(val) }},
60
+ {'shorthand' => '-l', 'optionalValue' => true, 'fieldName' => 'labels', 'fieldLabel' => 'Labels', 'type' => 'text', 'required' => false, 'noPrompt' => true, 'processValue' => lambda {|val| parse_labels(val) }},
61
61
  {'fieldName' => 'description', 'fieldLabel' => 'Description', 'type' => 'text', 'required' => false},
62
62
  {'fieldName' => 'enabled', 'fieldLabel' => 'Enabled', 'type' => 'checkbox', 'required' => false, 'defaultValue' => true},
63
63
  # {'code' => 'securityPackage.sourceType', 'fieldName' => 'sourceType', 'fieldLabel' => 'Source', 'type' => 'select', 'selectOptions' => [{'name'=>'url','value'=>'url'}], 'defaultValue' => 'url', 'required' => true},
@@ -385,7 +385,7 @@ EOT
385
385
 
386
386
  if hubmode == 'skip'
387
387
  if ::Morpheus::Cli::OptionTypes::confirm("Would you like to install your License Key now?", options.merge({:default => true}))
388
- cmd_res = Morpheus::Cli::License.new.apply([] + (options[:remote] ? ["-r",options[:remote]] : []))
388
+ cmd_res = Morpheus::Cli::License.new.install([] + (options[:remote] ? ["-r",options[:remote]] : []))
389
389
  # license_is_valid = cmd_res != false
390
390
  end
391
391
  end
@@ -1373,11 +1373,11 @@ class Morpheus::Cli::Tasks
1373
1373
  # this makes us all sad
1374
1374
  option_types.each do |option_type|
1375
1375
  if option_type['type'] == 'typeahead'
1376
- if ['operationalWorkflowName','containerScript','containerTemplate'].include?(option_type['code'])
1376
+ if ['operationalWorkflowName','ifOperationalWorkflowName','elseOperationalWorkflowName','containerScript','containerTemplate'].include?(option_type['code'])
1377
1377
  option_type.deep_merge!({'config' => {'valueField' => 'name'}})
1378
1378
  end
1379
1379
  elsif option_type['type'] == 'hidden'
1380
- if ['operationalWorkflowId','containerScriptId','containerTemplateId'].include?(option_type['code'])
1380
+ if ['operationalWorkflowId','ifOperationalWorkflowId','elseOperationalWorkflowId','containerScriptId','containerTemplateId'].include?(option_type['code'])
1381
1381
  option_type['processValue'] = lambda {|val|
1382
1382
  if val.to_s.empty?
1383
1383
  selected_option = Morpheus::Cli::OptionTypes.get_last_select()
@@ -104,7 +104,16 @@ EOT
104
104
  "ACCESS TOKEN" => lambda {|it| it['maskedAccessToken'] },
105
105
  "REFRESH TOKEN" => lambda {|it| it['maskedRefreshToken'] },
106
106
  "ACCESS EXPIRATION" => lambda {|it| format_local_dt(it['expiration']) },
107
- "ACCESS TTL" => lambda {|it| it['expiration'] ? (format_duration(it['expiration']) rescue '') : '' }
107
+ "ACCESS TTL" => lambda {|it|
108
+ if it['expiration']
109
+ expires_on = parse_time(it['expiration'])
110
+ if expires_on && expires_on < Time.now
111
+ "Expired"
112
+ else
113
+ it['expiration'] ? (format_duration(it['expiration']) rescue '') : ''
114
+ end
115
+ end
116
+ }
108
117
  }
109
118
  print cyan
110
119
  puts as_pretty_table(access_tokens, cols)
@@ -119,10 +119,10 @@ module Morpheus::Cli::BackupsHelper
119
119
  "Backup" => lambda {|it| it['backup']['name'] rescue '' },
120
120
  "Status" => lambda {|it| format_backup_result_status(it) },
121
121
  #"Duration" => lambda {|it| format_duration(it['startDate'], it['endDate']) },
122
- "Duration" => lambda {|it| format_duration_milliseconds(it['durationMillis']) },
122
+ "Duration" => lambda {|it| format_duration_milliseconds(it['durationMillis']) if it['durationMillis'].to_i > 0 },
123
123
  "Start Date" => lambda {|it| format_local_dt(it['startDate']) },
124
124
  "End Date" => lambda {|it| format_local_dt(it['endDate']) },
125
- "Size" => lambda {|it| format_bytes(it['sizeInMb'], 'MB') },
125
+ "Size" => lambda {|it| it['sizeInMb'].to_i != 0 ? format_bytes(it['sizeInMb'], 'MB') : '' },
126
126
  }
127
127
  end
128
128
 
@@ -155,7 +155,7 @@ module Morpheus::Cli::BackupsHelper
155
155
  "Target" => lambda {|it| it['instance']['name'] rescue '' },
156
156
  "Status" => lambda {|it| format_backup_result_status(it) },
157
157
  #"Duration" => lambda {|it| format_duration(it['startDate'], it['endDate']) },
158
- "Duration" => lambda {|it| format_duration_milliseconds(it['durationMillis']) },
158
+ "Duration" => lambda {|it| format_duration_milliseconds(it['durationMillis']) if it['durationMillis'].to_i > 0 },
159
159
  "Start Date" => lambda {|it| format_local_dt(it['startDate']) },
160
160
  "End Date" => lambda {|it| format_local_dt(it['endDate']) },
161
161
  }
@@ -168,4 +168,5 @@ module Morpheus::Cli::BackupsHelper
168
168
  def format_backup_restore_status(backup_restore, return_color=cyan)
169
169
  format_backup_result_status(backup_restore, return_color)
170
170
  end
171
+
171
172
  end
@@ -1113,8 +1113,15 @@ module Morpheus
1113
1113
  value_found = false
1114
1114
  value = nil
1115
1115
  while !value_found do
1116
- print "#{option_type['fieldLabel']}#{option_type['fieldAddOn'] ? (' (' + option_type['fieldAddOn'] + ') ') : '' }#{!option_type['required'] ? ' (optional)' : ''}#{!option_type['defaultValue'].to_s.empty? ? ' ['+option_type['defaultValue'].to_s+']' : ''}: "
1117
- input = $stdin.gets.chomp!
1116
+ # print "#{option_type['fieldLabel']}#{option_type['fieldAddOn'] ? (' (' + option_type['fieldAddOn'] + ') ') : '' }#{!option_type['required'] ? ' (optional)' : ''}#{!option_type['defaultValue'].to_s.empty? ? ' ['+option_type['defaultValue'].to_s+']' : ''}: "
1117
+ # input = $stdin.gets.chomp!
1118
+ Readline.completion_append_character = ""
1119
+ Readline.basic_word_break_characters = ''
1120
+ Readline.completion_proc = nil
1121
+ prompt_label = "#{option_type['fieldLabel']}#{option_type['fieldAddOn'] ? (' (' + option_type['fieldAddOn'] + ') ') : '' }#{!option_type['required'] ? ' (optional)' : ''}#{!option_type['defaultValue'].to_s.empty? ? ' ['+option_type['defaultValue'].to_s+']' : ''}: "
1122
+ input = Readline.readline(prompt_label, false).to_s
1123
+ input = input.chomp #.strip
1124
+
1118
1125
  value = input.empty? ? option_type['defaultValue'] : input
1119
1126
  if input == '?'
1120
1127
  help_prompt(option_type)
@@ -1,6 +1,6 @@
1
1
 
2
2
  module Morpheus
3
3
  module Cli
4
- VERSION = "6.3.2"
4
+ VERSION = "6.3.4"
5
5
  end
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: morpheus-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.3.2
4
+ version: 6.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Estes
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2023-12-14 00:00:00.000000000 Z
14
+ date: 2024-02-15 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: bundler
@@ -204,6 +204,7 @@ files:
204
204
  - lib/morpheus/api/backup_service_types_interface.rb
205
205
  - lib/morpheus/api/backup_services_interface.rb
206
206
  - lib/morpheus/api/backup_settings_interface.rb
207
+ - lib/morpheus/api/backup_types_interface.rb
207
208
  - lib/morpheus/api/backups_interface.rb
208
209
  - lib/morpheus/api/billing_interface.rb
209
210
  - lib/morpheus/api/blueprints_interface.rb
@@ -381,6 +382,7 @@ files:
381
382
  - lib/morpheus/cli/commands/backup_results_command.rb
382
383
  - lib/morpheus/cli/commands/backup_services_command.rb
383
384
  - lib/morpheus/cli/commands/backup_settings_command.rb
385
+ - lib/morpheus/cli/commands/backup_types_command.rb
384
386
  - lib/morpheus/cli/commands/backups_command.rb
385
387
  - lib/morpheus/cli/commands/benchmark_command.rb
386
388
  - lib/morpheus/cli/commands/blueprints_command.rb