morpheus-cli 5.0.2 → 5.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -333,10 +333,9 @@ class Morpheus::Cli::Tasks
333
333
  build_common_options(opts, options, [:options, :payload, :json, :dry_run, :quiet, :remote])
334
334
  end
335
335
  optparse.parse!(args)
336
- if args.count > 1
337
- raise_command_error "wrong number of arguments, expected 1 and got (#{args.count}) #{args.join(' ')}\n#{optparse}"
338
- end
336
+ #verify_args!(args:args, count:1, optparse:optparse)
339
337
  if args[0]
338
+ # task_name = args[0]
340
339
  task_name = args[0]
341
340
  end
342
341
  # if task_name.nil? || task_type_name.nil?
@@ -424,6 +423,9 @@ class Morpheus::Cli::Tasks
424
423
  has_file_content = true
425
424
  it['fieldContext'] = nil
426
425
  it['fieldName'] = 'file'
426
+ # this should be required right!? fix api optionType data plz
427
+ it['required'] = true
428
+ it['defaultValue'] = 'local'
427
429
  else
428
430
  if it['fieldContext'].nil? || it['fieldContext'] == ''
429
431
  it['fieldContext'] = 'taskOptions'
@@ -657,9 +659,7 @@ class Morpheus::Cli::Tasks
657
659
  build_common_options(opts, options, [:options, :payload, :json, :dry_run, :quiet, :remote])
658
660
  end
659
661
  optparse.parse!(args)
660
- if args.count != 1
661
- raise_command_error "wrong number of arguments, expected 1 and got (#{args.count}) #{args.join(' ')}\n#{optparse}"
662
- end
662
+ verify_args!(args:args, count:1, optparse:optparse)
663
663
  task_name = args[0]
664
664
  connect(options)
665
665
  begin
@@ -733,7 +733,6 @@ class Morpheus::Cli::Tasks
733
733
 
734
734
  def remove(args)
735
735
  params = {}
736
- task_name = args[0]
737
736
  options = {}
738
737
  optparse = Morpheus::Cli::OptionParser.new do |opts|
739
738
  opts.banner = subcommand_usage("[task]")
@@ -743,10 +742,8 @@ class Morpheus::Cli::Tasks
743
742
  end
744
743
  end
745
744
  optparse.parse!(args)
746
- if args.count < 1
747
- puts optparse
748
- exit 1
749
- end
745
+ verify_args!(args:args, count:1, optparse:optparse)
746
+ task_name = args[0]
750
747
  connect(options)
751
748
  begin
752
749
  task = find_task_by_name_or_id(task_name)
@@ -819,6 +816,7 @@ class Morpheus::Cli::Tasks
819
816
  if args.count != 1
820
817
  raise_command_error "wrong number of arguments, expected 1 and got (#{args.count}) #{args.join(' ')}\n#{optparse}"
821
818
  end
819
+ verify_args!(args:args, count:1, optparse:optparse)
822
820
  task_name = args[0]
823
821
  connect(options)
824
822
  begin
@@ -1,6 +1,6 @@
1
1
 
2
2
  module Morpheus
3
3
  module Cli
4
- VERSION = "5.0.2"
4
+ VERSION = "5.2.0"
5
5
  end
6
6
  end
@@ -8,6 +8,7 @@ require 'morpheus/cli/cli_command'
8
8
 
9
9
  class Morpheus::Cli::VirtualImages
10
10
  include Morpheus::Cli::CliCommand
11
+ include Morpheus::Cli::ProvisioningHelper
11
12
 
12
13
  register_subcommands :list, :get, :add, :add_file, :remove_file, :update, :remove, :types => :virtual_image_types
13
14
  alias_subcommand :details, :get
@@ -43,6 +44,17 @@ class Morpheus::Cli::VirtualImages
43
44
  opts.on('--system', "System Images" ) do
44
45
  options[:filterType] = 'System'
45
46
  end
47
+ opts.on('--tags Name=Value',String, "Filter by tags (metadata name value pairs).") do |val|
48
+ val.split(",").each do |value_pair|
49
+ k,v = value_pair.strip.split("=")
50
+ options[:tags] ||= {}
51
+ options[:tags][k] ||= []
52
+ options[:tags][k] << (v || '')
53
+ end
54
+ end
55
+ opts.on('-a', '--details', "Show more details." ) do
56
+ options[:details] = true
57
+ end
46
58
  build_standard_list_options(opts, options)
47
59
  opts.footer = "List virtual images."
48
60
  end
@@ -59,6 +71,11 @@ class Morpheus::Cli::VirtualImages
59
71
  if options[:filterType]
60
72
  params[:filterType] = options[:filterType]
61
73
  end
74
+ if options[:tags]
75
+ options[:tags].each do |k,v|
76
+ params['tags.' + k] = v
77
+ end
78
+ end
62
79
  @virtual_images_interface.setopts(options)
63
80
  if options[:dry_run]
64
81
  print_dry_run @virtual_images_interface.dry.list(params)
@@ -82,16 +99,35 @@ class Morpheus::Cli::VirtualImages
82
99
  if images.empty?
83
100
  print cyan,"No virtual images found.",reset,"\n"
84
101
  else
85
- # print as_pretty_table(images, virtual_image_column_definitions.upcase_keys!, options)
86
- rows = images.collect do |image|
87
- image_type = virtual_image_type_for_name_or_code(image['imageType'])
88
- image_type_display = image_type ? "#{image_type['name']}" : image['imageType']
89
- {name: image['name'], id: image['id'], type: image_type_display, source: image['userUploaded'] ? "#{green}UPLOADED#{cyan}" : (image['systemImage'] ? 'SYSTEM' : "#{white}SYNCED#{cyan}"), storage: !image['storageProvider'].nil? ? image['storageProvider']['name'] : 'Default', size: image['rawSize'].nil? ? 'Unknown' : "#{Filesize.from("#{image['rawSize']} B").pretty}"}
102
+ virtual_image_column_definitions = {
103
+ "ID" => 'id',
104
+ "Name" => 'name',
105
+ "Type" => lambda {|it|
106
+ # yick, api should return the type with every virtualImage
107
+ image_type = virtual_image_type_for_name_or_code(it['imageType'])
108
+ image_type ? "#{image_type['name']}" : it['imageType']
109
+ },
110
+ "Operating System" => lambda {|it| it['osType'] ? it['osType']['name'] : "" },
111
+ "Storage" => lambda {|it| !it['storageProvider'].nil? ? it['storageProvider']['name'] : 'Default' },
112
+ "Size" => lambda {|it| it['rawSize'].nil? ? 'Unknown' : "#{Filesize.from("#{it['rawSize']} B").pretty}" },
113
+ "Visibility" => lambda {|it| it['visibility'] },
114
+ # "Tenant" => lambda {|it| it['account'].instance_of?(Hash) ? it['account']['name'] : it['ownerId'] },
115
+ "Tenants" => lambda {|it| format_list(it['accounts'].collect {|a| a['name'] }, '', 3) rescue '' },
116
+ "Source" => lambda {|it| format_virtual_image_source(it) },
117
+ "Created" => lambda {|it| format_local_dt(it['dateCreated']) },
118
+ "Updated" => lambda {|it| format_local_dt(it['lastUpdated']) },
119
+ "Tags" => lambda {|it| it['tags'] ? it['tags'].collect {|m| "#{m['name']}: #{m['value']}" }.join(', ') : '' },
120
+ }
121
+ if json_response['multiTenant'] != true
122
+ virtual_image_column_definitions.delete("Visibility")
123
+ virtual_image_column_definitions.delete("Tenants")
124
+ end
125
+ if options[:details] != true
126
+ virtual_image_column_definitions.delete("Tags")
127
+ virtual_image_column_definitions.delete("Created")
128
+ virtual_image_column_definitions.delete("Updated")
90
129
  end
91
- columns = [:id, :name, :type, :storage, :size, :source]
92
- columns = options[:include_fields] if options[:include_fields]
93
- print cyan
94
- print as_pretty_table(rows, columns, options)
130
+ print as_pretty_table(images, virtual_image_column_definitions.upcase_keys!, options)
95
131
  print_results_pagination(json_response)
96
132
  end
97
133
  print reset,"\n"
@@ -108,9 +144,12 @@ class Morpheus::Cli::VirtualImages
108
144
  options = {}
109
145
  optparse = Morpheus::Cli::OptionParser.new do |opts|
110
146
  opts.banner = subcommand_usage("[image]")
111
- opts.on('--details', "Show more details." ) do
147
+ opts.on('-a', '--details', "Show more details." ) do
112
148
  options[:details] = true
113
149
  end
150
+ opts.on('--tags LIST', String, "Metadata tags in the format 'name:value, name:value'") do |val|
151
+ options[:tags] = val
152
+ end
114
153
  build_standard_get_options(opts, options)
115
154
  opts.footer = <<-EOT
116
155
  Get details about a virtual image.
@@ -148,6 +187,7 @@ EOT
148
187
  json_response = @virtual_images_interface.get(id.to_i)
149
188
  image = json_response['virtualImage']
150
189
  image_config = image['config'] || {}
190
+ image_volumes = image['volumes'] || []
151
191
  image_files = json_response['cloudFiles'] || json_response['files']
152
192
  image_type = virtual_image_type_for_name_or_code(image['imageType'])
153
193
  image_type_display = image_type ? "#{image_type['name']}" : image['imageType']
@@ -157,35 +197,39 @@ EOT
157
197
  "ID" => 'id',
158
198
  "Name" => 'name',
159
199
  "Type" => lambda {|it| image_type_display },
200
+ "Operating System" => lambda {|it| it['osType'] ? it['osType']['name'] : "" },
160
201
  "Storage" => lambda {|it| !image['storageProvider'].nil? ? image['storageProvider']['name'] : 'Default' },
161
202
  "Size" => lambda {|it| image['rawSize'].nil? ? 'Unknown' : "#{Filesize.from("#{image['rawSize']} B").pretty}" },
162
203
  "Azure Publisher" => lambda {|it| image_config['publisher'] },
163
204
  "Azure Offer" => lambda {|it| image_config['offer'] },
164
205
  "Azure Sku" => lambda {|it| image_config['sku'] },
165
206
  "Azure Version" => lambda {|it| image_config['version'] },
166
- "Source" => lambda {|it| image['userUploaded'] ? "#{green}UPLOADED#{cyan}" : (image['systemImage'] ? 'SYSTEM' : "#{white}SYNCED#{cyan}") },
167
- # "Created" => lambda {|it| format_local_dt(it['dateCreated']) },
168
- # "Updated" => lambda {|it| format_local_dt(it['lastUpdated']) }
207
+ "Source" => lambda {|it| format_virtual_image_source(it) },
208
+ "Tags" => lambda {|it| it['tags'] ? it['tags'].collect {|m| "#{m['name']}: #{m['value']}" }.join(', ') : '' },
209
+ "Created" => lambda {|it| format_local_dt(it['dateCreated']) },
210
+ "Updated" => lambda {|it| format_local_dt(it['lastUpdated']) }
169
211
  }
212
+ description_cols.delete("Tags") if image['tags'].nil? || image['tags'].empty?
170
213
  if image['imageType'] == "azure-reference" || image['imageType'] == "azure"
171
214
  description_cols.delete("Size")
172
215
  description_cols.delete("Storage")
173
216
  description_cols["Source"] = lambda {|it| "#{bold}#{cyan}AZURE#{reset}#{cyan}" }
174
217
  else
175
- description_cols.delete("Azure Marketplace")
176
- description_cols.delete("Azure Marketplace Publisher")
177
- description_cols.delete("Azure Marketplace Sku")
178
- description_cols.delete("Azure Marketplace Offer")
179
- description_cols.delete("Azure Marketplace Version")
218
+ description_cols.delete("Azure Publisher")
219
+ description_cols.delete("Azure Sku")
220
+ description_cols.delete("Azure Offer")
221
+ description_cols.delete("Azure Version")
180
222
  end
181
223
  advanced_description_cols = {
182
- "OS Type" => lambda {|it| it['osType'] ? it['osType']['name'] : "" },
224
+ #"OS Type" => lambda {|it| it['osType'] ? it['osType']['name'] : "" }, # displayed above as Operating System
183
225
  "Min Memory" => lambda {|it| it['minRam'].to_i != 0 ? Filesize.from("#{it['minRam']} B").pretty : "" },
226
+ "Min Disk" => lambda {|it| it['minDisk'].to_i != 0 ? Filesize.from("#{it['minDisk']} B").pretty : "" },
184
227
  "Cloud Init?" => lambda {|it| format_boolean it['osType'] },
185
228
  "Install Agent?" => lambda {|it| format_boolean it['osType'] },
186
229
  "SSH Username" => lambda {|it| it['sshUsername'] },
187
230
  "SSH Password" => lambda {|it| it['sshPassword'] },
188
231
  "User Data" => lambda {|it| it['userData'] },
232
+ "Owner" => lambda {|it| it['tenant'].instance_of?(Hash) ? it['tenant']['name'] : it['ownerId'] },
189
233
  "Visibility" => lambda {|it| it['visibility'].to_s.capitalize },
190
234
  "Tenants" => lambda {|it| format_tenants(it['accounts']) },
191
235
  "Auto Join Domain?" => lambda {|it| format_boolean it['isAutoJoinDomain'] },
@@ -200,14 +244,27 @@ EOT
200
244
  end
201
245
  print_description_list(description_cols, image)
202
246
 
247
+ if image_volumes && !image_volumes.empty?
248
+ print_h2 "Volumes", options
249
+ image_volume_rows = image_volumes.collect do |image_volume|
250
+ {name: image_volume['name'], size: Filesize.from("#{image_volume['rawSize']} B").pretty}
251
+ end
252
+ print cyan
253
+ print as_pretty_table(image_volume_rows, [:name, :size])
254
+ print cyan
255
+ # print "\n", reset
256
+ end
257
+
203
258
  if image_files
204
259
  print_h2 "Files (#{image_files.size})"
205
260
  # image_files.each {|image_file|
206
261
  # pretty_filesize = Filesize.from("#{image_file['size']} B").pretty
207
262
  # print cyan," = #{image_file['name']} [#{pretty_filesize}]", "\n"
208
263
  # }
264
+ # size property changed to GB to match volumes
265
+ # contentLength is bytes
209
266
  image_file_rows = image_files.collect do |image_file|
210
- {filename: image_file['name'], size: Filesize.from("#{image_file['size']} B").pretty}
267
+ {filename: image_file['name'], size: Filesize.from("#{image_file['contentLength'] || image_file['size']} B").pretty}
211
268
  end
212
269
  print cyan
213
270
  print as_pretty_table(image_file_rows, [:filename, :size])
@@ -218,8 +275,9 @@ EOT
218
275
  print_h2 "Config", options
219
276
  print cyan
220
277
  print as_description_list(image_config, image_config.keys, options)
221
- print "\n", reset
278
+ # print "\n", reset
222
279
  end
280
+
223
281
  print reset,"\n"
224
282
  end
225
283
  return 0, nil
@@ -238,60 +296,68 @@ EOT
238
296
  tenants_list = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
239
297
  end
240
298
  end
299
+ opts.on('--tags LIST', String, "Tags in the format 'name:value, name:value'. This will add and remove tags.") do |val|
300
+ options[:tags] = val
301
+ end
302
+ opts.on('--add-tags TAGS', String, "Add Tags in the format 'name:value, name:value'. This will only add/update project tags.") do |val|
303
+ options[:add_tags] = val
304
+ end
305
+ 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|
306
+ options[:remove_tags] = val
307
+ end
241
308
  build_common_options(opts, options, [:options, :payload, :json, :dry_run, :remote])
242
309
  opts.footer = "Update a virtual image." + "\n" +
243
310
  "[name] is required. This is the name or id of a virtual image."
244
311
  end
245
312
  optparse.parse!(args)
246
- if args.count < 1
247
- puts optparse
248
- exit 1
249
- end
313
+ verify_args!(args:args, optparse:optparse, count:1)
250
314
 
251
315
  connect(options)
252
- begin
253
- image = find_virtual_image_by_name_or_id(image_name)
254
- return 1 if image.nil?
316
+
317
+ virtual_image = find_virtual_image_by_name_or_id(image_name)
318
+ return 1 if virtual_image.nil?
255
319
 
256
- payload = nil
257
- if options[:payload]
258
- payload = options[:payload]
259
- # support -O OPTION switch on top of --payload
260
- if options[:options]
261
- payload['virtualImage'] ||= {}
262
- payload['virtualImage'].deep_merge!(options[:options].reject {|k,v| k.is_a?(Symbol) })
263
- end
320
+ passed_options = parse_passed_options(options)
321
+ payload = nil
322
+ if options[:payload]
323
+ payload = options[:payload]
324
+ payload.deep_merge!({virtual_image_object_key => passed_options}) unless passed_options.empty?
325
+ else
326
+ virtual_image_payload = passed_options
327
+ if tenants_list
328
+ virtual_image_payload['accounts'] = tenants_list
329
+ end
330
+ # metadata tags
331
+ if options[:tags]
332
+ virtual_image_payload['tags'] = parse_metadata(options[:tags])
264
333
  else
265
- params = options[:options] || {}
266
- if params.empty? && tenants_list.nil?
267
- puts optparse
268
- option_lines = update_virtual_image_option_types().collect {|it| "\t-O #{it['fieldContext'] ? (it['fieldContext'] + '.') : ''}#{it['fieldName']}=\"value\"" }.join("\n")
269
- puts "\nAvailable Options:\n#{option_lines}\n\n"
270
- exit 1
271
- end
272
- if tenants_list
273
- params['accounts'] = tenants_list
274
- end
275
- payload = {'virtualImage' => params}
334
+ # tags = prompt_metadata(options)
335
+ # payload[virtual_image_object_key]['tags'] = tags of tags
276
336
  end
277
- @virtual_images_interface.setopts(options)
278
- if options[:dry_run]
279
- print_dry_run @virtual_images_interface.dry.update(image['id'], payload)
280
- return
337
+ # metadata tags
338
+ if options[:add_tags]
339
+ virtual_image_payload['addTags'] = parse_metadata(options[:add_tags])
281
340
  end
282
- response = @virtual_images_interface.update(image['id'], payload)
283
- if options[:json]
284
- print JSON.pretty_generate(json_response)
285
- if !response['success']
286
- exit 1
287
- end
288
- else
289
- print "\n", cyan, "Virtual Image #{image['name']} updated", reset, "\n\n"
341
+ if options[:remove_tags]
342
+ virtual_image_payload['removeTags'] = parse_metadata(options[:remove_tags])
290
343
  end
291
- rescue RestClient::Exception => e
292
- print_rest_exception(e, options)
293
- exit 1
344
+ if virtual_image_payload.empty?
345
+ raise_command_error "Specify at least one option to update.\n#{optparse}"
346
+ end
347
+ payload = {'virtualImage' => virtual_image_payload}
294
348
  end
349
+ @virtual_images_interface.setopts(options)
350
+ if options[:dry_run]
351
+ print_dry_run @virtual_images_interface.dry.update(virtual_image['id'], payload)
352
+ return
353
+ end
354
+ json_response = @virtual_images_interface.update(virtual_image['id'], payload)
355
+ render_response(json_response, options, 'virtualImage') do
356
+ print_green_success "Updated virtual image #{virtual_image['name']}"
357
+ _get(virtual_image["id"], {}, options)
358
+ end
359
+ return 0, nil
360
+
295
361
  end
296
362
 
297
363
  def virtual_image_types(args)
@@ -368,6 +434,9 @@ EOT
368
434
  tenants_list = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
369
435
  end
370
436
  end
437
+ opts.on('--tags LIST', String, "Metadata tags in the format 'name:value, name:value'") do |val|
438
+ options[:tags] = val
439
+ end
371
440
  # build_option_type_options(opts, options, add_virtual_image_option_types)
372
441
  # build_option_type_options(opts, options, add_virtual_image_advanced_option_types)
373
442
  build_standard_add_options(opts, options)
@@ -450,6 +519,14 @@ EOT
450
519
  if tenants_list
451
520
  virtual_image_payload['accounts'] = tenants_list
452
521
  end
522
+ # metadata tags
523
+ if options[:tags]
524
+ tags = parse_metadata(options[:tags])
525
+ virtual_image_payload['tags'] = tags if tags
526
+ else
527
+ # tags = prompt_metadata(options)
528
+ # virtual_image_payload['tags'] = tags of tags
529
+ end
453
530
  # fix issue with api returning imageType vmware instead of vmdk
454
531
  if virtual_image_payload && virtual_image_payload['imageType'] == 'vmware'
455
532
  virtual_image_payload['imageType'] == 'vmdk'
@@ -719,16 +796,17 @@ EOT
719
796
  tmp_option_types = [
720
797
  {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true, 'displayOrder' => 1},
721
798
  #{'fieldName' => 'imageType', 'fieldLabel' => 'Image Type', 'type' => 'select', 'optionSource' => 'virtualImageTypes', 'required' => true, 'description' => 'Select Virtual Image Type.', 'displayOrder' => 2},
722
- {'fieldName' => 'osType', 'fieldLabel' => 'OS Type', 'type' => 'select', 'optionSource' => 'osTypes', 'required' => false, 'description' => 'Select OS Type.', 'displayOrder' => 3},
723
- {'fieldName' => 'minRam', 'fieldLabel' => 'Minimum Memory (MB)', 'type' => 'number', 'required' => false, 'description' => 'Minimum Memory (MB)', 'displayOrder' => 4},
724
- {'fieldName' => 'isCloudInit', 'fieldLabel' => 'Cloud Init Enabled?', 'type' => 'checkbox', 'required' => false, 'description' => 'Cloud Init Enabled?', 'displayOrder' => 5},
725
- {'fieldName' => 'installAgent', 'fieldLabel' => 'Install Agent?', 'type' => 'checkbox', 'required' => false, 'description' => 'Install Agent?', 'displayOrder' => 6},
799
+ {'fieldName' => 'osType', 'fieldLabel' => 'Operating System', 'type' => 'select', 'optionSource' => 'osTypes', 'required' => false, 'description' => 'Select Operating System.', 'displayOrder' => 3},
800
+ {'fieldName' => 'minRamGB', 'fieldLabel' => 'Minimum Memory (GB)', 'type' => 'number', 'required' => false, 'description' => 'Minimum Memory (GB)', 'displayOrder' => 4},
801
+ # {'fieldName' => 'minDiskGB', 'fieldLabel' => 'Minimum Disk (GB)', 'type' => 'number', 'required' => false, 'description' => 'Minimum Memory (GB)', 'displayOrder' => 4},
802
+ {'fieldName' => 'isCloudInit', 'fieldLabel' => 'Cloud Init Enabled?', 'type' => 'checkbox', 'defaultValue' => 'off', 'required' => false, 'description' => 'Cloud Init Enabled?', 'displayOrder' => 5},
803
+ {'fieldName' => 'installAgent', 'fieldLabel' => 'Install Agent?', 'type' => 'checkbox', 'defaultValue' => 'off', 'required' => false, 'description' => 'Install Agent?', 'displayOrder' => 6},
726
804
  {'fieldName' => 'sshUsername', 'fieldLabel' => 'SSH Username', 'type' => 'text', 'required' => false, 'description' => 'Enter an SSH Username', 'displayOrder' => 7},
727
805
  {'fieldName' => 'sshPassword', 'fieldLabel' => 'SSH Password', 'type' => 'password', 'required' => false, 'description' => 'Enter an SSH Password', 'displayOrder' => 8},
728
806
  {'fieldName' => 'storageProviderId', 'type' => 'select', 'fieldLabel' => 'Storage Provider', 'optionSource' => 'storageProviders', 'required' => false, 'description' => 'Select Storage Provider.', 'displayOrder' => 9},
729
807
  {'fieldName' => 'userData', 'fieldLabel' => 'Cloud-Init User Data', 'type' => 'textarea', 'required' => false, 'displayOrder' => 10},
730
808
  {'fieldName' => 'visibility', 'fieldLabel' => 'Visibility', 'type' => 'select', 'selectOptions' => [{'name' => 'Private', 'value' => 'private'},{'name' => 'Public', 'value' => 'public'}], 'required' => false, 'description' => 'Visibility', 'category' => 'permissions', 'defaultValue' => 'private', 'displayOrder' => 40},
731
- {'fieldName' => 'isAutoJoinDomain', 'fieldLabel' => 'Auto Join Domain?', 'type' => 'checkbox', 'required' => false, 'description' => 'Auto Join Domain?', 'category' => 'advanced', 'displayOrder' => 40},
809
+ {'fieldName' => 'isAutoJoinDomain', 'fieldLabel' => 'Auto Join Domain?', 'type' => 'checkbox', 'defaultValue' => 'off', 'required' => false, 'description' => 'Auto Join Domain?', 'category' => 'advanced', 'displayOrder' => 40},
732
810
  {'fieldName' => 'virtioSupported', 'fieldLabel' => 'VirtIO Drivers Loaded?', 'type' => 'checkbox', 'defaultValue' => 'on', 'required' => false, 'description' => 'VirtIO Drivers Loaded?', 'category' => 'advanced', 'displayOrder' => 40},
733
811
  {'fieldName' => 'vmToolsInstalled', 'fieldLabel' => 'VM Tools Installed?', 'type' => 'checkbox', 'defaultValue' => 'on', 'required' => false, 'description' => 'VM Tools Installed?', 'category' => 'advanced', 'displayOrder' => 40},
734
812
  {'fieldName' => 'isForceCustomization', 'fieldLabel' => 'Force Guest Customization?', 'type' => 'checkbox', 'defaultValue' => 'off', 'required' => false, 'description' => 'Force Guest Customization?', 'category' => 'advanced', 'displayOrder' => 40},
@@ -767,7 +845,10 @@ EOT
767
845
 
768
846
  def update_virtual_image_option_types(image_type = nil)
769
847
  list = add_virtual_image_option_types(image_type)
770
- list.each {|it| it['required'] = false }
848
+ list.each {|it|
849
+ it.delete('required')
850
+ it.delete('defaultValue')
851
+ }
771
852
  list
772
853
  end
773
854
 
@@ -817,4 +898,17 @@ EOT
817
898
  return rtn
818
899
  end
819
900
 
901
+ def format_virtual_image_source(virtual_image, return_color=cyan)
902
+ out = ""
903
+ if virtual_image['userUploaded']
904
+ # out << "#{green}UPLOADED#{return_color}"
905
+ out << "#{cyan}UPLOADED#{return_color}"
906
+ elsif virtual_image['systemImage']
907
+ out << "#{cyan}SYSTEM#{return_color}"
908
+ else
909
+ out << "#{cyan}SYNCED#{return_color}"
910
+ end
911
+ out
912
+ end
913
+
820
914
  end
@@ -352,7 +352,7 @@ def format_number(n, opts={})
352
352
  delim = opts[:delimiter] || ','
353
353
  out = ""
354
354
  parts = n.to_s.split(".")
355
- whole_number = parts[0]
355
+ whole_number = parts[0].to_s
356
356
  decimal = parts[1] ? parts[1..-1].join('.') : nil
357
357
  i = 0
358
358
  whole_number.reverse.each_char do |c|
@@ -366,10 +366,16 @@ def format_number(n, opts={})
366
366
  end
367
367
 
368
368
  def format_sig_dig(n, sigdig=3, min_sigdig=nil, pad_zeros=false)
369
- v = sprintf("%.#{sigdig}f", n)
370
- if pad_zeros != true
371
- v = v.to_f.to_s
369
+ v = ""
370
+ if sigdig && sigdig > 0
371
+ # v = n.to_i.round(sigdig).to_s
372
+ v = sprintf("%.#{sigdig}f", n)
373
+ else
374
+ v = n.to_i.round().to_s
372
375
  end
376
+ # if pad_zeros != true
377
+ # v = v.to_f.to_s
378
+ # end
373
379
  if min_sigdig
374
380
  v_parts = v.split(".")
375
381
  decimal_str = v_parts[1]
@@ -383,12 +389,18 @@ def format_sig_dig(n, sigdig=3, min_sigdig=nil, pad_zeros=false)
383
389
  end
384
390
 
385
391
  def currency_sym(currency)
386
- Money::Currency.new((currency || 'usd').to_sym).symbol
392
+ Money::Currency.new((currency || 'USD').to_sym).symbol
387
393
  end
388
394
 
389
395
  # returns currency amount formatted like "$4,5123.00". 0.00 is formatted as "$0"
390
396
  # this is not ideal
391
- def format_currency(amount, currency='usd', opts={})
397
+ def format_currency(amount, currency='USD', opts={})
398
+ # currency '' should probably error, like money gem does
399
+ if currency.to_s.empty?
400
+ currency = 'USD'
401
+ end
402
+ currency = currency.to_s.upcase
403
+
392
404
  amount = amount.to_f
393
405
  if amount == 0
394
406
  return currency_sym(currency).to_s + "0"
@@ -397,7 +409,7 @@ def format_currency(amount, currency='usd', opts={})
397
409
  # return currency_sym(currency).to_s + "#{amount}"
398
410
  else
399
411
  sigdig = opts[:sigdig] ? opts[:sigdig].to_i : 2 # max decimal digits
400
- min_sigdig = opts[:min_sigdig] ? opts[:min_sigdig].to_i : 2 # min decimal digits
412
+ min_sigdig = opts[:min_sigdig] ? opts[:min_sigdig].to_i : (sigdig || 2) # min decimal digits
401
413
  display_value = format_sig_dig(amount, sigdig, min_sigdig, opts[:pad_zeros])
402
414
  display_value = format_number(display_value) # commas
403
415
  rtn = currency_sym(currency).to_s + display_value
@@ -417,8 +429,9 @@ alias :format_money :format_currency
417
429
  # format_currency(amount, currency, opts)
418
430
  # end
419
431
 
420
- def format_list(items, conjunction="and", limit=nil)
432
+ def format_list(items, conjunction="", limit=nil)
421
433
  items = items ? items.clone : []
434
+ num_items = items.size
422
435
  if limit
423
436
  items = items.first(limit)
424
437
  end
@@ -426,7 +439,11 @@ def format_list(items, conjunction="and", limit=nil)
426
439
  if items.empty?
427
440
  return "#{last_item}"
428
441
  else
429
- return items.join(", ") + (conjunction.to_s.empty? ? ", " : " #{conjunction} ") + "#{last_item}" + ((limit && limit < (items.size+1)) ? " ..." : "")
442
+ if limit && limit < num_items
443
+ items << last_item
444
+ last_item = "(#{num_items - items.size} more)"
445
+ end
446
+ return items.join(", ") + (conjunction.to_s.empty? ? ", " : " #{conjunction} ") + "#{last_item}"
430
447
  end
431
448
  end
432
449
 
@@ -437,3 +454,11 @@ end
437
454
  def ored_list(items, limit=nil)
438
455
  format_list(items, "or", limit)
439
456
  end
457
+
458
+ def format_name_values(obj)
459
+ if obj.is_a?(Hash)
460
+ obj.collect {|k,v| "#{k}: #{v}"}.join(", ")
461
+ else
462
+ ""
463
+ end
464
+ end