morpheus-cli 5.0.1 → 5.2.3

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 (39) 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 +4 -0
  5. data/lib/morpheus/api/instances_interface.rb +9 -2
  6. data/lib/morpheus/api/invoices_interface.rb +12 -3
  7. data/lib/morpheus/api/servers_interface.rb +7 -0
  8. data/lib/morpheus/api/service_catalog_interface.rb +89 -0
  9. data/lib/morpheus/cli.rb +2 -1
  10. data/lib/morpheus/cli/apps.rb +3 -23
  11. data/lib/morpheus/cli/budgets_command.rb +389 -319
  12. data/lib/morpheus/cli/{catalog_command.rb → catalog_item_types_command.rb} +182 -67
  13. data/lib/morpheus/cli/cli_command.rb +25 -1
  14. data/lib/morpheus/cli/commands/standard/curl_command.rb +25 -10
  15. data/lib/morpheus/cli/commands/standard/history_command.rb +6 -2
  16. data/lib/morpheus/cli/containers_command.rb +0 -24
  17. data/lib/morpheus/cli/cypher_command.rb +6 -2
  18. data/lib/morpheus/cli/dashboard_command.rb +260 -20
  19. data/lib/morpheus/cli/groups.rb +1 -1
  20. data/lib/morpheus/cli/health_command.rb +57 -0
  21. data/lib/morpheus/cli/hosts.rb +111 -16
  22. data/lib/morpheus/cli/instances.rb +156 -108
  23. data/lib/morpheus/cli/invoices_command.rb +67 -4
  24. data/lib/morpheus/cli/jobs_command.rb +94 -92
  25. data/lib/morpheus/cli/library_option_lists_command.rb +1 -1
  26. data/lib/morpheus/cli/library_option_types_command.rb +10 -5
  27. data/lib/morpheus/cli/mixins/accounts_helper.rb +5 -1
  28. data/lib/morpheus/cli/mixins/print_helper.rb +13 -6
  29. data/lib/morpheus/cli/mixins/provisioning_helper.rb +88 -5
  30. data/lib/morpheus/cli/option_types.rb +10 -10
  31. data/lib/morpheus/cli/projects_command.rb +1 -1
  32. data/lib/morpheus/cli/roles.rb +193 -155
  33. data/lib/morpheus/cli/service_catalog_command.rb +1474 -0
  34. data/lib/morpheus/cli/tasks.rb +9 -11
  35. data/lib/morpheus/cli/version.rb +1 -1
  36. data/lib/morpheus/cli/virtual_images.rb +162 -68
  37. data/lib/morpheus/formatters.rb +55 -20
  38. metadata +5 -4
  39. data/lib/morpheus/cli/mixins/catalog_helper.rb +0 -66
@@ -52,7 +52,7 @@ class Morpheus::Cli::Groups
52
52
  end
53
53
  json_response = @groups_interface.list(params)
54
54
  exit_code, err = 0, nil
55
- render_response(json_response, options) do
55
+ render_response(json_response, options, "groups") do
56
56
  groups = json_response['groups']
57
57
  subtitles = []
58
58
  subtitles += parse_list_subtitles(options)
@@ -30,6 +30,7 @@ class Morpheus::Cli::HealthCommand
30
30
  opts.on('-a', '--all', "Display all details: CPU, Memory, Database, etc." ) do
31
31
  options[:details] = true
32
32
  options[:show_cpu] = true
33
+ options[:show_threads] = true
33
34
  options[:show_memory] = true
34
35
  options[:show_database] = true
35
36
  options[:show_elastic] = true
@@ -47,6 +48,9 @@ class Morpheus::Cli::HealthCommand
47
48
  opts.on('--cpu', "Display CPU details" ) do
48
49
  options[:show_cpu] = true
49
50
  end
51
+ opts.on('--threads', "Display Thread details" ) do
52
+ options[:show_threads] = true
53
+ end
50
54
  opts.on('--memory', "Display Memory details" ) do
51
55
  options[:show_memory] = true
52
56
  end
@@ -184,6 +188,59 @@ class Morpheus::Cli::HealthCommand
184
188
  end
185
189
  end
186
190
 
191
+ # Threads ()
192
+ if options[:show_threads]
193
+ print_h2 "Threads", options
194
+ if health['threads'].nil?
195
+ print yellow,"No thread information returned.",reset,"\n\n"
196
+ else
197
+ print cyan
198
+
199
+ thread_summary_columns = {
200
+ "Thread Count" => lambda {|it| it['totalThreads'].size rescue '' },
201
+ "Busy Threads" => lambda {|it| it['busyThreads'].size rescue '' },
202
+ "Running Threads" => lambda {|it| it['runningThreads'].size rescue '' },
203
+ "Blocked Threads" => lambda {|it| it['blockedThreads'].size rescue '' },
204
+ }
205
+ print_description_list(thread_summary_columns, health['threads'], options)
206
+
207
+
208
+ thread_columns = [
209
+ {"Name".upcase => lambda {|it| it['name']} },
210
+ {"Status".upcase => lambda {|it|
211
+ # hrmm
212
+ status_string = (it['status'] || it['state']).to_s.downcase
213
+ status_color = cyan
214
+ # if status_string.include?('waiting')
215
+ # status_color = yellow
216
+ # end
217
+ "#{status_color}#{status_string.upcase}#{cyan}"
218
+ } },
219
+ # {"CPU Time" => lambda {|it| it['cpuTime'].to_s } },
220
+ # {"CPU Time" => lambda {|it| format_human_duration(it['cpuTime'].to_f / 1000) rescue '' } },
221
+ {"CPU Percent" => lambda {|it| it['cpuPercent'].to_i.to_s + '%' } }
222
+ ]
223
+
224
+ if health['threads']['busyThreads'] && health['threads']['busyThreads'].size > 0
225
+ print_h2 "Busy Threads"
226
+ print cyan
227
+ print as_pretty_table(health['threads']['busyThreads'], thread_columns, options)
228
+ end
229
+
230
+ if health['threads']['runningThreads'] && health['threads']['runningThreads'].size > 0
231
+ print_h2 "Running Threads"
232
+ print cyan
233
+ print as_pretty_table(health['threads']['runningThreads'], thread_columns, options)
234
+ end
235
+
236
+ if health['threads']['blockedThreads'] && health['threads']['blockedThreads'].size > 0
237
+ print_h2 "Blocked Threads"
238
+ print cyan
239
+ print as_pretty_table(health['threads']['blockedThreads'], thread_columns, options)
240
+ end
241
+ end
242
+ end
243
+
187
244
  # Memory
188
245
  if options[:show_memory]
189
246
  if health['memory'].nil?
@@ -17,7 +17,7 @@ class Morpheus::Cli::Hosts
17
17
  set_command_name :hosts
18
18
  set_command_description "View and manage hosts (servers)."
19
19
  register_subcommands :list, :count, :get, :view, :stats, :add, :update, :remove, :logs, :start, :stop, :resize,
20
- :run_workflow, :make_managed, :upgrade_agent, :snapshots,
20
+ :run_workflow, :make_managed, :upgrade_agent, :snapshots, :software,
21
21
  {:'types' => :list_types},
22
22
  {:exec => :execution_request},
23
23
  :wiki, :update_wiki
@@ -54,13 +54,6 @@ class Morpheus::Cli::Hosts
54
54
  params = {}
55
55
  optparse = Morpheus::Cli::OptionParser.new do |opts|
56
56
  opts.banner = subcommand_usage()
57
- opts.on('-a', '--all', "Display all details: memory and storage usage used / max values." ) do
58
- options[:details] = true
59
- end
60
- opts.on('--details', "Display all details: alias for --all" ) do
61
- options[:details] = true
62
- end
63
- opts.add_hidden_option('--details')
64
57
  opts.on( '-g', '--group GROUP', "Group Name or ID" ) do |val|
65
58
  options[:group] = val
66
59
  end
@@ -86,6 +79,17 @@ class Morpheus::Cli::Hosts
86
79
  # params[:clusterId] = val
87
80
  options[:cluster] = val
88
81
  end
82
+ opts.on( '--plan NAME', String, "Filter by Plan name(s)" ) do |val|
83
+ # commas used in names a lot so use --plan one --plan two
84
+ params['plan'] ||= []
85
+ params['plan'] << val
86
+ end
87
+ opts.on( '--plan-id ID', String, "Filter by Plan id(s)" ) do |val|
88
+ params['planId'] = parse_id_list(val)
89
+ end
90
+ opts.on( '--plan-code CODE', String, "Filter by Plan code(s)" ) do |val|
91
+ params['planCode'] = parse_id_list(val)
92
+ end
89
93
  opts.on( '', '--vm', "Show only virtual machines" ) do |val|
90
94
  params[:vm] = true
91
95
  end
@@ -113,9 +117,6 @@ class Morpheus::Cli::Hosts
113
117
  opts.on( '--tenant TENANT', "Tenant Name or ID" ) do |val|
114
118
  options[:account] = val
115
119
  end
116
- opts.on('--details', "Display more details: memory and storage usage used / max values." ) do
117
- options[:details] = true
118
- end
119
120
  opts.on('--tags Name=Value',String, "Filter by tags.") do |val|
120
121
  val.split(",").each do |value_pair|
121
122
  k,v = value_pair.strip.split("=")
@@ -130,6 +131,12 @@ class Morpheus::Cli::Hosts
130
131
  opts.on('--non-tag-compliant', "Displays only servers with tag compliance warnings." ) do
131
132
  params[:tagCompliant] = false
132
133
  end
134
+ opts.on('--stats', "Display values for memory and storage usage used / max values." ) do
135
+ options[:stats] = true
136
+ end
137
+ opts.on('-a', '--details', "Display all details: hostname, private ip, plan, stats, etc." ) do
138
+ options[:details] = true
139
+ end
133
140
  build_standard_list_options(opts, options)
134
141
  opts.footer = "List hosts."
135
142
  end
@@ -258,7 +265,7 @@ class Morpheus::Cli::Hosts
258
265
  cpu_usage_str = !stats ? "" : generate_usage_bar((stats['usedCpu'] || stats['cpuUsage']).to_f, 100, {max_bars: 10})
259
266
  memory_usage_str = !stats ? "" : generate_usage_bar(stats['usedMemory'], stats['maxMemory'], {max_bars: 10})
260
267
  storage_usage_str = !stats ? "" : generate_usage_bar(stats['usedStorage'], stats['maxStorage'], {max_bars: 10})
261
- if options[:details]
268
+ if options[:details] || options[:stats]
262
269
  if stats['maxMemory'] && stats['maxMemory'].to_i != 0
263
270
  memory_usage_str = memory_usage_str + cyan + format_bytes_short(stats['usedMemory']).strip.rjust(8, ' ') + " / " + format_bytes_short(stats['maxMemory']).strip
264
271
  end
@@ -269,12 +276,14 @@ class Morpheus::Cli::Hosts
269
276
  row = {
270
277
  id: server['id'],
271
278
  name: server['name'],
279
+ external_name: server['externalName'],
272
280
  hostname: server['hostname'],
273
281
  platform: server['serverOs'] ? server['serverOs']['name'].upcase : 'N/A',
274
282
  type: server['computeServerType'] ? server['computeServerType']['name'] : 'unmanaged',
275
283
  tenant: server['account'] ? server['account']['name'] : server['accountId'],
276
284
  owner: server['owner'] ? server['owner']['username'] : server['owner'],
277
285
  cloud: server['zone'] ? server['zone']['name'] : '',
286
+ plan: server['plan'] ? server['plan']['name'] : '',
278
287
  ip: server['externalIp'],
279
288
  internal_ip: server['internalIp'],
280
289
  nodes: server['containers'] ? server['containers'].size : '',
@@ -293,11 +302,13 @@ class Morpheus::Cli::Hosts
293
302
  columns = {
294
303
  "ID" => :id,
295
304
  "Name" => :name,
305
+ "External Name" => :external_name,
296
306
  "Hostname" => :hostname,
297
307
  "Type" => :type,
298
308
  "Owner" => :owner,
299
309
  "Tenant" => :tenant,
300
310
  "Cloud" => :cloud,
311
+ "Plan" => :plan,
301
312
  "IP" => :ip,
302
313
  "Private IP" => :internal_ip,
303
314
  "Nodes" => :nodes,
@@ -310,7 +321,9 @@ class Morpheus::Cli::Hosts
310
321
  "Updated" => :updated,
311
322
  }
312
323
  if options[:details] != true
324
+ columns.delete("External Name")
313
325
  columns.delete("Hostname")
326
+ columns.delete("Plan")
314
327
  columns.delete("Private IP")
315
328
  columns.delete("Owner")
316
329
  columns.delete("Tenant")
@@ -318,6 +331,10 @@ class Morpheus::Cli::Hosts
318
331
  columns.delete("Created")
319
332
  columns.delete("Updated")
320
333
  end
334
+ # hide External Name if there are none
335
+ if !servers.find {|it| it['externalName'] && it['externalName'] != it['name']}
336
+ columns.delete("External Name")
337
+ end
321
338
  if !multi_tenant
322
339
  columns.delete("Tenant")
323
340
  end
@@ -344,7 +361,7 @@ class Morpheus::Cli::Hosts
344
361
  options = {}
345
362
  optparse = Morpheus::Cli::OptionParser.new do |opts|
346
363
  opts.banner = subcommand_usage("[options]")
347
- opts.on( '-a', '--account ACCOUNT', "Account Name or ID" ) do |val|
364
+ opts.on( '--tenant TENANT', "Tenant Name or ID" ) do |val|
348
365
  options[:account] = val
349
366
  end
350
367
  opts.on( '-g', '--group GROUP', "Group Name or ID" ) do |val|
@@ -513,9 +530,10 @@ class Morpheus::Cli::Hosts
513
530
  puts records_as_csv([json_response['server']], options)
514
531
  return 0
515
532
  end
516
- server = json_response['server']
533
+ server = json_response['server'] || json_response['host'] || {}
517
534
  #stats = server['stats'] || json_response['stats'] || {}
518
535
  stats = json_response['stats'] || {}
536
+ tags = server['tags'] || server['metadata']
519
537
  title = "Host Details"
520
538
  print_h1 title, [], options
521
539
  print cyan
@@ -524,6 +542,7 @@ class Morpheus::Cli::Hosts
524
542
  "Name" => 'name',
525
543
  "Hostname" => 'hostname',
526
544
  "Description" => 'description',
545
+ "Tags" => lambda {|it| tags ? format_metadata(tags) : '' },
527
546
  "Owner" => lambda {|it| it['owner'] ? it['owner']['username'] : '' },
528
547
  "Tenant" => lambda {|it| it['account'] ? it['account']['name'] : '' },
529
548
  #"Group" => lambda {|it| it['group'] ? it['group']['name'] : '' },
@@ -547,6 +566,7 @@ class Morpheus::Cli::Hosts
547
566
  # server_columns.delete("Tenant") if multi_tenant != true
548
567
  server_columns.delete("Cost") if server['hourlyCost'].to_f == 0
549
568
  server_columns.delete("Price") if server['hourlyPrice'].to_f == 0 || server['hourlyPrice'] == server['hourlyCost']
569
+ server_columns.delete("Tags") if tags.nil? || tags.empty?
550
570
 
551
571
  print_description_list(server_columns, server)
552
572
 
@@ -973,6 +993,19 @@ class Morpheus::Cli::Hosts
973
993
  opts.on('--power-schedule-type ID', String, "Power Schedule Type ID") do |val|
974
994
  params['powerScheduleType'] = val == "null" ? nil : val
975
995
  end
996
+ opts.on('--tags LIST', String, "Tags in the format 'name:value, name:value'. This will add and remove tags.") do |val|
997
+ options[:tags] = val
998
+ end
999
+ opts.on('--metadata LIST', String, "Alias for --tags.") do |val|
1000
+ options[:tags] = val
1001
+ end
1002
+ opts.add_hidden_option('--metadata')
1003
+ opts.on('--add-tags TAGS', String, "Add Tags in the format 'name:value, name:value'. This will only add/update tags.") do |val|
1004
+ options[:add_tags] = val
1005
+ end
1006
+ 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|
1007
+ options[:remove_tags] = val
1008
+ end
976
1009
  # opts.on('--created-by ID', String, "Created By User ID") do |val|
977
1010
  # params['createdById'] = val
978
1011
  # end
@@ -991,6 +1024,18 @@ class Morpheus::Cli::Hosts
991
1024
  new_group = nil
992
1025
  passed_options = options[:options] ? options[:options].reject {|k,v| k.is_a?(Symbol) } : {}
993
1026
  params.deep_merge!(passed_options) unless passed_options.empty?
1027
+ # metadata tags
1028
+ if options[:tags]
1029
+ params['tags'] = parse_metadata(options[:tags])
1030
+ else
1031
+ # params['tags'] = prompt_metadata(options)
1032
+ end
1033
+ if options[:add_tags]
1034
+ params['addTags'] = parse_metadata(options[:add_tags])
1035
+ end
1036
+ if options[:remove_tags]
1037
+ params['removeTags'] = parse_metadata(options[:remove_tags])
1038
+ end
994
1039
  payload = nil
995
1040
  if options[:payload]
996
1041
  payload = options[:payload]
@@ -1902,7 +1947,8 @@ class Morpheus::Cli::Hosts
1902
1947
  snapshot_column_definitions = {
1903
1948
  "ID" => lambda {|it| it['id'] },
1904
1949
  "Name" => lambda {|it| it['name'] },
1905
- "Description" => lambda {|it| it['snapshotType'] ? (it['snapshotType']['name'] || it['snapshotType']['code']) : '' },
1950
+ "Description" => lambda {|it| it['description'] },
1951
+ # "Type" => lambda {|it| it['snapshotType'] },
1906
1952
  "Date Created" => lambda {|it| format_local_dt(it['snapshotCreated']) },
1907
1953
  "Status" => lambda {|it| format_snapshot_status(it) }
1908
1954
  }
@@ -1919,6 +1965,55 @@ class Morpheus::Cli::Hosts
1919
1965
  end
1920
1966
  end
1921
1967
 
1968
+ def software(args)
1969
+ options = {}
1970
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
1971
+ opts.banner = subcommand_usage("[host]")
1972
+ build_standard_list_options(opts, options)
1973
+ end
1974
+ optparse.parse!(args)
1975
+ verify_args!(args:args, optparse:optparse, count:1)
1976
+ connect(options)
1977
+ begin
1978
+ server = find_host_by_name_or_id(args[0])
1979
+ return 1 if server.nil?
1980
+ params = {}
1981
+ params.merge!(parse_list_options(options))
1982
+ @servers_interface.setopts(options)
1983
+ if options[:dry_run]
1984
+ print_dry_run @servers_interface.dry.software(server['id'], params)
1985
+ return
1986
+ end
1987
+ json_response = @servers_interface.software(server['id'], params)
1988
+ software = json_response['software']
1989
+ render_response(json_response, options, 'software') do
1990
+ print_h1 "Software: #{server['name']}", [], options
1991
+ if software.empty?
1992
+ print cyan,"No software found",reset,"\n"
1993
+ else
1994
+ software_column_definitions = {
1995
+ # "ID" => lambda {|it| it['id'] },
1996
+ "Name" => lambda {|it| it['name'] },
1997
+ "Version" => lambda {|it| it['packageVersion'] },
1998
+ "Publisher" => lambda {|it| it['packagePublisher'] },
1999
+ # "Release" => lambda {|it| it['packageRelease'] },
2000
+ # "Type" => lambda {|it| it['packageType'] },
2001
+ # "Architecture" => lambda {|it| it['architecture'] },
2002
+ # "Install Date" => lambda {|it| format_local_dt(it['installDate']) },
2003
+ }
2004
+ print cyan
2005
+ print as_pretty_table(software, software_column_definitions.upcase_keys!, options)
2006
+ print_results_pagination({size: software.size, total: software.size})
2007
+ end
2008
+ print reset, "\n"
2009
+ end
2010
+ return 0
2011
+ rescue RestClient::Exception => e
2012
+ print_rest_exception(e, options)
2013
+ exit 1
2014
+ end
2015
+ end
2016
+
1922
2017
  private
1923
2018
 
1924
2019
  def find_host_by_id(id)
@@ -2071,7 +2166,7 @@ class Morpheus::Cli::Hosts
2071
2166
  def make_managed_option_types(connected=true)
2072
2167
  [
2073
2168
  #{'fieldName' => 'account', 'fieldLabel' => 'Account', 'type' => 'select', 'optionSource' => 'accounts', 'required' => true},
2074
- {'fieldName' => 'sshUsername', 'fieldLabel' => 'SSH Username', 'type' => 'text', 'required' => true},
2169
+ {'fieldName' => 'sshUsername', 'fieldLabel' => 'SSH Username', 'type' => 'text'},
2075
2170
  {'fieldName' => 'sshPassword', 'fieldLabel' => 'SSH Password', 'type' => 'password', 'required' => false},
2076
2171
  {'fieldName' => 'serverOs', 'fieldLabel' => 'OS Type', 'type' => 'select', 'optionSource' => 'osTypes', 'required' => false},
2077
2172
  ]
@@ -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, :snapshots,
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,6 +89,17 @@ 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
95
103
  opts.on('--labels label',String, "Filter by labels (keywords).") do |val|
96
104
  val.split(",").each do |k|
97
105
  options[:labels] ||= []
@@ -106,6 +114,12 @@ class Morpheus::Cli::Instances
106
114
  options[:tags][k] << (v || '')
107
115
  end
108
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
109
123
  build_common_options(opts, options, [:list, :query, :json, :yaml, :csv, :fields, :dry_run, :remote])
110
124
  opts.footer = "List instances."
111
125
  end
@@ -216,7 +230,7 @@ class Morpheus::Cli::Instances
216
230
  cpu_usage_str = !stats ? "" : generate_usage_bar((stats['usedCpu'] || stats['cpuUsage']).to_f, 100, {max_bars: 10})
217
231
  memory_usage_str = !stats ? "" : generate_usage_bar(stats['usedMemory'], stats['maxMemory'], {max_bars: 10})
218
232
  storage_usage_str = !stats ? "" : generate_usage_bar(stats['usedStorage'], stats['maxStorage'], {max_bars: 10})
219
- if options[:details]
233
+ if options[:details] || options[:stats]
220
234
  if stats['maxMemory'] && stats['maxMemory'].to_i != 0
221
235
  memory_usage_str = memory_usage_str + cyan + format_bytes_short(stats['usedMemory']).strip.rjust(8, ' ') + " / " + format_bytes_short(stats['maxMemory']).strip
222
236
  end
@@ -234,8 +248,9 @@ class Morpheus::Cli::Instances
234
248
  nodes: instance['containers'].count,
235
249
  status: format_instance_status(instance, cyan),
236
250
  type: instance['instanceType']['name'],
237
- group: !instance['group'].nil? ? instance['group']['name'] : nil,
238
- 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'] : '',
239
254
  version: instance['instanceVersion'] ? instance['instanceVersion'] : '',
240
255
  created: format_local_dt(instance['dateCreated']),
241
256
  cpu: cpu_usage_str + cyan,
@@ -249,12 +264,13 @@ class Morpheus::Cli::Instances
249
264
  {:created => {:display_name => "CREATED"}},
250
265
  # {:tenant => {:display_name => "TENANT"}},
251
266
  {:user => {:display_name => "OWNER", :max_width => 20}},
267
+ :plan,
252
268
  :nodes, {:connection => {:max_width => 30}}, :status, :cpu, :memory, :storage]
253
269
  # custom pretty table columns ... this is handled in as_pretty_table now(),
254
270
  # todo: remove all these.. and try to always pass rows as the json data itself..
255
- # if options[:include_fields]
256
- # columns = options[:include_fields]
257
- # end
271
+ if options[:details] != true
272
+ columns.delete(:plan)
273
+ end
258
274
  print cyan
259
275
  print as_pretty_table(rows, columns, options)
260
276
  print reset
@@ -443,17 +459,58 @@ class Morpheus::Cli::Instances
443
459
  options[:instance_name] = args[0]
444
460
  end
445
461
 
446
- # use active group by default
447
- options[:group] ||= @active_group_id
448
- options[:select_datastore] = true
449
- options[:name_required] = true
450
462
  begin
451
463
  payload = nil
452
464
  if options[:payload]
453
465
  payload = options[:payload]
454
466
  # support -O OPTION switch on top of --payload
455
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
+
456
509
  else
510
+ # use active group by default
511
+ options[:group] ||= @active_group_id
512
+ options[:select_datastore] = true
513
+ options[:name_required] = true
457
514
  # prompt for all the instance configuration options
458
515
  # this provisioning helper method handles all (most) of the parsing and prompting
459
516
  # and it relies on the method to exit non-zero on error, like a bad CLOUD or TYPE value
@@ -546,16 +603,17 @@ class Morpheus::Cli::Instances
546
603
  opts.on('--group GROUP', String, "Group Name or ID") do |val|
547
604
  options[:group] = val
548
605
  end
549
- opts.on('--tags LIST', String, "Metadata tags in the format 'ping=pong,flash=bang'") do |val|
550
- options[:metadata] = val
606
+ opts.on('--labels LIST', String, "Labels (keywords) in the format 'foo, bar'") do |val|
607
+ params['labels'] = val.split(',').collect {|it| it.to_s.strip }.compact.uniq.join(',')
551
608
  end
552
- opts.on('--metadata LIST', String, "Metadata tags in the format 'ping=pong,flash=bang'") do |val|
553
- options[:metadata] = val
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
554
611
  end
555
- opts.add_hidden_option('--metadata')
556
- opts.on('--labels LIST', String, "Labels (keywords) in the format 'foo, bar'") do |val|
557
- # todo switch this from 'tags' to 'labels'
558
- params['tags'] = val.split(',').collect {|it| it.to_s.strip }.compact.uniq.join(',')
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
614
+ end
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
559
617
  end
560
618
  opts.on('--power-schedule-type ID', String, "Power Schedule Type ID") do |val|
561
619
  params['powerScheduleType'] = val == "null" ? nil : val
@@ -611,32 +669,17 @@ class Morpheus::Cli::Instances
611
669
  payload['instance']['site'] = {'id' => group['id']}
612
670
  end
613
671
  # metadata tags
614
- # if options[:options]['metadata'].is_a?(Array) && !options[:metadata]
615
- # options[:metadata] = options[:options]['metadata']
616
- # end
617
- if options[:metadata]
618
- metadata = []
619
- if options[:metadata] == "[]" || options[:metadata] == "null"
620
- payload['instance']['metadata'] = []
621
- elsif options[:metadata].is_a?(Array)
622
- payload['instance']['metadata'] = options[:metadata]
623
- else
624
- # parse string into format name:value, name:value
625
- # merge IDs from current metadata
626
- # todo: should allow quoted semicolons..
627
- metadata_list = options[:metadata].split(",").select {|it| !it.to_s.empty? }
628
- metadata_list = metadata_list.collect do |it|
629
- metadata_pair = it.split(":")
630
- if metadata_pair.size == 1 && it.include?("=")
631
- metadata_pair = it.split("=")
632
- end
633
- row = {}
634
- row['name'] = metadata_pair[0].to_s.strip
635
- row['value'] = metadata_pair[1].to_s.strip
636
- row
637
- end
638
- payload['instance']['metadata'] = metadata_list
639
- 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])
640
683
  end
641
684
  if payload['instance'].empty? && params.empty? && options[:owner].nil?
642
685
  raise_command_error "Specify at least one option to update.\n#{optparse}"
@@ -1237,7 +1280,18 @@ class Morpheus::Cli::Instances
1237
1280
  instance = json_response['instance']
1238
1281
  stats = instance['stats'] || json_response['stats'] || {}
1239
1282
  # load_balancers = json_response['loadBalancers'] || {}
1240
-
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
1241
1295
  # containers are fetched via separate api call
1242
1296
  containers = nil
1243
1297
  if options[:include_containers]
@@ -1278,8 +1332,8 @@ class Morpheus::Cli::Instances
1278
1332
  # "Cost" => lambda {|it| it['hourlyCost'] ? format_money(it['hourlyCost'], (it['currency'] || 'USD'), {sigdig:15}).to_s + ' per hour' : '' },
1279
1333
  # "Price" => lambda {|it| it['hourlyPrice'] ? format_money(it['hourlyPrice'], (it['currency'] || 'USD'), {sigdig:15}).to_s + ' per hour' : '' },
1280
1334
  "Environment" => 'instanceContext',
1281
- "Labels" => lambda {|it| it['tags'] ? it['tags'].join(',') : '' },
1282
- "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(', ') : '' },
1283
1337
  "Owner" => lambda {|it|
1284
1338
  if it['owner']
1285
1339
  (it['owner']['username'] || it['owner']['id'])
@@ -1298,11 +1352,14 @@ class Morpheus::Cli::Instances
1298
1352
  "Connection" => lambda {|it| format_instance_connection_string(it) },
1299
1353
  "Status" => lambda {|it| format_instance_status(it) }
1300
1354
  }
1355
+ description_cols.delete("Labels") if labels.nil? || labels.empty?
1356
+ description_cols.delete("Tags") if tags.nil? || tags.empty?
1301
1357
  description_cols.delete("Power Schedule") if instance['powerSchedule'].nil?
1302
1358
  description_cols.delete("Expire Date") if instance['expireDate'].nil?
1303
1359
  description_cols.delete("Shutdown Date") if instance['shutdownDate'].nil?
1304
1360
  description_cols["Removal Date"] = lambda {|it| format_local_dt(it['removalDate'])} if instance['status'] == 'pendingRemoval'
1305
1361
  description_cols.delete("Last Deployment") if instance['lastDeploy'].nil?
1362
+ #description_cols.delete("Environment") if instance['instanceContext'].nil?
1306
1363
  print_description_list(description_cols, instance)
1307
1364
 
1308
1365
  if instance['statusMessage']
@@ -2616,6 +2673,48 @@ class Morpheus::Cli::Instances
2616
2673
  end
2617
2674
  end
2618
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
+
2619
2718
  def remove(args)
2620
2719
  options = {}
2621
2720
  query_params = {}
@@ -2925,6 +3024,10 @@ class Morpheus::Cli::Instances
2925
3024
  # no pagination yet
2926
3025
  # build_standard_list_options(opts, options)
2927
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
2928
3031
  end
2929
3032
  optparse.parse!(args)
2930
3033
  verify_args!(args:args, optparse:optparse, count:1)
@@ -2947,7 +3050,8 @@ class Morpheus::Cli::Instances
2947
3050
  snapshot_column_definitions = {
2948
3051
  "ID" => lambda {|it| it['id'] },
2949
3052
  "Name" => lambda {|it| it['name'] },
2950
- "Description" => lambda {|it| it['snapshotType'] ? (it['snapshotType']['name'] || it['snapshotType']['code']) : '' },
3053
+ "Description" => lambda {|it| it['description'] },
3054
+ # "Type" => lambda {|it| it['snapshotType'] },
2951
3055
  "Date Created" => lambda {|it| format_local_dt(it['snapshotCreated']) },
2952
3056
  "Status" => lambda {|it| format_snapshot_status(it) }
2953
3057
  }
@@ -3876,51 +3980,6 @@ private
3876
3980
  end
3877
3981
  end
3878
3982
 
3879
- def format_instance_status(instance, return_color=cyan)
3880
- out = ""
3881
- status_string = instance['status'].to_s
3882
- if status_string == 'running'
3883
- out << "#{green}#{status_string.upcase}#{return_color}"
3884
- elsif status_string == 'provisioning'
3885
- out << "#{cyan}#{status_string.upcase}#{return_color}"
3886
- elsif status_string == 'stopped' or status_string == 'failed'
3887
- out << "#{red}#{status_string.upcase}#{return_color}"
3888
- else
3889
- out << "#{yellow}#{status_string.upcase}#{return_color}"
3890
- end
3891
- out
3892
- end
3893
-
3894
- def format_instance_connection_string(instance)
3895
- if !instance['connectionInfo'].nil? && instance['connectionInfo'].empty? == false
3896
- connection_string = "#{instance['connectionInfo'][0]['ip']}:#{instance['connectionInfo'][0]['port']}"
3897
- end
3898
- end
3899
-
3900
- def format_container_status(container, return_color=cyan)
3901
- out = ""
3902
- status_string = container['status'].to_s
3903
- if status_string == 'running'
3904
- out << "#{green}#{status_string.upcase}#{return_color}"
3905
- elsif status_string == 'provisioning'
3906
- out << "#{cyan}#{status_string.upcase}#{return_color}"
3907
- elsif status_string == 'stopped' or status_string == 'failed'
3908
- out << "#{red}#{status_string.upcase}#{return_color}"
3909
- else
3910
- out << "#{yellow}#{status_string.upcase}#{return_color}"
3911
- end
3912
- out
3913
- end
3914
-
3915
- def format_container_connection_string(container)
3916
- if !container['ports'].nil? && container['ports'].empty? == false
3917
- connection_string = "#{container['ip']}:#{container['ports'][0]['external']}"
3918
- else
3919
- # eh? more logic needed here i think, see taglib morph:containerLocationMenu
3920
- connection_string = "#{container['ip']}"
3921
- end
3922
- end
3923
-
3924
3983
  def instance_scaling_option_types(instance=nil)
3925
3984
 
3926
3985
  # Group
@@ -3974,17 +4033,6 @@ private
3974
4033
  list
3975
4034
  end
3976
4035
 
3977
- def format_instance_container_display_name(instance, plural=false)
3978
- #<span class="info-label">${[null,'docker'].contains(instance.layout?.provisionType?.code) ? 'Containers' : 'Virtual Machines'}:</span> <span class="info-value">${instance.containers?.size()}</span>
3979
- v = plural ? "Containers" : "Container"
3980
- if instance && instance['layout'] && instance['layout'].key?("provisionTypeCode")
3981
- if [nil, 'docker'].include?(instance['layout']["provisionTypeCode"])
3982
- v = plural ? "Virtual Machines" : "Virtual Machine"
3983
- end
3984
- end
3985
- return v
3986
- end
3987
-
3988
4036
  def print_instance_threshold_description_list(instance_threshold)
3989
4037
  description_cols = {
3990
4038
  # "Instance" => lambda {|it| "#{instance['id']} - #{instance['name']}" },