morpheus-cli 2.12.5 → 3.1.0

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +5 -0
  3. data/lib/morpheus/api/api_client.rb +15 -30
  4. data/lib/morpheus/api/app_templates_interface.rb +34 -7
  5. data/lib/morpheus/api/apps_interface.rb +20 -1
  6. data/lib/morpheus/api/archive_buckets_interface.rb +124 -0
  7. data/lib/morpheus/api/archive_files_interface.rb +182 -0
  8. data/lib/morpheus/api/{network_pools_interface.rb → image_builder_boot_scripts_interface.rb} +6 -6
  9. data/lib/morpheus/api/{policies_interface.rb → image_builder_image_builds_interface.rb} +20 -15
  10. data/lib/morpheus/api/image_builder_interface.rb +26 -0
  11. data/lib/morpheus/api/{network_proxies_interface.rb → image_builder_preseed_scripts_interface.rb} +6 -6
  12. data/lib/morpheus/cli.rb +10 -9
  13. data/lib/morpheus/cli/alias_command.rb +10 -9
  14. data/lib/morpheus/cli/app_templates.rb +1566 -457
  15. data/lib/morpheus/cli/apps.rb +284 -108
  16. data/lib/morpheus/cli/archives_command.rb +2184 -0
  17. data/lib/morpheus/cli/boot_scripts_command.rb +382 -0
  18. data/lib/morpheus/cli/cli_command.rb +9 -35
  19. data/lib/morpheus/cli/error_handler.rb +2 -0
  20. data/lib/morpheus/cli/hosts.rb +15 -3
  21. data/lib/morpheus/cli/image_builder_command.rb +1208 -0
  22. data/lib/morpheus/cli/instances.rb +118 -47
  23. data/lib/morpheus/cli/man_command.rb +27 -24
  24. data/lib/morpheus/cli/mixins/print_helper.rb +19 -5
  25. data/lib/morpheus/cli/mixins/provisioning_helper.rb +20 -20
  26. data/lib/morpheus/cli/option_types.rb +45 -14
  27. data/lib/morpheus/cli/preseed_scripts_command.rb +381 -0
  28. data/lib/morpheus/cli/remote.rb +1 -0
  29. data/lib/morpheus/cli/roles.rb +2 -2
  30. data/lib/morpheus/cli/shell.rb +3 -2
  31. data/lib/morpheus/cli/version.rb +1 -1
  32. data/lib/morpheus/ext/hash.rb +22 -0
  33. data/lib/morpheus/formatters.rb +33 -0
  34. data/lib/morpheus/terminal.rb +1 -1
  35. metadata +13 -21
  36. data/lib/morpheus/api/cloud_policies_interface.rb +0 -47
  37. data/lib/morpheus/api/group_policies_interface.rb +0 -47
  38. data/lib/morpheus/api/network_domains_interface.rb +0 -47
  39. data/lib/morpheus/api/network_groups_interface.rb +0 -47
  40. data/lib/morpheus/api/network_pool_servers_interface.rb +0 -47
  41. data/lib/morpheus/api/network_services_interface.rb +0 -47
  42. data/lib/morpheus/api/networks_interface.rb +0 -54
  43. data/lib/morpheus/cli/network_domains_command.rb +0 -571
  44. data/lib/morpheus/cli/network_groups_command.rb +0 -602
  45. data/lib/morpheus/cli/network_pool_servers_command.rb +0 -430
  46. data/lib/morpheus/cli/network_pools_command.rb +0 -495
  47. data/lib/morpheus/cli/network_proxies_command.rb +0 -594
  48. data/lib/morpheus/cli/network_services_command.rb +0 -148
  49. data/lib/morpheus/cli/networks_command.rb +0 -855
  50. data/lib/morpheus/cli/policies_command.rb +0 -847
  51. data/scripts/generate_morpheus_commands_help.morpheus +0 -1313
@@ -55,6 +55,8 @@ class Morpheus::Cli::ErrorHandler
55
55
  # if !options[:debug]
56
56
  # return exit_code
57
57
  # end
58
+ when ArgumentError
59
+ @stderr.puts "#{red}Argument Error: #{err.message}#{reset}"
58
60
  else
59
61
  @stderr.puts "#{red}Unexpected Error#{reset}"
60
62
  end
@@ -605,13 +605,25 @@ class Morpheus::Cli::Hosts
605
605
  options = {}
606
606
  query_params = {removeResources: 'on', force: 'off'}
607
607
  optparse = OptionParser.new do|opts|
608
- opts.banner = subcommand_usage("[name] [-f] [-S]")
609
- opts.on( '-f', '--force', "Force Remove" ) do
608
+ opts.banner = subcommand_usage("[name] [-fS]")
609
+ opts.on( '-f', '--force', "Force Delete" ) do
610
610
  query_params[:force] = 'on'
611
611
  end
612
- opts.on( '-S', '--skip-remove-infrastructure', "Skip removal of underlying cloud infrastructure" ) do
612
+ opts.on( '-S', '--skip-remove-infrastructure', "Skip removal of underlying cloud infrastructure. Same as --remove-resources off" ) do
613
613
  query_params[:removeResources] = 'off'
614
614
  end
615
+ opts.on('--remove-resources [on|off]', ['on','off'], "Remove Infrastructure. Default is on if server is managed.") do |val|
616
+ query_params[:removeResources] = val
617
+ end
618
+ opts.on('--remove-volumes [on|off]', ['on','off'], "Remove Volumes. Default is on.") do |val|
619
+ query_params[:removeVolumes] = val
620
+ end
621
+ opts.on('--remove-instances [on|off]', ['on','off'], "Remove Associated Instances.") do |val|
622
+ query_params[:removeInstances] = val
623
+ end
624
+ opts.on('--release-eips [on|off]', ['on','off'], "Release EIPs, default is true. Amazon only.") do |val|
625
+ params[:releaseEIPs] = val
626
+ end
615
627
  build_common_options(opts, options, [:auto_confirm, :json, :dry_run, :quiet, :remote])
616
628
  end
617
629
  optparse.parse!(args)
@@ -0,0 +1,1208 @@
1
+ require 'json'
2
+ require 'yaml'
3
+ require 'rest_client'
4
+ require 'optparse'
5
+ require 'filesize'
6
+ require 'table_print'
7
+ require 'morpheus/cli/cli_command'
8
+ require 'morpheus/cli/mixins/provisioning_helper'
9
+ require 'morpheus/cli/boot_scripts_command'
10
+ require 'morpheus/cli/preseed_scripts_command'
11
+
12
+ class Morpheus::Cli::ImageBuilderCommand
13
+ include Morpheus::Cli::CliCommand
14
+ include Morpheus::Cli::ProvisioningHelper
15
+
16
+ set_command_name :'image-builder' # :'image-builds'
17
+
18
+ register_subcommands :list, :get, :add, :update, :remove
19
+ register_subcommands :run
20
+ register_subcommands :'list-runs' => :list_executions
21
+
22
+ # err, these are kept under this namespace
23
+ register_subcommands :'boot-scripts' => :boot_scripts
24
+ register_subcommands :'preseed-scripts' => :preseed_scripts
25
+
26
+ # set_default_subcommand :list
27
+
28
+ def initialize()
29
+ # @appliance_name, @appliance_url = Morpheus::Cli::Remote.active_appliance
30
+ end
31
+
32
+ def connect(opts)
33
+ @api_client = establish_remote_appliance_connection(opts)
34
+ @image_builder_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).image_builder
35
+ @image_builds_interface = @image_builder_interface.image_builds
36
+ @boot_scripts_interface = @image_builder_interface.boot_scripts
37
+ @preseed_scripts_interface = @image_builder_interface.preseed_scripts
38
+ @groups_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).groups
39
+ @clouds_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).clouds
40
+ @instances_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).instances
41
+ @instance_types_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).instance_types
42
+ @options_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).options
43
+ @active_group_id = Morpheus::Cli::Groups.active_groups[@appliance_name]
44
+ end
45
+
46
+ def handle(args)
47
+ handle_subcommand(args)
48
+ end
49
+
50
+ def boot_scripts(args)
51
+ Morpheus::Cli::BootScriptsCommand.new.handle(args)
52
+ end
53
+
54
+ def preseed_scripts(args)
55
+ Morpheus::Cli::PreseedScriptsCommand.new.handle(args)
56
+ end
57
+
58
+ def list(args)
59
+ options = {}
60
+ optparse = OptionParser.new do|opts|
61
+ opts.banner = subcommand_usage()
62
+ build_common_options(opts, options, [:list, :json, :dry_run])
63
+ end
64
+ optparse.parse!(args)
65
+ connect(options)
66
+ begin
67
+ params = {}
68
+ [:phrase, :offset, :max, :sort, :direction].each do |k|
69
+ params[k] = options[k] unless options[k].nil?
70
+ end
71
+
72
+ if options[:dry_run]
73
+ print_dry_run @image_builds_interface.dry.list(params)
74
+ return
75
+ end
76
+
77
+ json_response = @image_builds_interface.list(params)
78
+ if options[:json]
79
+ print JSON.pretty_generate(json_response)
80
+ print "\n"
81
+ return
82
+ end
83
+ image_builds = json_response['imageBuilds']
84
+ title = "Morpheus Image Builds"
85
+ subtitles = []
86
+ # if group
87
+ # subtitles << "Group: #{group['name']}".strip
88
+ # end
89
+ # if cloud
90
+ # subtitles << "Cloud: #{cloud['name']}".strip
91
+ # end
92
+ if params[:phrase]
93
+ subtitles << "Search: #{params[:phrase]}".strip
94
+ end
95
+ print_h1 title, subtitles
96
+ if image_builds.empty?
97
+ print cyan,"No image builds found.",reset,"\n"
98
+ else
99
+ rows = image_builds.collect {|image_build|
100
+ last_result = image_build['lastResult']
101
+ status_str = format_image_build_status(image_build, cyan)
102
+ result_str = format_image_build_execution_result(last_result, cyan)
103
+ row = {
104
+ id: image_build['id'],
105
+ name: image_build['name'],
106
+ # description: image_build['description'],
107
+ type: image_build['type'] ? image_build['type']['name'] : 'N/A',
108
+ group: image_build['site'] ? image_build['site']['name'] : '',
109
+ cloud: image_build['zone'] ? image_build['zone']['name'] : '',
110
+ executionCount: image_build['executionCount'] ? image_build['executionCount'].to_i : '',
111
+ lastRunDate: last_result ? format_local_dt(last_result['startDate']) : '',
112
+ status: status_str,
113
+ result: result_str
114
+ }
115
+ row
116
+ }
117
+ columns = [:id, :name, :type, {:lastRunDate => {label: 'Last Run Date'.upcase}},
118
+ :status, {:result => {max_width: 60}}]
119
+ term_width = current_terminal_width()
120
+ # if term_width > 170
121
+ # columns += [:cpu, :memory, :storage]
122
+ # end
123
+ # custom pretty table columns ...
124
+ if options[:include_fields]
125
+ columns = options[:include_fields]
126
+ end
127
+ print cyan
128
+ print as_pretty_table(rows, columns, options)
129
+ print reset
130
+ print_results_pagination(json_response)
131
+ end
132
+ print reset,"\n"
133
+ rescue RestClient::Exception => e
134
+ print_rest_exception(e, options)
135
+ exit 1
136
+ end
137
+ end
138
+
139
+ def get(args)
140
+ options = {}
141
+ optparse = OptionParser.new do|opts|
142
+ opts.banner = subcommand_usage("[image-build]")
143
+ build_common_options(opts, options, [:json, :dry_run])
144
+ end
145
+ optparse.parse!(args)
146
+ if args.count < 1
147
+ print_error Morpheus::Terminal.angry_prompt
148
+ puts_error "#{command_name} missing argument: [image-build]\n#{optparse}"
149
+ return 1
150
+ end
151
+ connect(options)
152
+ begin
153
+ if options[:dry_run]
154
+ if args[0].to_s =~ /\A\d{1,}\Z/
155
+ print_dry_run @image_builds_interface.dry.get(args[0].to_i)
156
+ else
157
+ print_dry_run @image_builds_interface.dry.list({name:args[0]})
158
+ end
159
+ return
160
+ end
161
+ image_build = find_image_build_by_name_or_id(args[0])
162
+ return 1 if image_build.nil?
163
+ # json_response = {'imageBuild' => image_build} # skip redundant request
164
+ json_response = @image_builds_interface.get(image_build['id'])
165
+ image_build = json_response['imageBuild']
166
+ if options[:json]
167
+ print JSON.pretty_generate(json_response)
168
+ return
169
+ end
170
+ print_h1 "Image Build Details"
171
+ print cyan
172
+ description_cols = {
173
+ "ID" => 'id',
174
+ # "Account" => lambda {|it| it['account'] ? it['account']['name'] : '' },
175
+ "Name" => 'name',
176
+ "Description" => 'description',
177
+ "Group" => lambda {|it| it['site'] ? it['site']['name'] : '' },
178
+ "Cloud" => lambda {|it| it['zone'] ? it['zone']['name'] : '' },
179
+ "Plan" => lambda {|it|
180
+ if it['config'] && it['config']['plan']
181
+ it['config']['plan']['code'] # name needed!
182
+ else
183
+ ""
184
+ end
185
+ },
186
+ "Image" => lambda {|it|
187
+ if it['config'] && it['config']['template']
188
+ it['config']['template']
189
+ elsif it['config'] && it['config']['image']
190
+ it['config']['image']
191
+ else
192
+ ""
193
+ end
194
+ },
195
+ "Boot Script" => lambda {|it|
196
+ if it['bootScript']
197
+ if it['bootScript'].kind_of?(Hash)
198
+ it['bootScript']['fileName'] || it['bootScript']['name'] || it['bootScript']['id']
199
+ else
200
+ it['bootScript']
201
+ end
202
+ else
203
+ ""
204
+ end
205
+ },
206
+ "Preseed Script" => lambda {|it|
207
+ if it['preseedScript']
208
+ if it['preseedScript'].kind_of?(Hash)
209
+ it['preseedScript']['fileName'] || it['preseedScript']['name'] || it['preseedScript']['id']
210
+ else
211
+ it['preseedScript']
212
+ end
213
+ else
214
+ ""
215
+ end
216
+ },
217
+ # Additional Scripts
218
+ "Scripts" => lambda {|it|
219
+ if it['scripts']
220
+ script_names = it['scripts'].collect do |script|
221
+ if script.kind_of?(Hash)
222
+ script['name']
223
+ else
224
+ script
225
+ end
226
+ end
227
+ script_names.join(", ")
228
+ else
229
+ ""
230
+ end
231
+ },
232
+ "SSH Username" => lambda {|it| it['sshUsername'] },
233
+ "SSH Password" => lambda {|it| it['sshPassword'].to_s.empty? ? '' : '(hidden)' }, # api returns masked
234
+ "Storage Provider" => lambda {|it|
235
+ if it['storageProvider']
236
+ if it['storageProvider'].kind_of?(Hash)
237
+ it['storageProvider']['name'] || it['storageProvider']['id']
238
+ else
239
+ it['storageProvider']
240
+ end
241
+ else
242
+ ""
243
+ end
244
+ },
245
+ "Build Output Name" => lambda {|it| it['buildOutputName'] },
246
+ "Conversion Formats" => lambda {|it| it['conversionFormats'] },
247
+ "Cloud Init?" => lambda {|it| it['isCloudInit'] ? 'Yes' : 'No' },
248
+ "Keep Results" => lambda {|it| it['keepResults'].to_i == 0 ? 'All' : it['keepResults'] },
249
+ "Last Run Date" => lambda {|it|
250
+ last_result = it['lastResult']
251
+ last_result ? format_local_dt(last_result['startDate']) : ''
252
+ },
253
+ "Status" => lambda {|it| format_image_build_status(it) },
254
+ }
255
+ print_description_list(description_cols, image_build)
256
+
257
+ #json_response = @image_builds_interface.list_executions(image_build['id'], params)
258
+ image_build_executions = json_response['imageBuildExecutions'] # yep, show() returns the last 100 run
259
+ image_build_executions = image_build_executions.first(10) # limit to just 10
260
+ if image_build_executions && image_build_executions.size > 0
261
+ print_h2 "Recent Executions"
262
+ print_image_build_executions_table(image_build_executions, opts={})
263
+ print_results_pagination({size:image_build_executions.size,total:image_build['executionCount'].to_i}, {:label => "execution", :n_label => "executions"})
264
+ else
265
+ puts "\nNo executions found.\n"
266
+ end
267
+
268
+ print reset,"\n"
269
+
270
+ rescue RestClient::Exception => e
271
+ print_rest_exception(e, options)
272
+ return 1
273
+ end
274
+ end
275
+
276
+ def add(args)
277
+ options = {}
278
+ optparse = OptionParser.new do|opts|
279
+ opts.banner = subcommand_usage("[options]")
280
+ # build_option_type_options(opts, options, add_image_build_option_types(false))
281
+ opts.on( '-t', '--type TYPE', "Image Build Type" ) do |val|
282
+ options['type'] = val
283
+ end
284
+ opts.on('--name VALUE', String, "Name") do |val|
285
+ options['name'] = val
286
+ end
287
+ opts.on('--description VALUE', String, "Description") do |val|
288
+ options['description'] = val
289
+ end
290
+ opts.on( '-g', '--group GROUP', "Group Name or ID" ) do |val|
291
+ options['group'] = val
292
+ end
293
+ opts.on( '-c', '--cloud CLOUD', "Cloud Name or ID" ) do |val|
294
+ options['cloud'] = val
295
+ end
296
+ # opts.on( '-e', '--env ENVIRONMENT', "Environment" ) do |val|
297
+ # options[:cloud] = val
298
+ # end
299
+ opts.on('--config JSON', String, "Instance Config JSON") do |val|
300
+ options['config'] = JSON.parse(val.to_s)
301
+ end
302
+ opts.on('--config-yaml YAML', String, "Instance Config YAML") do |val|
303
+ options['config'] = YAML.load(val.to_s)
304
+ end
305
+ opts.on('--config-file FILE', String, "Instance Config from a local JSON or YAML file") do |val|
306
+ options['configFile'] = val.to_s
307
+ end
308
+ # opts.on('--configFile FILE', String, "Instance Config from a local file") do |val|
309
+ # options['configFile'] = val.to_s
310
+ # end
311
+ opts.on('--bootScript VALUE', String, "Boot Script ID") do |val|
312
+ options['bootScript'] = val.to_s
313
+ end
314
+ opts.on('--bootCommand VALUE', String, "Boot Command. This can be used in place of a bootScript") do |val|
315
+ options['bootCommand'] = val.to_s
316
+ end
317
+ opts.on('--preseedScript VALUE', String, "Preseed Script ID") do |val|
318
+ options['preseedScript'] = val.to_s
319
+ end
320
+ opts.on('--scripts LIST', String, "Additional Scripts (comma separated names or ids)") do |val|
321
+ # uh don't put commas or leading/trailing spaces in script names pl
322
+ options['scripts'] = val.to_s.split(",").collect {|it| it.to_s.strip }.select {|it| it }.compact
323
+ end
324
+ opts.on('--sshUsername VALUE', String, "SSH Username") do |val|
325
+ options['sshUsername'] = val.to_s
326
+ end
327
+ opts.on('--sshPassword VALUE', String, "SSH Password") do |val|
328
+ options['sshPassword'] = val.to_s
329
+ end
330
+ opts.on('--storageProvider VALUE', String, "Storage Provider ID") do |val|
331
+ options['storageProvider'] = val.to_s
332
+ end
333
+ opts.on('--isCloudInit [on|off]', String, "Cloud Init?") do |val|
334
+ options['isCloudInit'] = (val.to_s == 'on' || val.to_s == 'true')
335
+ end
336
+ opts.on('--buildOutputName VALUE', String, "Build Output Name") do |val|
337
+ options['buildOutputName'] = val.to_s
338
+ end
339
+ opts.on('--conversionFormats VALUE', String, "Conversion Formats ie. ovf, qcow2, vhd") do |val|
340
+ options['conversionFormats'] = val.to_s
341
+ end
342
+ opts.on('--keepResults VALUE', String, "Keep only the most recent builds. Older executions will be deleted along with their associated Virtual Images. The value 0 disables this functionality.") do |val|
343
+ options['keepResults'] = val.to_i
344
+ end
345
+ build_common_options(opts, options, [:options, :json, :dry_run, :quiet])
346
+ end
347
+ optparse.parse!(args)
348
+ if args.count > 1
349
+ puts_error "#{Morpheus::Terminal.angry_prompt}wrong number of arguments. Expected 0-1 and received #{args.count} #{args.inspect}\n#{optparse}"
350
+ return 1
351
+ end
352
+ connect(options)
353
+ begin
354
+ options.merge!(options[:options]) if options[:options] # so -O var= works..
355
+
356
+ # use the -g GROUP or active group by default
357
+ # options['group'] ||= @active_group_id
358
+
359
+ # support first arg as name instead of --name
360
+ if args[0] && !options['name']
361
+ options['name'] = args[0]
362
+ end
363
+
364
+ image_build_payload = prompt_new_image_build(options)
365
+ return 1 if !image_build_payload
366
+ payload = {'imageBuild' => image_build_payload}
367
+
368
+ if options[:dry_run]
369
+ print_dry_run @image_builds_interface.dry.create(payload)
370
+ return
371
+ end
372
+ json_response = @image_builds_interface.create(payload)
373
+ if options[:json]
374
+ print JSON.pretty_generate(json_response)
375
+ print "\n"
376
+ elsif !options[:quiet]
377
+ new_image_build = json_response['imageBuild']
378
+ print_green_success "Added image build #{new_image_build['name']}"
379
+ get([new_image_build['id']])
380
+ # list([])
381
+ end
382
+
383
+ rescue RestClient::Exception => e
384
+ print_rest_exception(e, options)
385
+ return 1
386
+ end
387
+ end
388
+
389
+ def update(args)
390
+ options = {}
391
+ optparse = OptionParser.new do|opts|
392
+ opts.banner = subcommand_usage("[image-build] [options]")
393
+ # build_option_type_options(opts, options, update_image_build_option_types(false))
394
+ # cannot update type
395
+ opts.on( '-t', '--type TYPE', "Image Build Type" ) do |val|
396
+ options['type'] = val
397
+ end
398
+ opts.on('--name VALUE', String, "New Name") do |val|
399
+ options['name'] = val
400
+ end
401
+ opts.on('--description VALUE', String, "Description") do |val|
402
+ options['description'] = val
403
+ end
404
+ opts.on( '-g', '--group GROUP', "Group Name or ID" ) do |val|
405
+ options['group'] = val
406
+ end
407
+ opts.on( '-c', '--cloud CLOUD', "Cloud Name or ID" ) do |val|
408
+ options['cloud'] = val
409
+ end
410
+ # opts.on( '-e', '--env ENVIRONMENT', "Environment" ) do |val|
411
+ # options[:cloud] = val
412
+ # end
413
+ opts.on('--config JSON', String, "Instance Config JSON") do |val|
414
+ options['config'] = JSON.parse(val.to_s)
415
+ end
416
+ opts.on('--config-yaml YAML', String, "Instance Config YAML") do |val|
417
+ options['config'] = YAML.load(val.to_s)
418
+ end
419
+ opts.on('--config-file FILE', String, "Instance Config from a local JSON or YAML file") do |val|
420
+ options['configFile'] = val.to_s
421
+ end
422
+ # opts.on('--configFile FILE', String, "Instance Config from a local file") do |val|
423
+ # options['configFile'] = val.to_s
424
+ # end
425
+ opts.on('--bootScript VALUE', String, "Boot Script ID") do |val|
426
+ options['bootScript'] = val.to_s
427
+ end
428
+ opts.on('--bootCommand VALUE', String, "Boot Command. This can be used in place of a bootScript") do |val|
429
+ options['bootCommand'] = val.to_s
430
+ end
431
+ opts.on('--preseedScript VALUE', String, "Preseed Script ID") do |val|
432
+ options['preseedScript'] = val.to_s
433
+ end
434
+ opts.on('--scripts LIST', String, "Additional Scripts (comma separated names or ids)") do |val|
435
+ # uh don't put commas or leading/trailing spaces in script names pl
436
+ options['scripts'] = val.to_s.split(",").collect {|it| it.to_s.strip }.select {|it| it }.compact
437
+ end
438
+ opts.on('--sshUsername VALUE', String, "SSH Username") do |val|
439
+ options['sshUsername'] = val.to_s
440
+ end
441
+ opts.on('--sshPassword VALUE', String, "SSH Password") do |val|
442
+ options['sshPassword'] = val.to_s
443
+ end
444
+ opts.on('--storageProvider VALUE', String, "Storage Provider ID") do |val|
445
+ options['storageProvider'] = val.to_s
446
+ end
447
+ opts.on('--isCloudInit [on|off]', String, "Cloud Init?") do |val|
448
+ options['isCloudInit'] = (val.to_s == 'on' || val.to_s == 'true')
449
+ end
450
+ opts.on('--buildOutputName VALUE', String, "Build Output Name") do |val|
451
+ options['buildOutputName'] = val.to_s
452
+ end
453
+ opts.on('--conversionFormats VALUE', String, "Conversion Formats ie. ovf, qcow2, vhd") do |val|
454
+ options['conversionFormats'] = val.to_s
455
+ end
456
+ # opts.on('--keepResultsEnabled [on|off]', String, "Delete Old Results. Enables the Keep Results option") do |val|
457
+ # options['keepResultsEnabled'] = (val.to_s == 'on' || val.to_s == 'true')
458
+ # end
459
+ opts.on('--keepResults VALUE', String, "Keep only the most recent builds. Older executions will be deleted along with their associated Virtual Images. The value 0 disables this functionality.") do |val|
460
+ options['keepResults'] = val.to_i
461
+ # 0 disables it
462
+ # options['deleteOldResults'] = (options['keepResults'] > 0)
463
+ end
464
+ build_common_options(opts, options, [:options, :json, :dry_run, :quiet])
465
+ end
466
+ optparse.parse!(args)
467
+ if args.count != 1
468
+ puts_error "#{Morpheus::Terminal.angry_prompt}wrong number of arguments. Expected 1 and received #{args.count} #{args.inspect}\n#{optparse}"
469
+ return 1
470
+ end
471
+ connect(options)
472
+ begin
473
+ image_build = find_image_build_by_name_or_id(args[0])
474
+ return 1 if !image_build
475
+ image_build_payload = prompt_edit_image_build(image_build, options)
476
+ return 1 if !image_build_payload
477
+ payload = {imageBuild: image_build_payload}
478
+ if options[:dry_run]
479
+ print_dry_run @image_builds_interface.dry.update(image_build["id"], payload)
480
+ return
481
+ end
482
+ json_response = @image_builds_interface.update(image_build["id"], payload)
483
+ if options[:json]
484
+ print JSON.pretty_generate(json_response)
485
+ print "\n"
486
+ else
487
+ print_green_success "Updated image build #{image_build['name']}"
488
+ get([image_build['id']])
489
+ end
490
+ return 0
491
+ rescue RestClient::Exception => e
492
+ print_rest_exception(e, options)
493
+ return 1
494
+ end
495
+ end
496
+
497
+ def remove(args)
498
+ options = {}
499
+ query_params = {}
500
+ optparse = OptionParser.new do|opts|
501
+ opts.banner = subcommand_usage("[image-build]")
502
+ opts.on( '-K', '--keep-virtual-images', "Preserve associated virtual images" ) do
503
+ query_params['keepVirtualImages'] = 'on'
504
+ end
505
+ build_common_options(opts, options, [:account, :auto_confirm, :json, :dry_run])
506
+ end
507
+ optparse.parse!(args)
508
+
509
+ if args.count < 1
510
+ print_error Morpheus::Terminal.angry_prompt
511
+ puts_error "#{command_name} missing argument: [image-build]\n#{optparse}"
512
+ return 1
513
+ end
514
+
515
+ connect(options)
516
+ begin
517
+ image_build = find_image_build_by_name_or_id(args[0])
518
+ return 1 if image_build.nil?
519
+
520
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete the image build: #{image_build['name']}?")
521
+ return 9, "aborted command"
522
+ end
523
+ if query_params['keepVirtualImages'].nil?
524
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'keepVirtualImages', 'type' => 'checkbox', 'fieldLabel' => 'Keep Virtual Images?', 'required' => false, 'defaultValue' => false, 'description' => 'Preserve associated virtual images. By default, they are deleted as well.'}],options,@api_client,{})
525
+ query_params['keepVirtualImages'] = v_prompt['keepVirtualImages']
526
+ end
527
+ if options[:dry_run]
528
+ print_dry_run @image_builds_interface.dry.destroy(image_build['id'], query_params)
529
+ return 0
530
+ end
531
+ json_response = @image_builds_interface.destroy(image_build['id'], query_params)
532
+ if options[:json]
533
+ print JSON.pretty_generate(json_response)
534
+ print "\n"
535
+ else
536
+ print_green_success "Removed image build #{image_build['name']}"
537
+ # list([])
538
+ end
539
+ return 0
540
+ rescue RestClient::Exception => e
541
+ print_rest_exception(e, options)
542
+ return 1
543
+ end
544
+ end
545
+
546
+ def run(args)
547
+ options = {}
548
+ query_params = {}
549
+ optparse = OptionParser.new do|opts|
550
+ opts.banner = subcommand_usage("[image-build]")
551
+ build_common_options(opts, options, [:account, :auto_confirm, :json, :dry_run])
552
+ end
553
+ optparse.parse!(args)
554
+
555
+ if args.count < 1
556
+ print_error Morpheus::Terminal.angry_prompt
557
+ puts_error "#{command_name} missing argument: [image-build]\n#{optparse}"
558
+ return 1
559
+ end
560
+
561
+ connect(options)
562
+ begin
563
+ image_build = find_image_build_by_name_or_id(args[0])
564
+ return 1 if image_build.nil?
565
+
566
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to run the image build: #{image_build['name']}?")
567
+ return 9, "aborted command"
568
+ end
569
+ if options[:dry_run]
570
+ print_dry_run @image_builds_interface.dry.run(image_build['id'], query_params)
571
+ return 0
572
+ end
573
+ json_response = @image_builds_interface.run(image_build['id'], query_params)
574
+ if options[:json]
575
+ print JSON.pretty_generate(json_response)
576
+ print "\n"
577
+ else
578
+ print_green_success "New run started for image build #{image_build['name']}"
579
+ get([image_build['id']])
580
+ end
581
+ return 0
582
+ rescue RestClient::Exception => e
583
+ print_rest_exception(e, options)
584
+ return 1
585
+ end
586
+ end
587
+
588
+ def list_executions(args)
589
+ options = {}
590
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
591
+ opts.banner = subcommand_usage("[image-build]")
592
+ build_common_options(opts, options, [:list, :json, :dry_run])
593
+ opts.footer = "List executions for an image build."
594
+ opts.footer = "Display a list of executions for an image build.\n"
595
+ "[image-build] is the name or id of an image build."
596
+ end
597
+ optparse.parse!(args)
598
+ if args.count > 1
599
+ puts_error "#{Morpheus::Terminal.angry_prompt}wrong number of arguments. Expected 1 and received #{args.count} #{args.inspect}\n#{optparse}"
600
+ return 1
601
+ end
602
+ connect(options)
603
+
604
+ image_build = find_image_build_by_name_or_id(args[0])
605
+ return 1 if image_build.nil?
606
+
607
+
608
+ params = {}
609
+ [:phrase, :offset, :max, :sort, :direction].each do |k|
610
+ params[k] = options[k] unless options[k].nil?
611
+ end
612
+ if options[:dry_run]
613
+ print_dry_run @image_builds_interface.dry.list_executions(image_build['id'], params)
614
+ return 0
615
+ end
616
+ json_response = @image_builds_interface.list_executions(image_build['id'], params)
617
+ if options[:json]
618
+ puts JSON.pretty_generate(json_response)
619
+ return 0
620
+ end
621
+ image_build = json_response['imageBuild']
622
+ image_build_executions = json_response['imageBuildExecutions']
623
+ print_h1 "Image Build Executions: [#{image_build['id']}] #{image_build['name']}"
624
+ print cyan
625
+ if image_build_executions && image_build_executions.size > 0
626
+ print_image_build_executions_table(image_build_executions, opts={})
627
+ print_results_pagination(json_response, {:label => "execution", :n_label => "executions"})
628
+ end
629
+
630
+ return 0
631
+ end
632
+
633
+ def delete_execution(args)
634
+ puts "todo: implement me"
635
+ return 0
636
+ end
637
+
638
+ private
639
+
640
+ def get_available_image_build_types()
641
+ # todo: api call
642
+ [
643
+ {'name' => 'VMware', 'code' => 'vmware', 'instanceType' => {'code' => 'vmware'}}
644
+ ]
645
+ end
646
+
647
+ def get_available_image_build_types_dropdown(group=nil, cloud=nil)
648
+ get_available_image_build_types().collect {|it|
649
+ {'name' => it['name'], 'value' => it['code']}
650
+ }
651
+ end
652
+
653
+ def find_image_build_type(val)
654
+ if val.nil? || val.to_s.empty?
655
+ return nil
656
+ else
657
+ return get_available_image_build_types().find { |it|
658
+ (it['code'].to_s.downcase == val.to_s.downcase) ||
659
+ (it['name'].to_s.downcase == val.to_s.downcase)
660
+ }
661
+ end
662
+ end
663
+
664
+ def add_image_build_option_types(connected=true)
665
+ [
666
+ {'fieldName' => 'type', 'fieldLabel' => 'Type', 'type' => 'select', 'selectOptions' => get_available_image_build_types_dropdown(), 'required' => true, 'description' => 'Choose the type of image build.'},
667
+ {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true, 'description' => 'Enter a name for this image build.'},
668
+ {'fieldName' => 'description', 'fieldLabel' => 'Description', 'type' => 'text', 'required' => false},
669
+ {'fieldName' => 'group', 'fieldLabel' => 'Group', 'type' => 'select', 'selectOptions' => (connected ? get_available_groups() : []), 'required' => true},
670
+ #{'fieldName' => 'cloud', 'fieldLabel' => 'Cloud', 'type' => 'select', 'selectOptions' => [], 'required' => true},
671
+ # tons more, this isn't used anymore though..
672
+ ]
673
+ end
674
+
675
+ def update_image_build_option_types(connected=true)
676
+ list = add_image_build_option_types(connected)
677
+ # list = list.reject {|it| ["group"].include? it['fieldName'] }
678
+ list.each {|it| it['required'] = false }
679
+ list
680
+ end
681
+
682
+ def find_image_build_by_name_or_id(val)
683
+ if val.to_s =~ /\A\d{1,}\Z/
684
+ return find_image_build_by_id(val)
685
+ else
686
+ return find_image_build_by_name(val)
687
+ end
688
+ end
689
+
690
+ def find_image_build_by_id(id)
691
+ begin
692
+ json_response = @image_builds_interface.get(id.to_i)
693
+ return json_response['imageBuild']
694
+ rescue RestClient::Exception => e
695
+ if e.response && e.response.code == 404
696
+ print_red_alert "Image Build not found by id #{id}"
697
+ return nil
698
+ else
699
+ raise e
700
+ end
701
+ end
702
+ end
703
+
704
+ def find_image_build_by_name(name)
705
+ image_builds = @image_builds_interface.list({name: name.to_s})['imageBuilds']
706
+ if image_builds.empty?
707
+ print_red_alert "Image Build not found by name #{name}"
708
+ return nil
709
+ elsif image_builds.size > 1
710
+ print_red_alert "#{image_builds.size} image builds found by name #{name}"
711
+ # print_image_builds_table(image_builds, {color: red})
712
+ rows = image_builds.collect do |it|
713
+ {id: it['id'], name: it['name']}
714
+ end
715
+ print red
716
+ tp rows, [:id, :name]
717
+ print reset,"\n"
718
+ return nil
719
+ else
720
+ return image_builds[0]
721
+ end
722
+ end
723
+
724
+ # def find_group_by_name(name)
725
+ # group_results = @groups_interface.get(name)
726
+ # if group_results['groups'].empty?
727
+ # print_red_alert "Group not found by name #{name}"
728
+ # return nil
729
+ # end
730
+ # return group_results['groups'][0]
731
+ # end
732
+
733
+ # def find_cloud_by_name(group_id, name)
734
+ # option_results = @options_interface.options_for_source('clouds',{groupId: group_id})
735
+ # match = option_results['data'].find { |grp| grp['value'].to_s == name.to_s || grp['name'].downcase == name.downcase}
736
+ # if match.nil?
737
+ # print_red_alert "Cloud not found by name #{name}"
738
+ # return nil
739
+ # else
740
+ # return match['value']
741
+ # end
742
+ # end
743
+
744
+
745
+ def format_image_build_status(image_build, return_color=cyan)
746
+ out = ""
747
+ return out if !image_build
748
+ if image_build && image_build['lastResult']
749
+ out << format_image_build_execution_status(image_build['lastResult'])
750
+ else
751
+ out << ""
752
+ end
753
+ out
754
+ end
755
+
756
+ def format_image_build_execution_status(image_build_execution, return_color=cyan)
757
+ return "" if !image_build_execution
758
+ out = ""
759
+ status_string = image_build_execution['status']
760
+ if status_string == 'running'
761
+ out << "#{cyan}#{bold}#{status_string.upcase}#{reset}#{return_color}"
762
+ elsif status_string == 'success'
763
+ out << "#{green}#{status_string.upcase}#{return_color}"
764
+ elsif status_string == 'failed'
765
+ out << "#{red}#{status_string.upcase}#{return_color}"
766
+ elsif status_string == 'pending'
767
+ out << "#{cyan}#{status_string.upcase}#{return_color}"
768
+ elsif !status_string.to_s.empty?
769
+ out << "#{yellow}#{status_string.upcase}#{return_color}"
770
+ else
771
+ #out << "#{cyan}No Executions#{return_color}"
772
+ end
773
+ out
774
+ end
775
+
776
+ def format_image_build_execution_result(image_build_execution, return_color=cyan)
777
+ return "" if !image_build_execution
778
+ out = ""
779
+ status_string = image_build_execution['status']
780
+ if status_string == 'running' # || status_string == 'pending'
781
+ out << generate_usage_bar(image_build_execution['statusPercent'], 100, {max_bars: 10, bar_color: cyan})
782
+ out << return_color if return_color
783
+ end
784
+ if image_build_execution['tempInstance'].to_s != ''
785
+ out << " Instance:"
786
+ out << " [#{image_build_execution['tempInstance']['id']}] #{image_build_execution['tempInstance']['name']}"
787
+ out << " -"
788
+ end
789
+ if image_build_execution['statusMessage'].to_s != ''
790
+ out << " #{image_build_execution['statusMessage']}"
791
+ end
792
+ if image_build_execution['errorMessage'].to_s != ''
793
+ out << " #{red}#{image_build_execution['errorMessage']}#{return_color}"
794
+ end
795
+ if image_build_execution['virtualImages']
796
+ img_count = image_build_execution['virtualImages'].size
797
+ if img_count == 1
798
+ out << " Virtual Image:"
799
+ elsif img_count > 1
800
+ out << "(#{img_count}) Virtual Images:"
801
+ end
802
+ image_build_execution['virtualImages'].each do |virtual_image|
803
+ out << " [#{virtual_image['id']}] #{virtual_image['name']}#{return_color}"
804
+ end
805
+ end
806
+ out.strip #.squeeze(' ')
807
+ end
808
+
809
+ def print_image_build_executions_table(executions, opts={})
810
+ table_color = opts[:color] || cyan
811
+ rows = executions.collect do |execution|
812
+ {
813
+ id: execution['id'],
814
+ build: execution['buildNumber'],
815
+ createdBy: execution['createdBy'] ? execution['createdBy']['username'] : nil,
816
+ start: execution['startDate'] ? format_local_dt(execution['startDate']) : '',
817
+ end: execution['endDate'] ? format_local_dt(execution['endDate']) : '',
818
+ duration: format_duration(execution['startDate'], execution['endDate']),
819
+ status: format_image_build_execution_status(execution, table_color),
820
+ result: format_image_build_execution_result(execution, table_color)
821
+ }
822
+ end
823
+
824
+ term_width = current_terminal_width()
825
+ result_col_width = 60
826
+ if term_width > 250
827
+ result_col_width += 100
828
+ end
829
+ columns = [
830
+ #:id,
831
+ :build,
832
+ {:createdBy => {:display_name => "CREATED BY"} },
833
+ :start,
834
+ :end,
835
+ :duration,
836
+ {:status => {:display_name => "STATUS"} },
837
+ {:result => {:display_name => "RESULT", :max_width => result_col_width} }
838
+ ]
839
+ # # custom pretty table columns ...
840
+ # if options[:include_fields]
841
+ # columns = options[:include_fields]
842
+ # end
843
+ print table_color
844
+ print as_pretty_table(rows, columns, opts)
845
+ print reset
846
+ end
847
+
848
+ # def get_available_boot_scripts()
849
+ # boot_scripts_dropdown = []
850
+ # scripts = @boot_scripts_interface.list({max:1000})['bootScripts']
851
+ # scripts.each do |it|
852
+ # boot_scripts_dropdown << {'name'=>it['fileName'],'value'=>it['id']}
853
+ # end
854
+ # boot_scripts_dropdown << {'name'=>'Custom','value'=> 'custom'}
855
+ # return boot_scripts_dropdown
856
+ # end
857
+
858
+ def get_available_boot_scripts(refresh=false)
859
+ if !@available_boot_scripts || refresh
860
+ # option_results = options_interface.options_for_source('bootScripts',{})['data']
861
+ boot_scripts_dropdown = []
862
+ scripts = @boot_scripts_interface.list({max:1000})['bootScripts']
863
+ scripts.each do |it|
864
+ boot_scripts_dropdown << {'name'=>it['fileName'],'value'=>it['id'],'id'=>it['id']}
865
+ end
866
+ boot_scripts_dropdown << {'name'=>'Custom','value'=> 'custom','id'=> 'custom'}
867
+ @available_boot_scripts = boot_scripts_dropdown
868
+ end
869
+ #puts "available_boot_scripts() rtn: #{@available_boot_scripts.inspect}"
870
+ return @available_boot_scripts
871
+ end
872
+
873
+ def find_boot_script(val)
874
+ if val.nil? || val.to_s.empty?
875
+ return nil
876
+ else
877
+ return get_available_boot_scripts().find { |it|
878
+ (it['id'].to_s.downcase == val.to_s.downcase) ||
879
+ (it['name'].to_s.downcase == val.to_s.downcase)
880
+ }
881
+ end
882
+ end
883
+
884
+ def get_available_preseed_scripts(refresh=false)
885
+ if !@available_preseed_scripts || refresh
886
+ # option_results = options_interface.options_for_source('preseedScripts',{})['data']
887
+ preseed_scripts_dropdown = []
888
+ scripts = @preseed_scripts_interface.list({max:1000})['preseedScripts']
889
+ scripts.each do |it|
890
+ preseed_scripts_dropdown << {'name'=>it['fileName'],'value'=>it['id'],'id'=>it['id']}
891
+ end
892
+ # preseed_scripts_dropdown << {'name'=>'Custom','value'=> 'custom','value'=> 'custom'}
893
+ @available_preseed_scripts = preseed_scripts_dropdown
894
+ end
895
+ #puts "available_preseed_scripts() rtn: #{@available_preseed_scripts.inspect}"
896
+ return @available_preseed_scripts
897
+ end
898
+
899
+ def find_preseed_script(val)
900
+ if val.nil? || val.to_s.empty?
901
+ return nil
902
+ else
903
+ return get_available_preseed_scripts().find { |it|
904
+ (it['id'].to_s.downcase == val.to_s.downcase) ||
905
+ (it['name'].to_s.downcase == val.to_s.downcase)
906
+ }
907
+ end
908
+ end
909
+
910
+ def prompt_new_image_build(options={}, default_values={}, do_require=true)
911
+ payload = {}
912
+
913
+ # Summary / Settings Tab
914
+
915
+ # Image Build Type
916
+ image_build_type = nil
917
+ if options['type']
918
+ image_build_type = find_image_build_type(options['type'])
919
+ else
920
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'type', 'fieldLabel' => 'Type', 'type' => 'select', 'selectOptions' => get_available_image_build_types_dropdown(), 'required' => do_require, 'description' => 'Choose the type of image build.', 'defaultValue' => default_values['type'], :fmt=>:natural}], options, @api_client)
921
+ image_build_type = find_image_build_type(v_prompt['type'])
922
+ end
923
+ if !image_build_type
924
+ print_red_alert "Image Build Type not found!"
925
+ return false
926
+ end
927
+ payload['type'] = image_build_type['code'] # = {'id'=> image_build_type['id']}
928
+
929
+ # Name
930
+ if options['name']
931
+ payload['name'] = options['name']
932
+ else
933
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => do_require, 'description' => 'Enter a name for this image build.', 'defaultValue' => default_values['name'], :fmt=>:natural}], options, @api_client)
934
+ payload['name'] = v_prompt['name']
935
+ end
936
+
937
+ # Description
938
+ if options['description']
939
+ payload['description'] = options['description']
940
+ else
941
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'description', 'fieldLabel' => 'Description', 'type' => 'text', 'defaultValue' => default_values['description'], :fmt=>:natural}], options, @api_client)
942
+ payload['description'] = v_prompt['description']
943
+ end
944
+
945
+ # Group
946
+ group = nil
947
+ if options['group']
948
+ group = find_group_by_name_or_id_for_provisioning(options['group'])
949
+ else
950
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'group', 'type' => 'select', 'fieldLabel' => 'Group', 'selectOptions' => get_available_groups(), 'required' => do_require, 'description' => 'Select Group.', 'defaultValue' => default_values['group'], :fmt=>:natural}],options,@api_client,{})
951
+ group = find_group_by_name_or_id_for_provisioning(v_prompt['group'])
952
+ end
953
+ if !group
954
+ #print_red_alert "Group not found!"
955
+ return false
956
+ end
957
+ # pick one
958
+ #payload['group'] = {'id' => group['id']}
959
+ payload['site'] = {'id' => group['id']}
960
+
961
+ # Cloud
962
+ cloud = nil
963
+ if options['cloud']
964
+ cloud = find_cloud_by_name_or_id_for_provisioning(group['id'], options['cloud'])
965
+ else
966
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'cloud', 'type' => 'select', 'fieldLabel' => 'Cloud', 'selectOptions' => get_available_clouds(group['id']), 'required' => do_require, 'description' => 'Select Cloud.', 'defaultValue' => default_values['cloud'], :fmt=>:natural}],options,@api_client,{groupId: group['id']})
967
+ cloud = find_cloud_by_name_or_id_for_provisioning(group['id'], v_prompt['cloud'])
968
+ end
969
+ if !cloud
970
+ # print_red_alert "Cloud not found!"
971
+ return false
972
+ end
973
+ # pick one
974
+ #payload['cloud'] = {'id' => cloud['id']}
975
+ payload['zone'] = {'id' => cloud['id']}
976
+
977
+
978
+ # Configure Tab
979
+ # either pass --config, --configFile or be prompted..
980
+ if options['config']
981
+ payload['config'] = options['config']
982
+ elsif options['configFile']
983
+ config_file = File.expand_path(options['configFile'])
984
+ if !File.exists?(config_file) || !File.file?(config_file)
985
+ print_red_alert "File not found: #{config_file}"
986
+ return false
987
+ end
988
+ if config_file =~ /\.ya?ml\Z/
989
+ payload['config'] = YAML.load_file(config_file)
990
+ else
991
+ payload['config'] = JSON.parse(File.read(config_file))
992
+ end
993
+ elsif default_values['config']
994
+ # for now, skip this config prompting if updating existing record
995
+ # this is problematic, if they are changing the group or cloud though...
996
+ # payload['config'] = default_values['config']
997
+ else
998
+ # Instance Type is derived from the image build type
999
+ instance_type_code = image_build_type['instanceType']['code']
1000
+ instance_type = find_instance_type_by_code(instance_type_code)
1001
+ return false if !instance_type
1002
+
1003
+ instance_config_options = options.dup
1004
+ #instance_config_options[:no_prompt] = options[:no_prompt]
1005
+ # use active group by default
1006
+ instance_config_options[:group] = group['id']
1007
+ instance_config_options[:cloud] = cloud['id']
1008
+ instance_config_options[:instance_type_code] = instance_type["code"] # instance_type_code
1009
+ instance_config_options[:name_required] = false
1010
+ # this provisioning helper method handles all (most) of the parsing and prompting
1011
+ # puts "instance_config_options is: #{instance_config_options.inspect}"
1012
+ instance_config_payload = prompt_new_instance(instance_config_options)
1013
+ # strip all empty string and nil, would be problematic for update()
1014
+ instance_config_payload.deep_compact!
1015
+
1016
+ payload['config'] = instance_config_payload
1017
+ end
1018
+
1019
+ # merge group and cloud parameters into config..
1020
+ payload['config'] ||= {}
1021
+ payload['config']['zoneId'] = cloud['id']
1022
+ # payload['config']['siteId'] = group['id']
1023
+ payload['config']['instance'] ||= {}
1024
+ payload['config']['instance']['site'] = {'id' => group['id']}
1025
+
1026
+
1027
+ # Scripts tab
1028
+ boot_script = nil
1029
+ boot_script_id = nil
1030
+ boot_command = nil
1031
+ if options['bootScript']
1032
+ boot_script = find_boot_script(options['bootScript'])
1033
+ if !boot_script
1034
+ print_red_alert "Boot Script not found: #{options['bootScript']}"
1035
+ return false
1036
+ end
1037
+ boot_script_id = boot_script['id']
1038
+ else
1039
+ boot_script_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'bootScript', 'type' => 'select', 'fieldLabel' => 'Boot Script', 'selectOptions' => get_available_boot_scripts(), 'required' => do_require, 'description' => 'Select Boot Script.', 'defaultValue' => default_values['bootScript'], :fmt=>:natural}],options,api_client,{})
1040
+ # boot_script_id = boot_script_prompt['bootScript']
1041
+ boot_script = find_boot_script(boot_script_prompt['bootScript'])
1042
+ if !boot_script
1043
+ print_red_alert "Boot Script not found: '#{boot_script_prompt['bootScript']}'"
1044
+ return false
1045
+ end
1046
+ boot_script_id = boot_script['id']
1047
+ end
1048
+
1049
+ if boot_script_id == "custom" || options['bootCommand']
1050
+ if options['bootCommand']
1051
+ boot_command = options['bootCommand']
1052
+ else
1053
+ boot_command_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'bootCommand', 'type' => 'code-editor', 'fieldLabel' => 'Boot Command', 'required' => do_require, 'description' => 'Enter a boot command.', :fmt=>:natural}],options,api_client,{})
1054
+ boot_command = boot_command_prompt['bootCommand']
1055
+ end
1056
+ boot_script_id = nil
1057
+ else
1058
+ # boot_command = nil
1059
+ end
1060
+
1061
+ if boot_script_id
1062
+ # payload['bootScript'] = boot_script_id
1063
+ if boot_script_id == ""
1064
+ payload['bootScript'] = nil
1065
+ else
1066
+ payload['bootScript'] = {id: boot_script_id}
1067
+ end
1068
+ elsif boot_command
1069
+ payload['bootCommand'] = boot_command
1070
+ end
1071
+
1072
+ # Preseed Script
1073
+ preseed_script = nil
1074
+ preseed_script_id = nil
1075
+ if options['preseedScript']
1076
+ preseed_script = find_preseed_script(options['preseedScript'])
1077
+ if !preseed_script
1078
+ print_red_alert "Preseed Script not found: #{options['preseedScript']}"
1079
+ return false
1080
+ end
1081
+ preseed_script_id = preseed_script['id']
1082
+ else
1083
+ preseed_script_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'preseedScript', 'type' => 'select', 'fieldLabel' => 'Preseed Script', 'selectOptions' => get_available_preseed_scripts(), 'required' => false, 'description' => 'Select Preseed Script.', 'defaultValue' => default_values['preseedScript'], :fmt=>:natural}],options,api_client,{})
1084
+ # preseed_script_id = preseed_script_prompt['preseedScript']
1085
+ preseed_script = find_preseed_script(preseed_script_prompt['preseedScript'])
1086
+ if !preseed_script
1087
+ print_red_alert "Preseed Script not found: '#{preseed_script_prompt['preseedScript']}'"
1088
+ return false
1089
+ end
1090
+ preseed_script_id = preseed_script['id']
1091
+ end
1092
+ if preseed_script_id
1093
+ # payload['preseedScript'] = preseed_script_id
1094
+ if preseed_script_id == ""
1095
+ payload['preseedScript'] = nil
1096
+ else
1097
+ payload['preseedScript'] = {id: preseed_script_id}
1098
+ end
1099
+ end
1100
+
1101
+ # Additional Scripts
1102
+ if options['scripts']
1103
+ payload['scripts'] = options['scripts'] #.to_s.split(",").collect {|it| it.to_s.strip }.select {|it| it }.compact
1104
+ else
1105
+ scripts_default_value = default_values['scripts']
1106
+ if scripts_default_value.kind_of?(Array)
1107
+ scripts_default_value = scripts_default_value.collect {|it| it["name"] }.join(", ")
1108
+ end
1109
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'scripts', 'fieldLabel' => 'Additional Scripts', 'type' => 'text', 'description' => 'Additional Scripts (comma separated names or ids)', 'defaultValue' => scripts_default_value, :fmt=>:natural}], options, @api_client)
1110
+ payload['scripts'] = v_prompt['scripts'].to_s.split(",").collect {|it| it.to_s.strip }.select {|it| it }.compact
1111
+ end
1112
+
1113
+ # SSH Username
1114
+ if options['sshUsername']
1115
+ payload['sshUsername'] = options['sshUsername']
1116
+ else
1117
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'sshUsername', 'fieldLabel' => 'SSH Username', 'required' => do_require, 'type' => 'text', 'description' => 'SSH Username', 'defaultValue' => default_values['sshUsername'], :fmt=>:natural}], options, @api_client)
1118
+ payload['sshUsername'] = v_prompt['sshUsername']
1119
+ end
1120
+
1121
+ # SSH Password
1122
+ if options['sshPassword']
1123
+ payload['sshPassword'] = options['sshPassword']
1124
+ else
1125
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'sshPassword', 'fieldLabel' => 'SSH Password', 'required' => do_require, 'type' => 'password', 'description' => 'SSH Password', 'defaultValue' => default_values['sshPassword'], :fmt=>:natural}], options, @api_client)
1126
+ payload['sshPassword'] = v_prompt['sshPassword']
1127
+ end
1128
+
1129
+ # Storage Provider
1130
+ if options['storageProvider']
1131
+ payload['storageProvider'] = options['storageProvider']
1132
+ else
1133
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'storageProvider', 'fieldLabel' => 'Storage Provider', 'type' => 'select', 'optionSource' => 'storageProviders', 'description' => 'Storage Provider', 'defaultValue' => default_values['storageProvider'], :fmt=>:natural}], options, @api_client, {})
1134
+ payload['storageProvider'] = v_prompt['storageProvider']
1135
+ end
1136
+
1137
+ # Cloud Init
1138
+ if options['isCloudInit']
1139
+ payload['isCloudInit'] = options['isCloudInit']
1140
+ else
1141
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'isCloudInit', 'fieldLabel' => 'Cloud Init?', 'type' => 'checkbox', 'description' => 'Cloud Init', 'defaultValue' => (default_values['isCloudInit'].nil? ? false : default_values['isCloudInit']), :fmt=>:natural}], options, @api_client, {})
1142
+ payload['isCloudInit'] = v_prompt['isCloudInit']
1143
+ end
1144
+
1145
+ # Build Output Name
1146
+ if options['buildOutputName']
1147
+ payload['buildOutputName'] = options['buildOutputName']
1148
+ else
1149
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'buildOutputName', 'fieldLabel' => 'Build Output Name', 'type' => 'text', 'description' => 'Build Output Name', 'defaultValue' => default_values['buildOutputName'], :fmt=>:natural}], options, @api_client)
1150
+ payload['buildOutputName'] = v_prompt['buildOutputName']
1151
+ end
1152
+
1153
+ # Conversion Formats
1154
+ if options['conversionFormats']
1155
+ payload['conversionFormats'] = options['conversionFormats']
1156
+ else
1157
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'conversionFormats', 'fieldLabel' => 'Conversion Formats', 'type' => 'text', 'description' => 'Conversion Formats ie. ovf, qcow2, vhd', 'defaultValue' => default_values['conversionFormats'], :fmt=>:natural}], options, @api_client)
1158
+ payload['conversionFormats'] = v_prompt['conversionFormats']
1159
+ end
1160
+
1161
+ ## Retention
1162
+
1163
+ # Delete Old Builds?
1164
+ # Keep Results
1165
+ if options['keepResults']
1166
+ payload['keepResults'] = options['keepResults'].to_i
1167
+ else
1168
+ default_keep_results = default_values['keepResults'] ? default_values['keepResults'] : nil # 0
1169
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'keepResults', 'fieldLabel' => 'Keep Results', 'type' => 'text', 'description' => 'Keep only the most recent builds. Older executions will be deleted along with their associated Virtual Images. The value 0 disables this functionality.', 'defaultValue' => default_keep_results, :fmt=>:natural}], options, @api_client)
1170
+ payload['keepResults'] = v_prompt['keepResults'].to_i
1171
+ end
1172
+
1173
+ return payload
1174
+ end
1175
+
1176
+ def prompt_edit_image_build(image_build, options={}, do_require=false)
1177
+ # populate default prompt values with the existing image build
1178
+ default_values = image_build.dup # lazy, but works as long as GET matches POST api structure
1179
+ if image_build['type'].kind_of?(Hash)
1180
+ default_values['type'] = image_build['type']['code']
1181
+ end
1182
+ if image_build['group'].kind_of?(Hash)
1183
+ default_values['group'] = image_build['group']['name'] # ['id']
1184
+ elsif image_build['site'].kind_of?(Hash)
1185
+ default_values['group'] = image_build['site']['name'] # ['id']
1186
+ end
1187
+ if image_build['cloud'].kind_of?(Hash)
1188
+ default_values['cloud'] = image_build['cloud']['name']# ['id']
1189
+ elsif image_build['zone'].kind_of?(Hash)
1190
+ default_values['cloud'] = image_build['zone']['name'] # ['id']
1191
+ end
1192
+ if image_build['bootCommand'] && !image_build['bootScript']
1193
+ default_values['bootScript'] = 'custom'
1194
+ end
1195
+ if image_build['bootScript'].kind_of?(Hash)
1196
+ default_values['bootScript'] = image_build['bootScript']['fileName'] # ['id']
1197
+ end
1198
+ if image_build['preseedScript'].kind_of?(Hash)
1199
+ default_values['preseedScript'] = image_build['preseedScript']['fileName'] # ['id']
1200
+ end
1201
+ if image_build['storageProvider']
1202
+ default_values['storageProvider'] = image_build['storageProvider']['name'] # ['id']
1203
+ end
1204
+ # any other mismatches? preseedScript, bootScript?
1205
+ return prompt_new_image_build(options, default_values, do_require)
1206
+ end
1207
+
1208
+ end