morpheus-cli 6.3.3 → 7.0.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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +1 -1
  3. data/lib/morpheus/api/api_client.rb +8 -0
  4. data/lib/morpheus/api/backup_restores_interface.rb +4 -0
  5. data/lib/morpheus/api/backup_types_interface.rb +9 -0
  6. data/lib/morpheus/api/catalog_item_types_interface.rb +1 -2
  7. data/lib/morpheus/api/hub_interface.rb +25 -0
  8. data/lib/morpheus/cli/cli_command.rb +3 -0
  9. data/lib/morpheus/cli/commands/backup_types_command.rb +53 -0
  10. data/lib/morpheus/cli/commands/backups_command.rb +100 -21
  11. data/lib/morpheus/cli/commands/catalog_item_types_command.rb +3 -31
  12. data/lib/morpheus/cli/commands/clouds.rb +22 -4
  13. data/lib/morpheus/cli/commands/curl_command.rb +1 -1
  14. data/lib/morpheus/cli/commands/groups.rb +50 -7
  15. data/lib/morpheus/cli/commands/hub.rb +215 -0
  16. data/lib/morpheus/cli/commands/jobs_command.rb +10 -6
  17. data/lib/morpheus/cli/commands/library_forms_command.rb +1 -1
  18. data/lib/morpheus/cli/commands/library_option_lists_command.rb +1 -1
  19. data/lib/morpheus/cli/commands/library_option_types_command.rb +1 -1
  20. data/lib/morpheus/cli/commands/license.rb +151 -86
  21. data/lib/morpheus/cli/commands/networks_command.rb +1 -1
  22. data/lib/morpheus/cli/commands/security_packages.rb +1 -1
  23. data/lib/morpheus/cli/commands/setup.rb +1 -1
  24. data/lib/morpheus/cli/commands/tasks.rb +2 -2
  25. data/lib/morpheus/cli/commands/user_settings_command.rb +10 -1
  26. data/lib/morpheus/cli/mixins/backups_helper.rb +4 -3
  27. data/lib/morpheus/cli/mixins/jobs_helper.rb +3 -0
  28. data/lib/morpheus/cli/mixins/print_helper.rb +80 -2
  29. data/lib/morpheus/cli/option_types.rb +9 -2
  30. data/lib/morpheus/cli/version.rb +1 -1
  31. data/lib/morpheus/routes.rb +2 -1
  32. metadata +6 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 94da7995030caa8d8370454c1a17ae68e314080fa52dde2d95b48122e69e5f6d
4
- data.tar.gz: 19aa5d0040441e385606f43f1ae1b0d09e6eb5a3fa99958450acc6914dc8a803
3
+ metadata.gz: 763ccc16e1c853b5d76e8466a21fd94abbd098615a884ff252f72015d5031338
4
+ data.tar.gz: c8ea0dcb3534ca6c3c557ce58763dfcef22ed59e7bdd33bde6eff00ee8513c1f
5
5
  SHA512:
6
- metadata.gz: d718c881f79ecdf26a91172506f9500b69a3269bc1ae00bf53d0191fcbe484d10aea1e9919822c2a6fdf69d195b2d1c8009917d79ef9d5fd0df0a76962ba8f14
7
- data.tar.gz: da197f17e1617a9f909bd61b92c3f82b79ab70b951603033b08cd6b86e5b5ff20b8f659cd48fd5823fea80afad02712be72ad202ec5ef0d3e179a12a503af225
6
+ metadata.gz: a68ef51fb607ce70ba7c93bdeb5ab637c741ce03ad24e987a63a2ba0db2d2ffac674bae2ff4aab02c543c76df93da178fe5a1d8e709df99444f2b4ba8e506945
7
+ data.tar.gz: 90b6702b8ebc4b75a0aa29da1802155dab2aca529c2d9538adb363f6c663b4241f632c7b22cc17e9e5b949598994e152107072ff19d7429bf944c69fcc4ad35e
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.3
3
+ RUN gem install morpheus-cli -v 7.0.0
4
4
 
5
5
  ENTRYPOINT ["morpheus"]
@@ -846,6 +846,10 @@ class Morpheus::APIClient
846
846
  Morpheus::HealthInterface.new(common_interface_options).setopts(@options)
847
847
  end
848
848
 
849
+ def hub
850
+ Morpheus::HubInterface.new(common_interface_options).setopts(@options)
851
+ end
852
+
849
853
  def audit
850
854
  Morpheus::AuditInterface.new(common_interface_options).setopts(@options)
851
855
  end
@@ -894,6 +898,10 @@ class Morpheus::APIClient
894
898
  Morpheus::BackupServiceTypesInterface.new(common_interface_options).setopts(@options)
895
899
  end
896
900
 
901
+ def backup_types
902
+ Morpheus::BackupTypesInterface.new(common_interface_options).setopts(@options)
903
+ end
904
+
897
905
  def catalog_item_types
898
906
  Morpheus::CatalogItemTypesInterface.new(common_interface_options).setopts(@options)
899
907
  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
@@ -8,8 +8,7 @@ class Morpheus::CatalogItemTypesInterface < Morpheus::RestInterface
8
8
 
9
9
  # NOT json, multipart file upload, uses PUT update endpoint
10
10
  def update_logo(id, logo_file, dark_logo_file=nil)
11
- #url = "#{base_path}/#{id}/update-logo"
12
- url = "#{base_path}/#{id}"
11
+ url = "#{base_path}/#{id}/update-logo"
13
12
  headers = { :params => {}, :authorization => "Bearer #{@access_token}"}
14
13
  payload = {}
15
14
  payload["catalogItemType"] = {}
@@ -0,0 +1,25 @@
1
+ require 'morpheus/api/api_client'
2
+
3
+ class Morpheus::HubInterface < Morpheus::APIClient
4
+
5
+ def base_path
6
+ "/api/hub"
7
+ end
8
+
9
+ def get(params={}, headers={})
10
+ execute(method: :get, url: "#{base_path}", params: params, headers: headers)
11
+ end
12
+
13
+ def usage(params={}, headers={})
14
+ execute(method: :get, url: "#{base_path}/usage", params: params, headers: headers)
15
+ end
16
+
17
+ def checkin(payload={}, params={}, headers={})
18
+ execute(method: :post, url: "#{base_path}/checkin", payload: payload, params: params, headers: headers)
19
+ end
20
+
21
+ def register(payload={}, params={}, headers={})
22
+ execute(method: :post, url: "#{base_path}/register", payload: payload, params: params, headers: headers)
23
+ end
24
+
25
+ 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']
@@ -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
@@ -71,9 +71,6 @@ class Morpheus::Cli::CatalogItemTypesCommand
71
71
  print cyan,"No catalog item types found.",reset,"\n"
72
72
  else
73
73
  list_columns = catalog_item_type_list_column_definitions.upcase_keys!
74
- list_columns.delete("Blueprint")
75
- list_columns.delete("Workflow")
76
- list_columns.delete("Context")
77
74
  #list_columns["Config"] = lambda {|it| truncate_string(it['config'], 100) }
78
75
  print as_pretty_table(catalog_item_types, list_columns.upcase_keys!, options)
79
76
  print_results_pagination(json_response)
@@ -456,32 +453,6 @@ EOT
456
453
  opts.on('-l', '--labels [LIST]', String, "Labels") do |val|
457
454
  options[:options]['labels'] = parse_labels(val)
458
455
  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
456
  opts.on('--config-file FILE', String, "Config from a local JSON or YAML file") do |val|
486
457
  options[:config_file] = val.to_s
487
458
  file_content = nil
@@ -727,12 +698,13 @@ EOT
727
698
  "Description" => 'description',
728
699
  "Type" => lambda {|it| format_catalog_type(it) },
729
700
  "Visibility" => 'visibility',
730
- "Layout Code" => 'layoutCode',
701
+ #"Layout Code" => 'layoutCode',
731
702
  "Blueprint" => lambda {|it| it['blueprint'] ? it['blueprint']['name'] : nil },
732
703
  "Workflow" => lambda {|it| it['workflow'] ? it['workflow']['name'] : nil },
733
- "Context" => lambda {|it| it['context'] },
704
+ # "Context" => lambda {|it| it['context'] },
734
705
  # "Content" => lambda {|it| it['content'] },
735
706
  "Form Type" => lambda {|it| it['formType'] == 'form' ? "Form" : "Inputs" },
707
+ "Form" => lambda {|it| it['form'] ? it['form']['name'] : nil },
736
708
  "Enabled" => lambda {|it| format_boolean(it['enabled']) },
737
709
  "Featured" => lambda {|it| format_boolean(it['featured']) },
738
710
  #"Config" => lambda {|it| it['config'] },
@@ -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
@@ -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