morpheus-cli 4.2.22 → 5.2.1

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/Dockerfile +1 -1
  4. data/lib/morpheus/api/api_client.rb +30 -0
  5. data/lib/morpheus/api/billing_interface.rb +34 -0
  6. data/lib/morpheus/api/catalog_item_types_interface.rb +9 -0
  7. data/lib/morpheus/api/deploy_interface.rb +1 -1
  8. data/lib/morpheus/api/deployments_interface.rb +20 -1
  9. data/lib/morpheus/api/forgot_password_interface.rb +17 -0
  10. data/lib/morpheus/api/instances_interface.rb +16 -2
  11. data/lib/morpheus/api/rest_interface.rb +0 -6
  12. data/lib/morpheus/api/roles_interface.rb +14 -0
  13. data/lib/morpheus/api/search_interface.rb +13 -0
  14. data/lib/morpheus/api/servers_interface.rb +14 -0
  15. data/lib/morpheus/api/service_catalog_interface.rb +89 -0
  16. data/lib/morpheus/api/usage_interface.rb +18 -0
  17. data/lib/morpheus/cli.rb +7 -3
  18. data/lib/morpheus/cli/apps.rb +6 -27
  19. data/lib/morpheus/cli/backup_jobs_command.rb +3 -0
  20. data/lib/morpheus/cli/backups_command.rb +3 -0
  21. data/lib/morpheus/cli/catalog_item_types_command.rb +622 -0
  22. data/lib/morpheus/cli/cli_command.rb +70 -21
  23. data/lib/morpheus/cli/commands/standard/curl_command.rb +3 -5
  24. data/lib/morpheus/cli/commands/standard/history_command.rb +3 -1
  25. data/lib/morpheus/cli/commands/standard/man_command.rb +74 -40
  26. data/lib/morpheus/cli/commands/standard/source_command.rb +1 -1
  27. data/lib/morpheus/cli/commands/standard/update_command.rb +76 -0
  28. data/lib/morpheus/cli/containers_command.rb +14 -24
  29. data/lib/morpheus/cli/cypher_command.rb +6 -2
  30. data/lib/morpheus/cli/deploy.rb +199 -90
  31. data/lib/morpheus/cli/deployments.rb +341 -28
  32. data/lib/morpheus/cli/deploys.rb +206 -41
  33. data/lib/morpheus/cli/error_handler.rb +7 -0
  34. data/lib/morpheus/cli/forgot_password.rb +133 -0
  35. data/lib/morpheus/cli/groups.rb +1 -1
  36. data/lib/morpheus/cli/health_command.rb +59 -2
  37. data/lib/morpheus/cli/hosts.rb +295 -35
  38. data/lib/morpheus/cli/instances.rb +247 -130
  39. data/lib/morpheus/cli/invoices_command.rb +37 -19
  40. data/lib/morpheus/cli/library_option_lists_command.rb +15 -7
  41. data/lib/morpheus/cli/library_option_types_command.rb +5 -2
  42. data/lib/morpheus/cli/logs_command.rb +9 -6
  43. data/lib/morpheus/cli/mixins/accounts_helper.rb +12 -7
  44. data/lib/morpheus/cli/mixins/backups_helper.rb +2 -4
  45. data/lib/morpheus/cli/mixins/deployments_helper.rb +31 -3
  46. data/lib/morpheus/cli/mixins/option_source_helper.rb +1 -1
  47. data/lib/morpheus/cli/mixins/print_helper.rb +46 -21
  48. data/lib/morpheus/cli/mixins/provisioning_helper.rb +108 -5
  49. data/lib/morpheus/cli/option_types.rb +271 -22
  50. data/lib/morpheus/cli/ping.rb +0 -1
  51. data/lib/morpheus/cli/remote.rb +35 -12
  52. data/lib/morpheus/cli/reports_command.rb +99 -30
  53. data/lib/morpheus/cli/roles.rb +453 -113
  54. data/lib/morpheus/cli/search_command.rb +182 -0
  55. data/lib/morpheus/cli/service_catalog_command.rb +1474 -0
  56. data/lib/morpheus/cli/setup.rb +1 -1
  57. data/lib/morpheus/cli/shell.rb +33 -11
  58. data/lib/morpheus/cli/storage_providers_command.rb +40 -56
  59. data/lib/morpheus/cli/tasks.rb +29 -32
  60. data/lib/morpheus/cli/usage_command.rb +203 -0
  61. data/lib/morpheus/cli/user_settings_command.rb +1 -0
  62. data/lib/morpheus/cli/users.rb +12 -1
  63. data/lib/morpheus/cli/version.rb +1 -1
  64. data/lib/morpheus/cli/virtual_images.rb +429 -254
  65. data/lib/morpheus/cli/whoami.rb +6 -6
  66. data/lib/morpheus/cli/workflows.rb +33 -40
  67. data/lib/morpheus/formatters.rb +75 -7
  68. data/lib/morpheus/terminal.rb +6 -2
  69. metadata +14 -2
@@ -19,7 +19,7 @@ class Morpheus::Cli::Instances
19
19
  :history, {:'history-details' => :history_details}, {:'history-event' => :history_event_details},
20
20
  :stats, :stop, :start, :restart, :actions, :action, :suspend, :eject, :stop_service, :start_service, :restart_service,
21
21
  :backup, :backups, :resize, :clone, :envs, :setenv, :delenv,
22
- :security_groups, :apply_security_groups, :run_workflow, :import_snapshot,
22
+ :security_groups, :apply_security_groups, :run_workflow, :import_snapshot, :snapshot, :snapshots,
23
23
  :console, :status_check, {:containers => :list_containers},
24
24
  :scaling, {:'scaling-update' => :scaling_update},
25
25
  :wiki, :update_wiki,
@@ -80,9 +80,6 @@ class Morpheus::Cli::Instances
80
80
  options[:owner] = val
81
81
  end
82
82
  opts.add_hidden_option('--created-by')
83
- opts.on('--details', "Display more details: memory and storage usage used / max values." ) do
84
- options[:details] = true
85
- end
86
83
  opts.on('--status STATUS', "Filter by status i.e. provisioning,running,starting,stopping") do |val|
87
84
  params['status'] = (params['status'] || []) + val.to_s.split(',').collect {|s| s.strip }.select {|s| s != "" }
88
85
  end
@@ -92,11 +89,45 @@ class Morpheus::Cli::Instances
92
89
  opts.on('--pending-removal-only', "Only instances pending removal.") do
93
90
  options[:deleted] = true
94
91
  end
92
+ opts.on( '--plan NAME', String, "Filter by Plan name(s)" ) do |val|
93
+ # commas used in names a lot so use --plan one --plan two
94
+ params['plan'] ||= []
95
+ params['plan'] << val
96
+ end
97
+ opts.on( '--plan-id ID', String, "Filter by Plan id(s)" ) do |val|
98
+ params['planId'] = parse_id_list(val)
99
+ end
100
+ opts.on( '--plan-code CODE', String, "Filter by Plan code(s)" ) do |val|
101
+ params['planCode'] = parse_id_list(val)
102
+ end
103
+ opts.on('--labels label',String, "Filter by labels (keywords).") do |val|
104
+ val.split(",").each do |k|
105
+ options[:labels] ||= []
106
+ options[:labels] << k.strip
107
+ end
108
+ end
109
+ opts.on('--tags Name=Value',String, "Filter by tags (metadata name value pairs).") do |val|
110
+ val.split(",").each do |value_pair|
111
+ k,v = value_pair.strip.split("=")
112
+ options[:tags] ||= {}
113
+ options[:tags][k] ||= []
114
+ options[:tags][k] << (v || '')
115
+ end
116
+ end
117
+ opts.on('--stats', "Display values for memory and storage usage used / max values." ) do
118
+ options[:stats] = true
119
+ end
120
+ opts.on('-a', '--details', "Display all details: plan, stats, etc" ) do
121
+ options[:details] = true
122
+ end
95
123
  build_common_options(opts, options, [:list, :query, :json, :yaml, :csv, :fields, :dry_run, :remote])
96
124
  opts.footer = "List instances."
97
125
  end
98
126
  optparse.parse!(args)
99
- verify_args!(args:args, count:0, optparse:optparse)
127
+ # verify_args!(args:args, optparse:optparse, count:0)
128
+ if args.count > 0
129
+ options[:phrase] = args.join(" ")
130
+ end
100
131
  connect(options)
101
132
  begin
102
133
  params.merge!(parse_list_options(options))
@@ -129,7 +160,13 @@ class Morpheus::Cli::Instances
129
160
 
130
161
  params['showDeleted'] = true if options[:showDeleted]
131
162
  params['deleted'] = true if options[:deleted]
132
-
163
+ params['labels'] = options[:labels] if options[:labels]
164
+ if options[:tags]
165
+ options[:tags].each do |k,v|
166
+ params['tags.' + k] = v
167
+ end
168
+ end
169
+
133
170
  @instances_interface.setopts(options)
134
171
  if options[:dry_run]
135
172
  print_dry_run @instances_interface.dry.list(params)
@@ -193,7 +230,7 @@ class Morpheus::Cli::Instances
193
230
  cpu_usage_str = !stats ? "" : generate_usage_bar((stats['usedCpu'] || stats['cpuUsage']).to_f, 100, {max_bars: 10})
194
231
  memory_usage_str = !stats ? "" : generate_usage_bar(stats['usedMemory'], stats['maxMemory'], {max_bars: 10})
195
232
  storage_usage_str = !stats ? "" : generate_usage_bar(stats['usedStorage'], stats['maxStorage'], {max_bars: 10})
196
- if options[:details]
233
+ if options[:details] || options[:stats]
197
234
  if stats['maxMemory'] && stats['maxMemory'].to_i != 0
198
235
  memory_usage_str = memory_usage_str + cyan + format_bytes_short(stats['usedMemory']).strip.rjust(8, ' ') + " / " + format_bytes_short(stats['maxMemory']).strip
199
236
  end
@@ -211,8 +248,9 @@ class Morpheus::Cli::Instances
211
248
  nodes: instance['containers'].count,
212
249
  status: format_instance_status(instance, cyan),
213
250
  type: instance['instanceType']['name'],
214
- group: !instance['group'].nil? ? instance['group']['name'] : nil,
215
- cloud: !instance['cloud'].nil? ? instance['cloud']['name'] : nil,
251
+ group: instance['group'] ? instance['group']['name'] : nil,
252
+ cloud: instance['cloud'] ? instance['cloud']['name'] : nil,
253
+ plan: instance['plan'] ? instance['plan']['name'] : '',
216
254
  version: instance['instanceVersion'] ? instance['instanceVersion'] : '',
217
255
  created: format_local_dt(instance['dateCreated']),
218
256
  cpu: cpu_usage_str + cyan,
@@ -226,12 +264,13 @@ class Morpheus::Cli::Instances
226
264
  {:created => {:display_name => "CREATED"}},
227
265
  # {:tenant => {:display_name => "TENANT"}},
228
266
  {:user => {:display_name => "OWNER", :max_width => 20}},
267
+ :plan,
229
268
  :nodes, {:connection => {:max_width => 30}}, :status, :cpu, :memory, :storage]
230
269
  # custom pretty table columns ... this is handled in as_pretty_table now(),
231
270
  # todo: remove all these.. and try to always pass rows as the json data itself..
232
- # if options[:include_fields]
233
- # columns = options[:include_fields]
234
- # end
271
+ if options[:details] != true
272
+ columns.delete(:plan)
273
+ end
235
274
  print cyan
236
275
  print as_pretty_table(rows, columns, options)
237
276
  print reset
@@ -344,18 +383,16 @@ class Morpheus::Cli::Instances
344
383
  opts.on("--environment ENV", String, "Environment code") do |val|
345
384
  options[:environment] = val.to_s
346
385
  end
347
- opts.on('--metadata LIST', String, "Metadata tags in the format 'name:value, name:value'") do |val|
386
+ opts.on('--tags LIST', String, "Metadata tags in the format 'ping=pong,flash=bang'") do |val|
348
387
  options[:metadata] = val
349
388
  end
350
- opts.on('--labels LIST', String, "Labels (keywords) in the format 'foo, bar'") do |val|
351
- #options[:tags] = val
352
- options[:tags] = val.split(',').collect {|it| it.to_s.strip }.compact.uniq.join(',')
389
+ opts.on('--metadata LIST', String, "Metadata tags in the format 'ping=pong,flash=bang'") do |val|
390
+ options[:metadata] = val
353
391
  end
354
- opts.on('--tags LIST', String, "Tags") do |val|
355
- #options[:tags] = val
356
- options[:tags] = val.split(',').collect {|it| it.to_s.strip }.compact.uniq.join(',')
392
+ opts.add_hidden_option('--metadata')
393
+ opts.on('--labels LIST', String, "Labels (keywords) in the format 'foo, bar'") do |val|
394
+ options[:labels] = val.split(',').collect {|it| it.to_s.strip }.compact.uniq.join(',')
357
395
  end
358
- opts.add_hidden_option('--tags')
359
396
  opts.on("--copies NUMBER", Integer, "Number of copies to provision") do |val|
360
397
  options[:copies] = val.to_i
361
398
  end
@@ -422,17 +459,58 @@ class Morpheus::Cli::Instances
422
459
  options[:instance_name] = args[0]
423
460
  end
424
461
 
425
- # use active group by default
426
- options[:group] ||= @active_group_id
427
- options[:select_datastore] = true
428
- options[:name_required] = true
429
462
  begin
430
463
  payload = nil
431
464
  if options[:payload]
432
465
  payload = options[:payload]
433
466
  # support -O OPTION switch on top of --payload
434
467
  payload.deep_merge!(options[:options].reject {|k,v| k.is_a?(Symbol) }) if options[:options]
468
+ # obviously should support every option that prompt supports on top of -- payload as well
469
+ # group, cloud and type for now
470
+ # todo: also support :layout, service_plan, :resource_pool, etc.
471
+ group = nil
472
+ if options[:group]
473
+ group = find_group_by_name_or_id_for_provisioning(options[:group])
474
+ if group.nil?
475
+ return 1, "group not found by #{options[:group]}"
476
+ end
477
+ #payload["siteId"] = group["id"]
478
+ payload.deep_merge!({"instance" => {"site" => {"id" => group["id"]} } })
479
+ end
480
+ if options[:cloud]
481
+ group_id = group ? group["id"] : ((payload["instance"] && payload["instance"]["site"].is_a?(Hash)) ? payload["instance"]["site"]["id"] : nil)
482
+ cloud = find_cloud_by_name_or_id_for_provisioning(group_id, options[:cloud])
483
+ if cloud.nil?
484
+ return 1, "cloud not found by #{options[:cloud]}"
485
+ end
486
+ payload["zoneId"] = cloud["id"]
487
+ payload.deep_merge!({"instance" => {"cloud" => cloud["name"] } })
488
+ end
489
+ if options[:cloud]
490
+ group_id = group ? group["id"] : ((payload["instance"] && payload["instance"]["site"].is_a?(Hash)) ? payload["instance"]["site"]["id"] : nil)
491
+ cloud = find_cloud_by_name_or_id_for_provisioning(group_id, options[:cloud])
492
+ if cloud.nil?
493
+ return 1, "cloud not found by #{options[:cloud]}"
494
+ end
495
+ payload["zoneId"] = cloud["id"]
496
+ payload.deep_merge!({"instance" => {"cloud" => cloud["name"] } })
497
+ end
498
+ if options[:instance_type_code]
499
+ # should just use find_instance_type_by_name_or_id
500
+ # note that the api actually will match name name or code
501
+ instance_type = (options[:instance_type_code].to_s =~ /\A\d{1,}\Z/) ? find_instance_type_by_id(options[:instance_type_code]) : find_instance_type_by_code(options[:instance_type_code])
502
+ if instance_type.nil?
503
+ return 1, "instance type not found by #{options[:cloud]}"
504
+ end
505
+ payload.deep_merge!({"instance" => {"type" => instance_type["code"] } })
506
+ payload.deep_merge!({"instance" => {"instanceType" => {"code" => instance_type["code"]} } })
507
+ end
508
+
435
509
  else
510
+ # use active group by default
511
+ options[:group] ||= @active_group_id
512
+ options[:select_datastore] = true
513
+ options[:name_required] = true
436
514
  # prompt for all the instance configuration options
437
515
  # this provisioning helper method handles all (most) of the parsing and prompting
438
516
  # and it relies on the method to exit non-zero on error, like a bad CLOUD or TYPE value
@@ -525,18 +603,18 @@ class Morpheus::Cli::Instances
525
603
  opts.on('--group GROUP', String, "Group Name or ID") do |val|
526
604
  options[:group] = val
527
605
  end
528
- opts.on('--metadata LIST', String, "Metadata tags in the format 'name:value, name:value'") do |val|
529
- options[:metadata] = val
530
- end
531
606
  opts.on('--labels LIST', String, "Labels (keywords) in the format 'foo, bar'") do |val|
532
- #params['tags'] = val
533
- params['tags'] = val.split(',').collect {|it| it.to_s.strip }.compact.uniq.join(',')
607
+ params['labels'] = val.split(',').collect {|it| it.to_s.strip }.compact.uniq.join(',')
608
+ end
609
+ opts.on('--tags LIST', String, "Tags in the format 'name:value, name:value'. This will add and remove tags.") do |val|
610
+ options[:tags] = val
611
+ end
612
+ opts.on('--add-tags TAGS', String, "Add Tags in the format 'name:value, name:value'. This will only add/update tags.") do |val|
613
+ options[:add_tags] = val
534
614
  end
535
- opts.on('--tags LIST', String, "Tags") do |val|
536
- #params['tags'] = val
537
- params['tags'] = val.split(',').collect {|it| it.to_s.strip }.compact.uniq.join(',')
615
+ opts.on('--remove-tags TAGS', String, "Remove Tags in the format 'name, name:value'. This removes tags, the :value component is optional and must match if passed.") do |val|
616
+ options[:remove_tags] = val
538
617
  end
539
- opts.add_hidden_option('--tags')
540
618
  opts.on('--power-schedule-type ID', String, "Power Schedule Type ID") do |val|
541
619
  params['powerScheduleType'] = val == "null" ? nil : val
542
620
  end
@@ -591,29 +669,17 @@ class Morpheus::Cli::Instances
591
669
  payload['instance']['site'] = {'id' => group['id']}
592
670
  end
593
671
  # metadata tags
594
- # if options[:options]['metadata'].is_a?(Array) && !options[:metadata]
595
- # options[:metadata] = options[:options]['metadata']
596
- # end
597
- if options[:metadata]
598
- metadata = []
599
- if options[:metadata] == "[]" || options[:metadata] == "null"
600
- payload['instance']['metadata'] = []
601
- elsif options[:metadata].is_a?(Array)
602
- payload['instance']['metadata'] = options[:metadata]
603
- else
604
- # parse string into format name:value, name:value
605
- # merge IDs from current metadata
606
- # todo: should allow quoted semicolons..
607
- metadata_list = options[:metadata].split(",").select {|it| !it.to_s.empty? }
608
- metadata_list = metadata_list.collect do |it|
609
- metadata_pair = it.split(":")
610
- row = {}
611
- row['name'] = metadata_pair[0].to_s.strip
612
- row['value'] = metadata_pair[1].to_s.strip
613
- row
614
- end
615
- payload['instance']['metadata'] = metadata_list
616
- end
672
+ if options[:tags]
673
+ # api version 4.2.5 and later supports tags, older versions expect metadata
674
+ # todo: use tags instead like everywhere else
675
+ # payload['instance']['tags'] = parse_metadata(options[:tags])
676
+ payload['instance']['metadata'] = parse_metadata(options[:tags])
677
+ end
678
+ if options[:add_tags]
679
+ payload['instance']['addTags'] = parse_metadata(options[:add_tags])
680
+ end
681
+ if options[:remove_tags]
682
+ payload['instance']['removeTags'] = parse_metadata(options[:remove_tags])
617
683
  end
618
684
  if payload['instance'].empty? && params.empty? && options[:owner].nil?
619
685
  raise_command_error "Specify at least one option to update.\n#{optparse}"
@@ -1148,6 +1214,9 @@ class Morpheus::Cli::Instances
1148
1214
  opts.on( nil, '--scaling', "Display Instance Scaling Settings" ) do
1149
1215
  options[:include_scaling] = true
1150
1216
  end
1217
+ opts.on( nil, '--costs', "Display Cost and Price" ) do
1218
+ options[:include_costs] = true
1219
+ end
1151
1220
  opts.on('--refresh [SECONDS]', String, "Refresh until status is running,failed. Default interval is #{default_refresh_interval} seconds.") do |val|
1152
1221
  options[:refresh_until_status] ||= "running,failed"
1153
1222
  if !val.to_s.empty?
@@ -1211,7 +1280,18 @@ class Morpheus::Cli::Instances
1211
1280
  instance = json_response['instance']
1212
1281
  stats = instance['stats'] || json_response['stats'] || {}
1213
1282
  # load_balancers = json_response['loadBalancers'] || {}
1214
-
1283
+ # metadata tags used to be returned as metadata and are now returned as tags
1284
+ # the problem is tags is what we used to call Labels (keywords)
1285
+ # the api will change to tags and labels, so handle the old format as long as metadata is returned.
1286
+ labels = nil
1287
+ tags = nil
1288
+ if instance.key?('labels')
1289
+ labels = instance['labels']
1290
+ tags = instance['tags']
1291
+ else
1292
+ labels = instance['tags']
1293
+ tags = instance['metadata']
1294
+ end
1215
1295
  # containers are fetched via separate api call
1216
1296
  containers = nil
1217
1297
  if options[:include_containers]
@@ -1249,9 +1329,11 @@ class Morpheus::Cli::Instances
1249
1329
  "Layout" => lambda {|it| it['layout'] ? it['layout']['name'] : '' },
1250
1330
  "Version" => lambda {|it| it['instanceVersion'] },
1251
1331
  "Plan" => lambda {|it| it['plan'] ? it['plan']['name'] : '' },
1332
+ # "Cost" => lambda {|it| it['hourlyCost'] ? format_money(it['hourlyCost'], (it['currency'] || 'USD'), {sigdig:15}).to_s + ' per hour' : '' },
1333
+ # "Price" => lambda {|it| it['hourlyPrice'] ? format_money(it['hourlyPrice'], (it['currency'] || 'USD'), {sigdig:15}).to_s + ' per hour' : '' },
1252
1334
  "Environment" => 'instanceContext',
1253
- "Labels" => lambda {|it| it['tags'] ? it['tags'].join(',') : '' },
1254
- "Metadata" => lambda {|it| it['metadata'] ? it['metadata'].collect {|m| "#{m['name']}: #{m['value']}" }.join(', ') : '' },
1335
+ "Labels" => lambda {|it| labels ? labels.join(',') : '' },
1336
+ "Tags" => lambda {|it| tags ? tags.collect {|m| "#{m['name']}: #{m['value']}" }.join(', ') : '' },
1255
1337
  "Owner" => lambda {|it|
1256
1338
  if it['owner']
1257
1339
  (it['owner']['username'] || it['owner']['id'])
@@ -1270,11 +1352,14 @@ class Morpheus::Cli::Instances
1270
1352
  "Connection" => lambda {|it| format_instance_connection_string(it) },
1271
1353
  "Status" => lambda {|it| format_instance_status(it) }
1272
1354
  }
1355
+ description_cols.delete("Labels") if labels.nil? || labels.empty?
1356
+ description_cols.delete("Tags") if tags.nil? || tags.empty?
1273
1357
  description_cols.delete("Power Schedule") if instance['powerSchedule'].nil?
1274
1358
  description_cols.delete("Expire Date") if instance['expireDate'].nil?
1275
1359
  description_cols.delete("Shutdown Date") if instance['shutdownDate'].nil?
1276
1360
  description_cols["Removal Date"] = lambda {|it| format_local_dt(it['removalDate'])} if instance['status'] == 'pendingRemoval'
1277
1361
  description_cols.delete("Last Deployment") if instance['lastDeploy'].nil?
1362
+ #description_cols.delete("Environment") if instance['instanceContext'].nil?
1278
1363
  print_description_list(description_cols, instance)
1279
1364
 
1280
1365
  if instance['statusMessage']
@@ -1304,6 +1389,16 @@ class Morpheus::Cli::Instances
1304
1389
  print_h2 "Instance Usage", options
1305
1390
  print_stats_usage(stats)
1306
1391
  end
1392
+
1393
+ if options[:include_costs]
1394
+ print_h2 "Instance Cost"
1395
+ cost_columns = {
1396
+ "Cost" => lambda {|it| it['hourlyCost'] ? format_money(it['hourlyCost'], (it['currency'] || 'USD'), {sigdig:15}).to_s + ' per hour' : '' },
1397
+ "Price" => lambda {|it| it['hourlyPrice'] ? format_money(it['hourlyPrice'], (it['currency'] || 'USD'), {sigdig:15}).to_s + ' per hour' : '' },
1398
+ }
1399
+ print_description_list(cost_columns, instance)
1400
+ end
1401
+
1307
1402
  print reset, "\n"
1308
1403
 
1309
1404
  # if options[:include_lb]
@@ -2578,6 +2673,48 @@ class Morpheus::Cli::Instances
2578
2673
  end
2579
2674
  end
2580
2675
 
2676
+ def snapshot(args)
2677
+ options = {}
2678
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
2679
+ opts.banner = subcommand_usage("[instance]")
2680
+ opts.on( '--name VALUE', String, "Snapshot Name. Default is server name + timestamp" ) do |val|
2681
+ options[:options]['name'] = val
2682
+ end
2683
+ opts.on( '--description VALUE', String, "Snapshot Description." ) do |val|
2684
+ options[:options]['description'] = val
2685
+ end
2686
+ build_standard_add_options(opts, options, [:auto_confirm])
2687
+ opts.footer = <<-EOT
2688
+ Create a snapshot for an instance.
2689
+ [instance] is required. This is the name or id of an instance
2690
+ EOT
2691
+ end
2692
+ optparse.parse!(args)
2693
+ verify_args!(args:args, optparse:optparse, count:1)
2694
+ connect(options)
2695
+ instance = find_instance_by_name_or_id(args[0])
2696
+ unless options[:yes] || ::Morpheus::Cli::OptionTypes::confirm("Are you sure you would like to snapshot the instance '#{instance['name']}'?", options)
2697
+ exit 1
2698
+ end
2699
+ payload = {}
2700
+ if options[:payload]
2701
+ payload = options[:payload]
2702
+ payload.deep_merge!({'snapshot' => parse_passed_options(options)})
2703
+ else
2704
+ payload.deep_merge!({'snapshot' => parse_passed_options(options)})
2705
+ end
2706
+ @instances_interface.setopts(options)
2707
+ if options[:dry_run]
2708
+ print_dry_run @instances_interface.dry.snapshot(instance['id'], payload)
2709
+ return
2710
+ end
2711
+ json_response = @instances_interface.snapshot(instance['id'], payload)
2712
+ render_response(json_response, options, 'snapshots') do
2713
+ print_green_success "Snapshot initiated."
2714
+ end
2715
+ return 0, nil
2716
+ end
2717
+
2581
2718
  def remove(args)
2582
2719
  options = {}
2583
2720
  query_params = {}
@@ -2880,6 +3017,57 @@ class Morpheus::Cli::Instances
2880
3017
  end
2881
3018
  end
2882
3019
 
3020
+ def snapshots(args)
3021
+ options = {}
3022
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
3023
+ opts.banner = subcommand_usage("[instance]")
3024
+ # no pagination yet
3025
+ # build_standard_list_options(opts, options)
3026
+ build_standard_get_options(opts, options)
3027
+ opts.footer = <<-EOT
3028
+ List snapshots for an instance.
3029
+ [instance] is required. This is the name or id of an instance
3030
+ EOT
3031
+ end
3032
+ optparse.parse!(args)
3033
+ verify_args!(args:args, optparse:optparse, count:1)
3034
+ connect(options)
3035
+ begin
3036
+ instance = find_instance_by_name_or_id(args[0])
3037
+ params = {}
3038
+ @instances_interface.setopts(options)
3039
+ if options[:dry_run]
3040
+ print_dry_run @instances_interface.dry.snapshots(instance['id'], params)
3041
+ return
3042
+ end
3043
+ json_response = @instances_interface.snapshots(instance['id'], params)
3044
+ snapshots = json_response['snapshots']
3045
+ render_response(json_response, options, 'snapshots') do
3046
+ print_h1 "Snapshots: #{instance['name']} (#{instance['instanceType']['name']})", [], options
3047
+ if snapshots.empty?
3048
+ print cyan,"No snapshots found",reset,"\n"
3049
+ else
3050
+ snapshot_column_definitions = {
3051
+ "ID" => lambda {|it| it['id'] },
3052
+ "Name" => lambda {|it| it['name'] },
3053
+ "Description" => lambda {|it| it['description'] },
3054
+ # "Type" => lambda {|it| it['snapshotType'] },
3055
+ "Date Created" => lambda {|it| format_local_dt(it['snapshotCreated']) },
3056
+ "Status" => lambda {|it| format_snapshot_status(it) }
3057
+ }
3058
+ print cyan
3059
+ print as_pretty_table(snapshots, snapshot_column_definitions.upcase_keys!, options)
3060
+ print_results_pagination({size: snapshots.size, total: snapshots.size})
3061
+ end
3062
+ print reset, "\n"
3063
+ end
3064
+ return 0
3065
+ rescue RestClient::Exception => e
3066
+ print_rest_exception(e, options)
3067
+ exit 1
3068
+ end
3069
+ end
3070
+
2883
3071
  def import_snapshot(args)
2884
3072
  options = {}
2885
3073
  query_params = {}
@@ -3792,51 +3980,6 @@ private
3792
3980
  end
3793
3981
  end
3794
3982
 
3795
- def format_instance_status(instance, return_color=cyan)
3796
- out = ""
3797
- status_string = instance['status'].to_s
3798
- if status_string == 'running'
3799
- out << "#{green}#{status_string.upcase}#{return_color}"
3800
- elsif status_string == 'provisioning'
3801
- out << "#{cyan}#{status_string.upcase}#{return_color}"
3802
- elsif status_string == 'stopped' or status_string == 'failed'
3803
- out << "#{red}#{status_string.upcase}#{return_color}"
3804
- else
3805
- out << "#{yellow}#{status_string.upcase}#{return_color}"
3806
- end
3807
- out
3808
- end
3809
-
3810
- def format_instance_connection_string(instance)
3811
- if !instance['connectionInfo'].nil? && instance['connectionInfo'].empty? == false
3812
- connection_string = "#{instance['connectionInfo'][0]['ip']}:#{instance['connectionInfo'][0]['port']}"
3813
- end
3814
- end
3815
-
3816
- def format_container_status(container, return_color=cyan)
3817
- out = ""
3818
- status_string = container['status'].to_s
3819
- if status_string == 'running'
3820
- out << "#{green}#{status_string.upcase}#{return_color}"
3821
- elsif status_string == 'provisioning'
3822
- out << "#{cyan}#{status_string.upcase}#{return_color}"
3823
- elsif status_string == 'stopped' or status_string == 'failed'
3824
- out << "#{red}#{status_string.upcase}#{return_color}"
3825
- else
3826
- out << "#{yellow}#{status_string.upcase}#{return_color}"
3827
- end
3828
- out
3829
- end
3830
-
3831
- def format_container_connection_string(container)
3832
- if !container['ports'].nil? && container['ports'].empty? == false
3833
- connection_string = "#{container['ip']}:#{container['ports'][0]['external']}"
3834
- else
3835
- # eh? more logic needed here i think, see taglib morph:containerLocationMenu
3836
- connection_string = "#{container['ip']}"
3837
- end
3838
- end
3839
-
3840
3983
  def instance_scaling_option_types(instance=nil)
3841
3984
 
3842
3985
  # Group
@@ -3890,17 +4033,6 @@ private
3890
4033
  list
3891
4034
  end
3892
4035
 
3893
- def format_instance_container_display_name(instance, plural=false)
3894
- #<span class="info-label">${[null,'docker'].contains(instance.layout?.provisionType?.code) ? 'Containers' : 'Virtual Machines'}:</span> <span class="info-value">${instance.containers?.size()}</span>
3895
- v = plural ? "Containers" : "Container"
3896
- if instance && instance['layout'] && instance['layout'].key?("provisionTypeCode")
3897
- if [nil, 'docker'].include?(instance['layout']["provisionTypeCode"])
3898
- v = plural ? "Virtual Machines" : "Virtual Machine"
3899
- end
3900
- end
3901
- return v
3902
- end
3903
-
3904
4036
  def print_instance_threshold_description_list(instance_threshold)
3905
4037
  description_cols = {
3906
4038
  # "Instance" => lambda {|it| "#{instance['id']} - #{instance['name']}" },
@@ -4006,19 +4138,4 @@ private
4006
4138
  }
4007
4139
  end
4008
4140
 
4009
- def format_app_deploy_status(status, return_color=cyan)
4010
- out = ""
4011
- s = status.to_s.downcase
4012
- if s == 'deployed'
4013
- out << "#{green}#{s.upcase}#{return_color}"
4014
- elsif s == 'open' || s == 'archived' || s == 'committed'
4015
- out << "#{cyan}#{s.upcase}#{return_color}"
4016
- elsif s == 'failed'
4017
- out << "#{red}#{s.upcase}#{return_color}"
4018
- else
4019
- out << "#{yellow}#{s.upcase}#{return_color}"
4020
- end
4021
- out
4022
- end
4023
-
4024
4141
  end