morpheus-cli 5.0.2 → 5.2.4

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 (38) 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 +30 -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/health_command.rb +57 -0
  20. data/lib/morpheus/cli/hosts.rb +128 -11
  21. data/lib/morpheus/cli/instances.rb +270 -108
  22. data/lib/morpheus/cli/invoices_command.rb +67 -4
  23. data/lib/morpheus/cli/jobs_command.rb +94 -92
  24. data/lib/morpheus/cli/library_option_lists_command.rb +1 -1
  25. data/lib/morpheus/cli/library_option_types_command.rb +10 -5
  26. data/lib/morpheus/cli/mixins/accounts_helper.rb +5 -1
  27. data/lib/morpheus/cli/mixins/print_helper.rb +13 -6
  28. data/lib/morpheus/cli/mixins/provisioning_helper.rb +88 -5
  29. data/lib/morpheus/cli/option_types.rb +10 -10
  30. data/lib/morpheus/cli/projects_command.rb +1 -1
  31. data/lib/morpheus/cli/roles.rb +193 -155
  32. data/lib/morpheus/cli/service_catalog_command.rb +1474 -0
  33. data/lib/morpheus/cli/tasks.rb +9 -11
  34. data/lib/morpheus/cli/version.rb +1 -1
  35. data/lib/morpheus/cli/virtual_images.rb +162 -68
  36. data/lib/morpheus/formatters.rb +55 -20
  37. metadata +5 -4
  38. data/lib/morpheus/cli/mixins/catalog_helper.rb +0 -66
@@ -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,9 +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', '--account ACCOUNT', "Account Name or ID" ) do |val|
58
- options[:account] = val
59
- end
60
57
  opts.on( '-g', '--group GROUP', "Group Name or ID" ) do |val|
61
58
  options[:group] = val
62
59
  end
@@ -82,6 +79,17 @@ class Morpheus::Cli::Hosts
82
79
  # params[:clusterId] = val
83
80
  options[:cluster] = val
84
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
85
93
  opts.on( '', '--vm', "Show only virtual machines" ) do |val|
86
94
  params[:vm] = true
87
95
  end
@@ -106,8 +114,14 @@ class Morpheus::Cli::Hosts
106
114
  opts.on( '--created-by USER', "Created By User Username or ID" ) do |val|
107
115
  options[:created_by] = val
108
116
  end
109
- opts.on('--details', "Display more details: memory and storage usage used / max values." ) do
110
- options[:details] = true
117
+ opts.on( '--tenant TENANT', "Tenant Name or ID" ) do |val|
118
+ options[:account] = val
119
+ end
120
+ opts.on('--labels label',String, "Filter by labels (keywords).") do |val|
121
+ val.split(",").each do |k|
122
+ options[:labels] ||= []
123
+ options[:labels] << k.strip
124
+ end
111
125
  end
112
126
  opts.on('--tags Name=Value',String, "Filter by tags.") do |val|
113
127
  val.split(",").each do |value_pair|
@@ -123,6 +137,12 @@ class Morpheus::Cli::Hosts
123
137
  opts.on('--non-tag-compliant', "Displays only servers with tag compliance warnings." ) do
124
138
  params[:tagCompliant] = false
125
139
  end
140
+ opts.on('--stats', "Display values for memory and storage usage used / max values." ) do
141
+ options[:stats] = true
142
+ end
143
+ opts.on('-a', '--details', "Display all details: hostname, private ip, plan, stats, etc." ) do
144
+ options[:details] = true
145
+ end
126
146
  build_standard_list_options(opts, options)
127
147
  opts.footer = "List hosts."
128
148
  end
@@ -172,6 +192,7 @@ class Morpheus::Cli::Hosts
172
192
  params['clusterId'] = cluster['id']
173
193
  end
174
194
  end
195
+ params['labels'] = options[:labels] if options[:labels]
175
196
  if options[:tags] && !options[:tags].empty?
176
197
  options[:tags].each do |k,v|
177
198
  params['tags.' + k] = v
@@ -208,6 +229,9 @@ class Morpheus::Cli::Hosts
208
229
  multi_tenant = json_response['multiTenant'] == true
209
230
  title = "Morpheus Hosts"
210
231
  subtitles = []
232
+ if account
233
+ subtitles << "Tenant: #{account['name']}".strip
234
+ end
211
235
  if group
212
236
  subtitles << "Group: #{group['name']}".strip
213
237
  end
@@ -248,7 +272,7 @@ class Morpheus::Cli::Hosts
248
272
  cpu_usage_str = !stats ? "" : generate_usage_bar((stats['usedCpu'] || stats['cpuUsage']).to_f, 100, {max_bars: 10})
249
273
  memory_usage_str = !stats ? "" : generate_usage_bar(stats['usedMemory'], stats['maxMemory'], {max_bars: 10})
250
274
  storage_usage_str = !stats ? "" : generate_usage_bar(stats['usedStorage'], stats['maxStorage'], {max_bars: 10})
251
- if options[:details]
275
+ if options[:details] || options[:stats]
252
276
  if stats['maxMemory'] && stats['maxMemory'].to_i != 0
253
277
  memory_usage_str = memory_usage_str + cyan + format_bytes_short(stats['usedMemory']).strip.rjust(8, ' ') + " / " + format_bytes_short(stats['maxMemory']).strip
254
278
  end
@@ -259,12 +283,14 @@ class Morpheus::Cli::Hosts
259
283
  row = {
260
284
  id: server['id'],
261
285
  name: server['name'],
286
+ external_name: server['externalName'],
262
287
  hostname: server['hostname'],
263
288
  platform: server['serverOs'] ? server['serverOs']['name'].upcase : 'N/A',
264
289
  type: server['computeServerType'] ? server['computeServerType']['name'] : 'unmanaged',
265
290
  tenant: server['account'] ? server['account']['name'] : server['accountId'],
266
291
  owner: server['owner'] ? server['owner']['username'] : server['owner'],
267
292
  cloud: server['zone'] ? server['zone']['name'] : '',
293
+ plan: server['plan'] ? server['plan']['name'] : '',
268
294
  ip: server['externalIp'],
269
295
  internal_ip: server['internalIp'],
270
296
  nodes: server['containers'] ? server['containers'].size : '',
@@ -283,11 +309,13 @@ class Morpheus::Cli::Hosts
283
309
  columns = {
284
310
  "ID" => :id,
285
311
  "Name" => :name,
312
+ "External Name" => :external_name,
286
313
  "Hostname" => :hostname,
287
314
  "Type" => :type,
288
315
  "Owner" => :owner,
289
316
  "Tenant" => :tenant,
290
317
  "Cloud" => :cloud,
318
+ "Plan" => :plan,
291
319
  "IP" => :ip,
292
320
  "Private IP" => :internal_ip,
293
321
  "Nodes" => :nodes,
@@ -300,7 +328,9 @@ class Morpheus::Cli::Hosts
300
328
  "Updated" => :updated,
301
329
  }
302
330
  if options[:details] != true
331
+ columns.delete("External Name")
303
332
  columns.delete("Hostname")
333
+ columns.delete("Plan")
304
334
  columns.delete("Private IP")
305
335
  columns.delete("Owner")
306
336
  columns.delete("Tenant")
@@ -308,6 +338,10 @@ class Morpheus::Cli::Hosts
308
338
  columns.delete("Created")
309
339
  columns.delete("Updated")
310
340
  end
341
+ # hide External Name if there are none
342
+ if !servers.find {|it| it['externalName'] && it['externalName'] != it['name']}
343
+ columns.delete("External Name")
344
+ end
311
345
  if !multi_tenant
312
346
  columns.delete("Tenant")
313
347
  end
@@ -334,7 +368,7 @@ class Morpheus::Cli::Hosts
334
368
  options = {}
335
369
  optparse = Morpheus::Cli::OptionParser.new do |opts|
336
370
  opts.banner = subcommand_usage("[options]")
337
- opts.on( '-a', '--account ACCOUNT', "Account Name or ID" ) do |val|
371
+ opts.on( '--tenant TENANT', "Tenant Name or ID" ) do |val|
338
372
  options[:account] = val
339
373
  end
340
374
  opts.on( '-g', '--group GROUP', "Group Name or ID" ) do |val|
@@ -503,9 +537,10 @@ class Morpheus::Cli::Hosts
503
537
  puts records_as_csv([json_response['server']], options)
504
538
  return 0
505
539
  end
506
- server = json_response['server']
540
+ server = json_response['server'] || json_response['host'] || {}
507
541
  #stats = server['stats'] || json_response['stats'] || {}
508
542
  stats = json_response['stats'] || {}
543
+ tags = server['tags'] || server['metadata']
509
544
  title = "Host Details"
510
545
  print_h1 title, [], options
511
546
  print cyan
@@ -514,6 +549,8 @@ class Morpheus::Cli::Hosts
514
549
  "Name" => 'name',
515
550
  "Hostname" => 'hostname',
516
551
  "Description" => 'description',
552
+ "Labels" => lambda {|it| it['labels'] ? it['labels'].join(',') : '' },
553
+ "Tags" => lambda {|it| tags ? format_metadata(tags) : '' },
517
554
  "Owner" => lambda {|it| it['owner'] ? it['owner']['username'] : '' },
518
555
  "Tenant" => lambda {|it| it['account'] ? it['account']['name'] : '' },
519
556
  #"Group" => lambda {|it| it['group'] ? it['group']['name'] : '' },
@@ -537,6 +574,8 @@ class Morpheus::Cli::Hosts
537
574
  # server_columns.delete("Tenant") if multi_tenant != true
538
575
  server_columns.delete("Cost") if server['hourlyCost'].to_f == 0
539
576
  server_columns.delete("Price") if server['hourlyPrice'].to_f == 0 || server['hourlyPrice'] == server['hourlyCost']
577
+ server_columns.delete("Labels") if server['labels'].nil? || server['labels'].empty?
578
+ server_columns.delete("Tags") if tags.nil? || tags.empty?
540
579
 
541
580
  print_description_list(server_columns, server)
542
581
 
@@ -963,6 +1002,22 @@ class Morpheus::Cli::Hosts
963
1002
  opts.on('--power-schedule-type ID', String, "Power Schedule Type ID") do |val|
964
1003
  params['powerScheduleType'] = val == "null" ? nil : val
965
1004
  end
1005
+ opts.on('--labels [LIST]', String, "Labels (keywords) in the format 'foo, bar'") do |val|
1006
+ params['labels'] = val.to_s.split(',').collect {|it| it.to_s.strip }.compact.uniq.join(',')
1007
+ end
1008
+ opts.on('--tags LIST', String, "Tags in the format 'name:value, name:value'. This will add and remove tags.") do |val|
1009
+ options[:tags] = val
1010
+ end
1011
+ opts.on('--metadata LIST', String, "Alias for --tags.") do |val|
1012
+ options[:tags] = val
1013
+ end
1014
+ opts.add_hidden_option('--metadata')
1015
+ opts.on('--add-tags TAGS', String, "Add Tags in the format 'name:value, name:value'. This will only add/update tags.") do |val|
1016
+ options[:add_tags] = val
1017
+ end
1018
+ 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|
1019
+ options[:remove_tags] = val
1020
+ end
966
1021
  # opts.on('--created-by ID', String, "Created By User ID") do |val|
967
1022
  # params['createdById'] = val
968
1023
  # end
@@ -981,6 +1036,18 @@ class Morpheus::Cli::Hosts
981
1036
  new_group = nil
982
1037
  passed_options = options[:options] ? options[:options].reject {|k,v| k.is_a?(Symbol) } : {}
983
1038
  params.deep_merge!(passed_options) unless passed_options.empty?
1039
+ # metadata tags
1040
+ if options[:tags]
1041
+ params['tags'] = parse_metadata(options[:tags])
1042
+ else
1043
+ # params['tags'] = prompt_metadata(options)
1044
+ end
1045
+ if options[:add_tags]
1046
+ params['addTags'] = parse_metadata(options[:add_tags])
1047
+ end
1048
+ if options[:remove_tags]
1049
+ params['removeTags'] = parse_metadata(options[:remove_tags])
1050
+ end
984
1051
  payload = nil
985
1052
  if options[:payload]
986
1053
  payload = options[:payload]
@@ -1892,7 +1959,8 @@ class Morpheus::Cli::Hosts
1892
1959
  snapshot_column_definitions = {
1893
1960
  "ID" => lambda {|it| it['id'] },
1894
1961
  "Name" => lambda {|it| it['name'] },
1895
- "Description" => lambda {|it| it['snapshotType'] ? (it['snapshotType']['name'] || it['snapshotType']['code']) : '' },
1962
+ "Description" => lambda {|it| it['description'] },
1963
+ # "Type" => lambda {|it| it['snapshotType'] },
1896
1964
  "Date Created" => lambda {|it| format_local_dt(it['snapshotCreated']) },
1897
1965
  "Status" => lambda {|it| format_snapshot_status(it) }
1898
1966
  }
@@ -1909,6 +1977,55 @@ class Morpheus::Cli::Hosts
1909
1977
  end
1910
1978
  end
1911
1979
 
1980
+ def software(args)
1981
+ options = {}
1982
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
1983
+ opts.banner = subcommand_usage("[host]")
1984
+ build_standard_list_options(opts, options)
1985
+ end
1986
+ optparse.parse!(args)
1987
+ verify_args!(args:args, optparse:optparse, count:1)
1988
+ connect(options)
1989
+ begin
1990
+ server = find_host_by_name_or_id(args[0])
1991
+ return 1 if server.nil?
1992
+ params = {}
1993
+ params.merge!(parse_list_options(options))
1994
+ @servers_interface.setopts(options)
1995
+ if options[:dry_run]
1996
+ print_dry_run @servers_interface.dry.software(server['id'], params)
1997
+ return
1998
+ end
1999
+ json_response = @servers_interface.software(server['id'], params)
2000
+ software = json_response['software']
2001
+ render_response(json_response, options, 'software') do
2002
+ print_h1 "Software: #{server['name']}", [], options
2003
+ if software.empty?
2004
+ print cyan,"No software found",reset,"\n"
2005
+ else
2006
+ software_column_definitions = {
2007
+ # "ID" => lambda {|it| it['id'] },
2008
+ "Name" => lambda {|it| it['name'] },
2009
+ "Version" => lambda {|it| it['packageVersion'] },
2010
+ "Publisher" => lambda {|it| it['packagePublisher'] },
2011
+ # "Release" => lambda {|it| it['packageRelease'] },
2012
+ # "Type" => lambda {|it| it['packageType'] },
2013
+ # "Architecture" => lambda {|it| it['architecture'] },
2014
+ # "Install Date" => lambda {|it| format_local_dt(it['installDate']) },
2015
+ }
2016
+ print cyan
2017
+ print as_pretty_table(software, software_column_definitions.upcase_keys!, options)
2018
+ print_results_pagination({size: software.size, total: software.size})
2019
+ end
2020
+ print reset, "\n"
2021
+ end
2022
+ return 0
2023
+ rescue RestClient::Exception => e
2024
+ print_rest_exception(e, options)
2025
+ exit 1
2026
+ end
2027
+ end
2028
+
1912
2029
  private
1913
2030
 
1914
2031
  def find_host_by_id(id)
@@ -2061,7 +2178,7 @@ class Morpheus::Cli::Hosts
2061
2178
  def make_managed_option_types(connected=true)
2062
2179
  [
2063
2180
  #{'fieldName' => 'account', 'fieldLabel' => 'Account', 'type' => 'select', 'optionSource' => 'accounts', 'required' => true},
2064
- {'fieldName' => 'sshUsername', 'fieldLabel' => 'SSH Username', 'type' => 'text', 'required' => true},
2181
+ {'fieldName' => 'sshUsername', 'fieldLabel' => 'SSH Username', 'type' => 'text'},
2065
2182
  {'fieldName' => 'sshPassword', 'fieldLabel' => 'SSH Password', 'type' => 'password', 'required' => false},
2066
2183
  {'fieldName' => 'serverOs', 'fieldLabel' => 'OS Type', 'type' => 'select', 'optionSource' => 'osTypes', 'required' => false},
2067
2184
  ]
@@ -19,7 +19,8 @@ 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
+ :lock, :unlock, :clone_image,
23
+ :security_groups, :apply_security_groups, :run_workflow, :import_snapshot, :snapshot, :snapshots,
23
24
  :console, :status_check, {:containers => :list_containers},
24
25
  :scaling, {:'scaling-update' => :scaling_update},
25
26
  :wiki, :update_wiki,
@@ -80,9 +81,6 @@ class Morpheus::Cli::Instances
80
81
  options[:owner] = val
81
82
  end
82
83
  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
84
  opts.on('--status STATUS', "Filter by status i.e. provisioning,running,starting,stopping") do |val|
87
85
  params['status'] = (params['status'] || []) + val.to_s.split(',').collect {|s| s.strip }.select {|s| s != "" }
88
86
  end
@@ -92,6 +90,17 @@ class Morpheus::Cli::Instances
92
90
  opts.on('--pending-removal-only', "Only instances pending removal.") do
93
91
  options[:deleted] = true
94
92
  end
93
+ opts.on( '--plan NAME', String, "Filter by Plan name(s)" ) do |val|
94
+ # commas used in names a lot so use --plan one --plan two
95
+ params['plan'] ||= []
96
+ params['plan'] << val
97
+ end
98
+ opts.on( '--plan-id ID', String, "Filter by Plan id(s)" ) do |val|
99
+ params['planId'] = parse_id_list(val)
100
+ end
101
+ opts.on( '--plan-code CODE', String, "Filter by Plan code(s)" ) do |val|
102
+ params['planCode'] = parse_id_list(val)
103
+ end
95
104
  opts.on('--labels label',String, "Filter by labels (keywords).") do |val|
96
105
  val.split(",").each do |k|
97
106
  options[:labels] ||= []
@@ -106,6 +115,12 @@ class Morpheus::Cli::Instances
106
115
  options[:tags][k] << (v || '')
107
116
  end
108
117
  end
118
+ opts.on('--stats', "Display values for memory and storage usage used / max values." ) do
119
+ options[:stats] = true
120
+ end
121
+ opts.on('-a', '--details', "Display all details: plan, stats, etc" ) do
122
+ options[:details] = true
123
+ end
109
124
  build_common_options(opts, options, [:list, :query, :json, :yaml, :csv, :fields, :dry_run, :remote])
110
125
  opts.footer = "List instances."
111
126
  end
@@ -216,7 +231,7 @@ class Morpheus::Cli::Instances
216
231
  cpu_usage_str = !stats ? "" : generate_usage_bar((stats['usedCpu'] || stats['cpuUsage']).to_f, 100, {max_bars: 10})
217
232
  memory_usage_str = !stats ? "" : generate_usage_bar(stats['usedMemory'], stats['maxMemory'], {max_bars: 10})
218
233
  storage_usage_str = !stats ? "" : generate_usage_bar(stats['usedStorage'], stats['maxStorage'], {max_bars: 10})
219
- if options[:details]
234
+ if options[:details] || options[:stats]
220
235
  if stats['maxMemory'] && stats['maxMemory'].to_i != 0
221
236
  memory_usage_str = memory_usage_str + cyan + format_bytes_short(stats['usedMemory']).strip.rjust(8, ' ') + " / " + format_bytes_short(stats['maxMemory']).strip
222
237
  end
@@ -234,8 +249,9 @@ class Morpheus::Cli::Instances
234
249
  nodes: instance['containers'].count,
235
250
  status: format_instance_status(instance, cyan),
236
251
  type: instance['instanceType']['name'],
237
- group: !instance['group'].nil? ? instance['group']['name'] : nil,
238
- cloud: !instance['cloud'].nil? ? instance['cloud']['name'] : nil,
252
+ group: instance['group'] ? instance['group']['name'] : nil,
253
+ cloud: instance['cloud'] ? instance['cloud']['name'] : nil,
254
+ plan: instance['plan'] ? instance['plan']['name'] : '',
239
255
  version: instance['instanceVersion'] ? instance['instanceVersion'] : '',
240
256
  created: format_local_dt(instance['dateCreated']),
241
257
  cpu: cpu_usage_str + cyan,
@@ -249,12 +265,13 @@ class Morpheus::Cli::Instances
249
265
  {:created => {:display_name => "CREATED"}},
250
266
  # {:tenant => {:display_name => "TENANT"}},
251
267
  {:user => {:display_name => "OWNER", :max_width => 20}},
268
+ :plan,
252
269
  :nodes, {:connection => {:max_width => 30}}, :status, :cpu, :memory, :storage]
253
270
  # custom pretty table columns ... this is handled in as_pretty_table now(),
254
271
  # 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
272
+ if options[:details] != true
273
+ columns.delete(:plan)
274
+ end
258
275
  print cyan
259
276
  print as_pretty_table(rows, columns, options)
260
277
  print reset
@@ -443,17 +460,58 @@ class Morpheus::Cli::Instances
443
460
  options[:instance_name] = args[0]
444
461
  end
445
462
 
446
- # use active group by default
447
- options[:group] ||= @active_group_id
448
- options[:select_datastore] = true
449
- options[:name_required] = true
450
463
  begin
451
464
  payload = nil
452
465
  if options[:payload]
453
466
  payload = options[:payload]
454
467
  # support -O OPTION switch on top of --payload
455
468
  payload.deep_merge!(options[:options].reject {|k,v| k.is_a?(Symbol) }) if options[:options]
469
+ # obviously should support every option that prompt supports on top of -- payload as well
470
+ # group, cloud and type for now
471
+ # todo: also support :layout, service_plan, :resource_pool, etc.
472
+ group = nil
473
+ if options[:group]
474
+ group = find_group_by_name_or_id_for_provisioning(options[:group])
475
+ if group.nil?
476
+ return 1, "group not found by #{options[:group]}"
477
+ end
478
+ #payload["siteId"] = group["id"]
479
+ payload.deep_merge!({"instance" => {"site" => {"id" => group["id"]} } })
480
+ end
481
+ if options[:cloud]
482
+ group_id = group ? group["id"] : ((payload["instance"] && payload["instance"]["site"].is_a?(Hash)) ? payload["instance"]["site"]["id"] : nil)
483
+ cloud = find_cloud_by_name_or_id_for_provisioning(group_id, options[:cloud])
484
+ if cloud.nil?
485
+ return 1, "cloud not found by #{options[:cloud]}"
486
+ end
487
+ payload["zoneId"] = cloud["id"]
488
+ payload.deep_merge!({"instance" => {"cloud" => cloud["name"] } })
489
+ end
490
+ if options[:cloud]
491
+ group_id = group ? group["id"] : ((payload["instance"] && payload["instance"]["site"].is_a?(Hash)) ? payload["instance"]["site"]["id"] : nil)
492
+ cloud = find_cloud_by_name_or_id_for_provisioning(group_id, options[:cloud])
493
+ if cloud.nil?
494
+ return 1, "cloud not found by #{options[:cloud]}"
495
+ end
496
+ payload["zoneId"] = cloud["id"]
497
+ payload.deep_merge!({"instance" => {"cloud" => cloud["name"] } })
498
+ end
499
+ if options[:instance_type_code]
500
+ # should just use find_instance_type_by_name_or_id
501
+ # note that the api actually will match name name or code
502
+ 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])
503
+ if instance_type.nil?
504
+ return 1, "instance type not found by #{options[:cloud]}"
505
+ end
506
+ payload.deep_merge!({"instance" => {"type" => instance_type["code"] } })
507
+ payload.deep_merge!({"instance" => {"instanceType" => {"code" => instance_type["code"]} } })
508
+ end
509
+
456
510
  else
511
+ # use active group by default
512
+ options[:group] ||= @active_group_id
513
+ options[:select_datastore] = true
514
+ options[:name_required] = true
457
515
  # prompt for all the instance configuration options
458
516
  # this provisioning helper method handles all (most) of the parsing and prompting
459
517
  # and it relies on the method to exit non-zero on error, like a bad CLOUD or TYPE value
@@ -546,16 +604,17 @@ class Morpheus::Cli::Instances
546
604
  opts.on('--group GROUP', String, "Group Name or ID") do |val|
547
605
  options[:group] = val
548
606
  end
549
- opts.on('--tags LIST', String, "Metadata tags in the format 'ping=pong,flash=bang'") do |val|
550
- options[:metadata] = val
607
+ opts.on('--labels [LIST]', String, "Labels (keywords) in the format 'foo, bar'") do |val|
608
+ params['labels'] = val.to_s.split(',').collect {|it| it.to_s.strip }.compact.uniq.join(',')
551
609
  end
552
- opts.on('--metadata LIST', String, "Metadata tags in the format 'ping=pong,flash=bang'") do |val|
553
- options[:metadata] = val
610
+ opts.on('--tags LIST', String, "Tags in the format 'name:value, name:value'. This will add and remove tags.") do |val|
611
+ options[:tags] = val
554
612
  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(',')
613
+ opts.on('--add-tags TAGS', String, "Add Tags in the format 'name:value, name:value'. This will only add/update tags.") do |val|
614
+ options[:add_tags] = val
615
+ end
616
+ 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|
617
+ options[:remove_tags] = val
559
618
  end
560
619
  opts.on('--power-schedule-type ID', String, "Power Schedule Type ID") do |val|
561
620
  params['powerScheduleType'] = val == "null" ? nil : val
@@ -611,32 +670,17 @@ class Morpheus::Cli::Instances
611
670
  payload['instance']['site'] = {'id' => group['id']}
612
671
  end
613
672
  # 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
673
+ if options[:tags]
674
+ # api version 4.2.5 and later supports tags, older versions expect metadata
675
+ # todo: use tags instead like everywhere else
676
+ # payload['instance']['tags'] = parse_metadata(options[:tags])
677
+ payload['instance']['metadata'] = parse_metadata(options[:tags])
678
+ end
679
+ if options[:add_tags]
680
+ payload['instance']['addTags'] = parse_metadata(options[:add_tags])
681
+ end
682
+ if options[:remove_tags]
683
+ payload['instance']['removeTags'] = parse_metadata(options[:remove_tags])
640
684
  end
641
685
  if payload['instance'].empty? && params.empty? && options[:owner].nil?
642
686
  raise_command_error "Specify at least one option to update.\n#{optparse}"
@@ -1237,7 +1281,18 @@ class Morpheus::Cli::Instances
1237
1281
  instance = json_response['instance']
1238
1282
  stats = instance['stats'] || json_response['stats'] || {}
1239
1283
  # load_balancers = json_response['loadBalancers'] || {}
1240
-
1284
+ # metadata tags used to be returned as metadata and are now returned as tags
1285
+ # the problem is tags is what we used to call Labels (keywords)
1286
+ # the api will change to tags and labels, so handle the old format as long as metadata is returned.
1287
+ labels = nil
1288
+ tags = nil
1289
+ if instance.key?('labels')
1290
+ labels = instance['labels']
1291
+ tags = instance['tags']
1292
+ else
1293
+ labels = instance['tags']
1294
+ tags = instance['metadata']
1295
+ end
1241
1296
  # containers are fetched via separate api call
1242
1297
  containers = nil
1243
1298
  if options[:include_containers]
@@ -1278,8 +1333,8 @@ class Morpheus::Cli::Instances
1278
1333
  # "Cost" => lambda {|it| it['hourlyCost'] ? format_money(it['hourlyCost'], (it['currency'] || 'USD'), {sigdig:15}).to_s + ' per hour' : '' },
1279
1334
  # "Price" => lambda {|it| it['hourlyPrice'] ? format_money(it['hourlyPrice'], (it['currency'] || 'USD'), {sigdig:15}).to_s + ' per hour' : '' },
1280
1335
  "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(', ') : '' },
1336
+ "Labels" => lambda {|it| labels ? labels.join(',') : '' },
1337
+ "Tags" => lambda {|it| tags ? tags.collect {|m| "#{m['name']}: #{m['value']}" }.join(', ') : '' },
1283
1338
  "Owner" => lambda {|it|
1284
1339
  if it['owner']
1285
1340
  (it['owner']['username'] || it['owner']['id'])
@@ -1296,13 +1351,18 @@ class Morpheus::Cli::Instances
1296
1351
  "Shutdown Date" => lambda {|it| it['shutdownDate'] ? format_local_dt(it['shutdownDate']) : '' },
1297
1352
  "Nodes" => lambda {|it| it['containers'] ? it['containers'].count : 0 },
1298
1353
  "Connection" => lambda {|it| format_instance_connection_string(it) },
1354
+ "Locked" => lambda {|it| format_boolean(it['locked']) },
1299
1355
  "Status" => lambda {|it| format_instance_status(it) }
1300
1356
  }
1357
+ description_cols.delete("Labels") if labels.nil? || labels.empty?
1358
+ description_cols.delete("Tags") if tags.nil? || tags.empty?
1301
1359
  description_cols.delete("Power Schedule") if instance['powerSchedule'].nil?
1302
1360
  description_cols.delete("Expire Date") if instance['expireDate'].nil?
1303
1361
  description_cols.delete("Shutdown Date") if instance['shutdownDate'].nil?
1304
1362
  description_cols["Removal Date"] = lambda {|it| format_local_dt(it['removalDate'])} if instance['status'] == 'pendingRemoval'
1305
1363
  description_cols.delete("Last Deployment") if instance['lastDeploy'].nil?
1364
+ description_cols.delete("Locked") if instance['locked'] != true
1365
+ #description_cols.delete("Environment") if instance['instanceContext'].nil?
1306
1366
  print_description_list(description_cols, instance)
1307
1367
 
1308
1368
  if instance['statusMessage']
@@ -2616,6 +2676,48 @@ class Morpheus::Cli::Instances
2616
2676
  end
2617
2677
  end
2618
2678
 
2679
+ def snapshot(args)
2680
+ options = {}
2681
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
2682
+ opts.banner = subcommand_usage("[instance]")
2683
+ opts.on( '--name VALUE', String, "Snapshot Name. Default is server name + timestamp" ) do |val|
2684
+ options[:options]['name'] = val
2685
+ end
2686
+ opts.on( '--description VALUE', String, "Snapshot Description." ) do |val|
2687
+ options[:options]['description'] = val
2688
+ end
2689
+ build_standard_add_options(opts, options, [:auto_confirm])
2690
+ opts.footer = <<-EOT
2691
+ Create a snapshot for an instance.
2692
+ [instance] is required. This is the name or id of an instance
2693
+ EOT
2694
+ end
2695
+ optparse.parse!(args)
2696
+ verify_args!(args:args, optparse:optparse, count:1)
2697
+ connect(options)
2698
+ instance = find_instance_by_name_or_id(args[0])
2699
+ unless options[:yes] || ::Morpheus::Cli::OptionTypes::confirm("Are you sure you would like to snapshot the instance '#{instance['name']}'?", options)
2700
+ exit 1
2701
+ end
2702
+ payload = {}
2703
+ if options[:payload]
2704
+ payload = options[:payload]
2705
+ payload.deep_merge!({'snapshot' => parse_passed_options(options)})
2706
+ else
2707
+ payload.deep_merge!({'snapshot' => parse_passed_options(options)})
2708
+ end
2709
+ @instances_interface.setopts(options)
2710
+ if options[:dry_run]
2711
+ print_dry_run @instances_interface.dry.snapshot(instance['id'], payload)
2712
+ return
2713
+ end
2714
+ json_response = @instances_interface.snapshot(instance['id'], payload)
2715
+ render_response(json_response, options, 'snapshots') do
2716
+ print_green_success "Snapshot initiated."
2717
+ end
2718
+ return 0, nil
2719
+ end
2720
+
2619
2721
  def remove(args)
2620
2722
  options = {}
2621
2723
  query_params = {}
@@ -2925,6 +3027,10 @@ class Morpheus::Cli::Instances
2925
3027
  # no pagination yet
2926
3028
  # build_standard_list_options(opts, options)
2927
3029
  build_standard_get_options(opts, options)
3030
+ opts.footer = <<-EOT
3031
+ List snapshots for an instance.
3032
+ [instance] is required. This is the name or id of an instance
3033
+ EOT
2928
3034
  end
2929
3035
  optparse.parse!(args)
2930
3036
  verify_args!(args:args, optparse:optparse, count:1)
@@ -2947,7 +3053,8 @@ class Morpheus::Cli::Instances
2947
3053
  snapshot_column_definitions = {
2948
3054
  "ID" => lambda {|it| it['id'] },
2949
3055
  "Name" => lambda {|it| it['name'] },
2950
- "Description" => lambda {|it| it['snapshotType'] ? (it['snapshotType']['name'] || it['snapshotType']['code']) : '' },
3056
+ "Description" => lambda {|it| it['description'] },
3057
+ # "Type" => lambda {|it| it['snapshotType'] },
2951
3058
  "Date Created" => lambda {|it| format_local_dt(it['snapshotCreated']) },
2952
3059
  "Status" => lambda {|it| format_snapshot_status(it) }
2953
3060
  }
@@ -3784,6 +3891,117 @@ EOT
3784
3891
  return 0
3785
3892
  end
3786
3893
 
3894
+ def clone_image(args)
3895
+ options = {}
3896
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
3897
+ opts.banner = subcommand_usage("[instance]")
3898
+ opts.on( '--name VALUE', String, "Image Name (Template Name). Default is server name + timestamp" ) do |val|
3899
+ options[:options]['templateName'] = val
3900
+ end
3901
+ build_standard_update_options(opts, options)
3902
+ opts.footer = <<-EOT
3903
+ Clone to image (template) for an instance
3904
+ [instance] is required. This is the name or id of an instance
3905
+ EOT
3906
+ end
3907
+ optparse.parse!(args)
3908
+ verify_args!(args:args, optparse:optparse, count:1)
3909
+ connect(options)
3910
+ instance = find_instance_by_name_or_id(args[0])
3911
+ return 1 if instance.nil?
3912
+ payload = {}
3913
+ if options[:payload]
3914
+ payload = options[:payload]
3915
+ payload.deep_merge!(parse_passed_options(options))
3916
+ else
3917
+ payload.deep_merge!(parse_passed_options(options))
3918
+ if payload['templateName'].nil?
3919
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'templateName', 'type' => 'text', 'fieldLabel' => 'Image Name', 'description' => 'Choose a name for the new image template. Default is the server name + timestamp'}], options[:options])
3920
+ if v_prompt['templateName'].to_s != ''
3921
+ payload['templateName'] = v_prompt['templateName']
3922
+ end
3923
+ end
3924
+ end
3925
+ @instances_interface.setopts(options)
3926
+ if options[:dry_run]
3927
+ print_dry_run @instances_interface.dry.clone_image(instance['id'], payload)
3928
+ return
3929
+ end
3930
+ json_response = @instances_interface.clone_image(instance['id'], payload)
3931
+ render_response(json_response, options) do
3932
+ print_green_success "Clone Image initiated."
3933
+ end
3934
+ return 0, nil
3935
+ end
3936
+
3937
+ def lock(args)
3938
+ options = {}
3939
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
3940
+ opts.banner = subcommand_usage("[instance]")
3941
+ build_standard_update_options(opts, options)
3942
+ opts.footer = <<-EOT
3943
+ Lock an instance
3944
+ [instance] is required. This is the name or id of an instance
3945
+ EOT
3946
+ end
3947
+ optparse.parse!(args)
3948
+ verify_args!(args:args, optparse:optparse, count:1)
3949
+ connect(options)
3950
+ instance = find_instance_by_name_or_id(args[0])
3951
+ return 1 if instance.nil?
3952
+ payload = {}
3953
+ if options[:payload]
3954
+ payload = options[:payload]
3955
+ payload.deep_merge!(parse_passed_options(options))
3956
+ else
3957
+ payload.deep_merge!(parse_passed_options(options))
3958
+ end
3959
+ @instances_interface.setopts(options)
3960
+ if options[:dry_run]
3961
+ print_dry_run @instances_interface.dry.lock(instance['id'], payload)
3962
+ return
3963
+ end
3964
+ json_response = @instances_interface.lock(instance['id'], payload)
3965
+ render_response(json_response, options) do
3966
+ print_green_success "Locked instance #{instance['name']}"
3967
+ end
3968
+ return 0, nil
3969
+ end
3970
+
3971
+ def unlock(args)
3972
+ options = {}
3973
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
3974
+ opts.banner = subcommand_usage("[instance]")
3975
+ build_standard_update_options(opts, options)
3976
+ opts.footer = <<-EOT
3977
+ Unlock an instance
3978
+ [instance] is required. This is the name or id of an instance
3979
+ EOT
3980
+ end
3981
+ optparse.parse!(args)
3982
+ verify_args!(args:args, optparse:optparse, count:1)
3983
+ connect(options)
3984
+ instance = find_instance_by_name_or_id(args[0])
3985
+ return 1 if instance.nil?
3986
+ payload = {}
3987
+ if options[:payload]
3988
+ payload = options[:payload]
3989
+ payload.deep_merge!(parse_passed_options(options))
3990
+ else
3991
+ payload.deep_merge!(parse_passed_options(options))
3992
+ end
3993
+ @instances_interface.setopts(options)
3994
+ if options[:dry_run]
3995
+ print_dry_run @instances_interface.dry.unlock(instance['id'], payload)
3996
+ return
3997
+ end
3998
+ json_response = @instances_interface.unlock(instance['id'], payload)
3999
+ render_response(json_response, options) do
4000
+ print_green_success "Unlocked instance #{instance['name']}"
4001
+ end
4002
+ return 0, nil
4003
+ end
4004
+
3787
4005
  private
3788
4006
 
3789
4007
  def find_zone_by_name_or_id(group_id, val)
@@ -3876,51 +4094,6 @@ private
3876
4094
  end
3877
4095
  end
3878
4096
 
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
4097
  def instance_scaling_option_types(instance=nil)
3925
4098
 
3926
4099
  # Group
@@ -3974,17 +4147,6 @@ private
3974
4147
  list
3975
4148
  end
3976
4149
 
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
4150
  def print_instance_threshold_description_list(instance_threshold)
3989
4151
  description_cols = {
3990
4152
  # "Instance" => lambda {|it| "#{instance['id']} - #{instance['name']}" },