morpheus-cli 6.3.3 → 7.0.0

Sign up to get free protection for your applications and to get access to all the features.
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