morpheus-cli 3.5.2 → 3.5.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/lib/morpheus/api/api_client.rb +16 -0
  3. data/lib/morpheus/api/blueprints_interface.rb +84 -0
  4. data/lib/morpheus/api/execution_request_interface.rb +33 -0
  5. data/lib/morpheus/api/instances_interface.rb +21 -0
  6. data/lib/morpheus/api/packages_interface.rb +25 -5
  7. data/lib/morpheus/api/processes_interface.rb +34 -0
  8. data/lib/morpheus/api/roles_interface.rb +7 -0
  9. data/lib/morpheus/api/servers_interface.rb +8 -0
  10. data/lib/morpheus/api/user_settings_interface.rb +76 -0
  11. data/lib/morpheus/cli.rb +5 -1
  12. data/lib/morpheus/cli/alias_command.rb +1 -1
  13. data/lib/morpheus/cli/app_templates.rb +2 -1
  14. data/lib/morpheus/cli/apps.rb +173 -19
  15. data/lib/morpheus/cli/blueprints_command.rb +2134 -0
  16. data/lib/morpheus/cli/cli_command.rb +3 -1
  17. data/lib/morpheus/cli/clouds.rb +4 -10
  18. data/lib/morpheus/cli/coloring_command.rb +14 -8
  19. data/lib/morpheus/cli/containers_command.rb +92 -5
  20. data/lib/morpheus/cli/execution_request_command.rb +313 -0
  21. data/lib/morpheus/cli/hosts.rb +188 -7
  22. data/lib/morpheus/cli/instances.rb +472 -9
  23. data/lib/morpheus/cli/login.rb +1 -1
  24. data/lib/morpheus/cli/mixins/print_helper.rb +8 -0
  25. data/lib/morpheus/cli/mixins/processes_helper.rb +134 -0
  26. data/lib/morpheus/cli/option_types.rb +21 -16
  27. data/lib/morpheus/cli/packages_command.rb +469 -17
  28. data/lib/morpheus/cli/processes_command.rb +313 -0
  29. data/lib/morpheus/cli/remote.rb +20 -9
  30. data/lib/morpheus/cli/roles.rb +186 -6
  31. data/lib/morpheus/cli/shell.rb +10 -1
  32. data/lib/morpheus/cli/tasks.rb +4 -1
  33. data/lib/morpheus/cli/user_settings_command.rb +431 -0
  34. data/lib/morpheus/cli/version.rb +1 -1
  35. data/lib/morpheus/cli/whoami.rb +1 -1
  36. data/lib/morpheus/formatters.rb +14 -0
  37. data/lib/morpheus/morpkg.rb +119 -0
  38. data/morpheus-cli.gemspec +1 -0
  39. metadata +26 -2
@@ -0,0 +1,2134 @@
1
+ require 'morpheus/cli/cli_command'
2
+ require 'morpheus/cli/mixins/provisioning_helper'
3
+
4
+ class Morpheus::Cli::BlueprintsCommand
5
+ include Morpheus::Cli::CliCommand
6
+ include Morpheus::Cli::ProvisioningHelper
7
+ set_command_name :'blueprints'
8
+ register_subcommands :list, :get, :add, :update, :remove
9
+ register_subcommands :duplicate
10
+ register_subcommands :'upload-image' => :upload_image
11
+ register_subcommands :'update-permissions' => :update_permissions
12
+ register_subcommands :'available-tiers'
13
+ register_subcommands :'add-tier', :'update-tier', :'remove-tier', :'connect-tiers', :'disconnect-tiers'
14
+ register_subcommands :'add-instance'
15
+ #register_subcommands :'update-instance'
16
+ register_subcommands :'remove-instance'
17
+ register_subcommands :'add-instance-config'
18
+ #register_subcommands :'update-instance-config'
19
+ register_subcommands :'remove-instance-config'
20
+ # alias_subcommand :details, :get
21
+ # set_default_subcommand :list
22
+
23
+ def initialize()
24
+ # @appliance_name, @appliance_url = Morpheus::Cli::Remote.active_appliance
25
+ end
26
+
27
+ def connect(opts)
28
+ @api_client = establish_remote_appliance_connection(opts)
29
+ @blueprints_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).blueprints
30
+ @groups_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).groups
31
+ @instances_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).instances
32
+ @instance_types_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).instance_types
33
+ @options_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).options
34
+ @active_group_id = Morpheus::Cli::Groups.active_groups[@appliance_name]
35
+ end
36
+
37
+ def handle(args)
38
+ handle_subcommand(args)
39
+ end
40
+
41
+ def list(args)
42
+ options = {}
43
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
44
+ opts.banner = subcommand_usage()
45
+ build_common_options(opts, options, [:list, :json, :yaml, :csv, :fields, :dry_run, :remote])
46
+ opts.footer = "List blueprints."
47
+ end
48
+ optparse.parse!(args)
49
+ connect(options)
50
+ begin
51
+ params = {}
52
+ [:phrase, :offset, :max, :sort, :direction].each do |k|
53
+ params[k] = options[k] unless options[k].nil?
54
+ end
55
+ if options[:dry_run]
56
+ print_dry_run @blueprints_interface.dry.list(params)
57
+ return
58
+ end
59
+
60
+ json_response = @blueprints_interface.list(params)
61
+ blueprints = json_response['blueprints']
62
+
63
+ if options[:json]
64
+ puts as_json(json_response, options, "blueprints")
65
+ return 0
66
+ elsif options[:csv]
67
+ puts records_as_csv(json_response['blueprints'], options)
68
+ return 0
69
+ elsif options[:yaml]
70
+ puts as_yaml(json_response, options, "blueprints")
71
+ return 0
72
+ end
73
+
74
+
75
+ title = "Morpheus Blueprints"
76
+ subtitles = []
77
+ if params[:phrase]
78
+ subtitles << "Search: #{params[:phrase]}".strip
79
+ end
80
+ print_h1 title, subtitles
81
+ if blueprints.empty?
82
+ print cyan,"No blueprints found.",reset,"\n"
83
+ else
84
+ print_blueprints_table(blueprints, options)
85
+ print_results_pagination(json_response)
86
+ end
87
+ print reset,"\n"
88
+ return 0
89
+
90
+ rescue RestClient::Exception => e
91
+ print_rest_exception(e, options)
92
+ exit 1
93
+ end
94
+ end
95
+
96
+ def get(args)
97
+ options = {}
98
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
99
+ opts.banner = subcommand_usage("[id]")
100
+ opts.on( '-c', '--config', "Display raw config only. Default is YAML. Combine with -j for JSON instead." ) do
101
+ options[:show_config] = true
102
+ end
103
+ build_common_options(opts, options, [:json, :yaml, :csv, :fields, :dry_run, :remote])
104
+ opts.footer = "Get details about a blueprint.\n" +
105
+ "[id] is required. This is the name or id of a blueprint."
106
+ end
107
+ optparse.parse!(args)
108
+ if args.count < 1
109
+ puts optparse
110
+ exit 1
111
+ end
112
+ connect(options)
113
+ begin
114
+ if options[:dry_run]
115
+ if args[0].to_s =~ /\A\d{1,}\Z/
116
+ print_dry_run @blueprints_interface.dry.get(args[0].to_i)
117
+ else
118
+ print_dry_run @blueprints_interface.dry.list({name:args[0]})
119
+ end
120
+ return
121
+ end
122
+ blueprint = find_blueprint_by_name_or_id(args[0])
123
+ exit 1 if blueprint.nil?
124
+
125
+ json_response = {'blueprint' => blueprint} # skip redundant request
126
+ #json_response = @blueprints_interface.get(blueprint['id'])
127
+ blueprint = json_response['blueprint']
128
+
129
+ if options[:show_config]
130
+ #print_h2 "RAW"
131
+ if options[:json]
132
+ print cyan
133
+ print "// JSON config for Morpheus Blueprint: #{blueprint['name']}","\n"
134
+ print reset
135
+ puts as_json(blueprint["config"])
136
+ else
137
+ print cyan
138
+ print "# YAML config for Morpheus Blueprint: #{blueprint['name']}","\n"
139
+ print reset
140
+ puts as_yaml(blueprint["config"])
141
+ end
142
+ return 0
143
+ end
144
+
145
+ if options[:json]
146
+ puts as_json(json_response, options, "blueprint")
147
+ return 0
148
+ elsif options[:yaml]
149
+ puts as_yaml(json_response, options, "blueprint")
150
+ return 0
151
+ elsif options[:csv]
152
+ puts records_as_csv([json_response['blueprint']], options)
153
+ return 0
154
+ end
155
+
156
+ print_h1 "Blueprint Details"
157
+
158
+ print_blueprint_details(blueprint)
159
+
160
+ if blueprint['resourcePermission'].nil?
161
+ print "\n", "No group access found", "\n"
162
+ else
163
+ print_h2 "Group Access"
164
+ rows = []
165
+ if blueprint['resourcePermission']['allSites'] || blueprint['resourcePermission']['all']
166
+ rows.push({"name" => 'All'})
167
+ end
168
+ if blueprint['resourcePermission']['sites']
169
+ blueprint['resourcePermission']['sites'].each do |site|
170
+ rows.push(site)
171
+ end
172
+ end
173
+ rows = rows.collect do |site|
174
+ {group: site['name'], default: site['default'] ? 'Yes' : ''}
175
+ end
176
+ # columns = [:group, :default]
177
+ columns = [:group]
178
+ print cyan
179
+ print as_pretty_table(rows, columns)
180
+ end
181
+
182
+ print reset,"\n"
183
+
184
+ rescue RestClient::Exception => e
185
+ print_rest_exception(e, options)
186
+ exit 1
187
+ end
188
+ end
189
+
190
+ def add(args)
191
+ options = {}
192
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
193
+ opts.banner = subcommand_usage("[name] [options]")
194
+ opts.on('--config JSON', String, "Blueprint Config JSON") do |val|
195
+ options['config'] = JSON.parse(val.to_s)
196
+ end
197
+ opts.on('--config-yaml YAML', String, "Blueprint Config YAML") do |val|
198
+ options['config'] = YAML.load(val.to_s)
199
+ end
200
+ opts.on('--config-file FILE', String, "Blueprint Config from a local JSON or YAML file") do |val|
201
+ options['configFile'] = val.to_s
202
+ end
203
+ build_option_type_options(opts, options, add_blueprint_option_types(false))
204
+ build_common_options(opts, options, [:options, :json, :dry_run, :remote])
205
+ opts.footer = "Create a new blueprint.\n" +
206
+ "[name] is optional and can be passed as --name or inside the config instead."
207
+ "[--config] or [--config-file] can be used to define the blueprint."
208
+ end
209
+ optparse.parse!(args)
210
+ if args.count > 1
211
+ print_error Morpheus::Terminal.angry_prompt
212
+ puts_error "#{command_name} add expects 0-1 arguments and received #{args.count}: #{args.join(' ')}\n#{optparse}"
213
+ return 1
214
+ end
215
+ options[:options] ||= {}
216
+ if args[0] && !options[:options]['name']
217
+ options[:options]['name'] = args[0]
218
+ end
219
+ connect(options)
220
+ begin
221
+ request_payload = nil
222
+ config_payload = {}
223
+ if options['config']
224
+ config_payload = options['config']
225
+ request_payload = config_payload
226
+ elsif options['configFile']
227
+ config_file = File.expand_path(options['configFile'])
228
+ if !File.exists?(config_file) || !File.file?(config_file)
229
+ print_red_alert "File not found: #{config_file}"
230
+ return false
231
+ end
232
+ if config_file =~ /\.ya?ml\Z/
233
+ config_payload = YAML.load_file(config_file)
234
+ else
235
+ config_payload = JSON.parse(File.read(config_file))
236
+ end
237
+ request_payload = config_payload
238
+ else
239
+ params = Morpheus::Cli::OptionTypes.prompt(add_blueprint_option_types, options[:options], @api_client, options[:params])
240
+ blueprint_payload = params.select {|k,v| ['name', 'description', 'category'].include?(k) }
241
+ # expects no namespace, just the config
242
+ request_payload = blueprint_payload
243
+ end
244
+
245
+ if options[:dry_run]
246
+ print_dry_run @blueprints_interface.dry.create(request_payload)
247
+ return
248
+ end
249
+
250
+ json_response = @blueprints_interface.create(request_payload)
251
+
252
+ if options[:json]
253
+ print JSON.pretty_generate(json_response)
254
+ print "\n"
255
+ else
256
+ blueprint = json_response["blueprint"]
257
+ print_green_success "Added blueprint #{blueprint['name']}"
258
+ if !options[:no_prompt]
259
+ if ::Morpheus::Cli::OptionTypes::confirm("Would you like to add a tier now?", options.merge({default: false}))
260
+ add_tier([blueprint['id']])
261
+ while ::Morpheus::Cli::OptionTypes::confirm("Add another tier?", options.merge({default: false})) do
262
+ add_tier([blueprint['id']])
263
+ end
264
+ else
265
+ # print details
266
+ get([blueprint['id']])
267
+ end
268
+ end
269
+ end
270
+
271
+ rescue RestClient::Exception => e
272
+ print_rest_exception(e, options)
273
+ exit 1
274
+ end
275
+ end
276
+
277
+ def update(args)
278
+ options = {}
279
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
280
+ opts.banner = subcommand_usage("[id] [options]")
281
+ opts.on('--config JSON', String, "Blueprint Config JSON") do |val|
282
+ options['config'] = JSON.parse(val.to_s)
283
+ end
284
+ opts.on('--config-yaml YAML', String, "Blueprint Config YAML") do |val|
285
+ options['config'] = YAML.load(val.to_s)
286
+ end
287
+ opts.on('--config-file FILE', String, "Blueprint Config from a local JSON or YAML file") do |val|
288
+ options['configFile'] = val.to_s
289
+ end
290
+ build_option_type_options(opts, options, update_blueprint_option_types(false))
291
+ build_common_options(opts, options, [:options, :json, :dry_run, :quiet, :remote])
292
+ opts.footer = "Update a blueprint.\n" +
293
+ "[id] is required. This is the name or id of a blueprint.\n" +
294
+ "[options] Available options include --name and --description. This will update only the specified values.\n" +
295
+ "[--config] or [--config-file] can be used to replace the entire blueprint."
296
+ end
297
+ optparse.parse!(args)
298
+
299
+ if args.count < 1
300
+ puts optparse
301
+ exit 1
302
+ end
303
+
304
+ connect(options)
305
+
306
+ begin
307
+
308
+ blueprint = find_blueprint_by_name_or_id(args[0])
309
+ exit 1 if blueprint.nil?
310
+
311
+ request_payload = nil
312
+ config_payload = {}
313
+ if options['config']
314
+ config_payload = options['config']
315
+ request_payload = config_payload
316
+ elsif options['configFile']
317
+ config_file = options['configFile']
318
+ if !File.exists?(config_file)
319
+ print_red_alert "File not found: #{config_file}"
320
+ return false
321
+ end
322
+ if config_file =~ /\.ya?ml\Z/
323
+ config_payload = YAML.load_file(config_file)
324
+ else
325
+ config_payload = JSON.parse(File.read(config_file))
326
+ end
327
+ request_payload = config_payload
328
+ else
329
+ # update just name,description,category
330
+ # preserve all other attributes of the config..
331
+
332
+ #params = Morpheus::Cli::OptionTypes.prompt(update_blueprint_option_types, options[:options], @api_client, options[:params])
333
+ params = options[:options] || {}
334
+
335
+ if params.empty?
336
+ # print_red_alert "Specify atleast one option to update"
337
+ print_red_alert "Specify atleast one option to update.\nOr use --config or --config-file to replace the entire config."
338
+ puts optparse
339
+ exit 1
340
+ end
341
+
342
+ #puts "parsed params is : #{params.inspect}"
343
+ blueprint_payload = params.select {|k,v| ['name','description','category'].include?(k) }
344
+ # expects no namespace, just the config
345
+ # preserve all other attributes of the config.
346
+ request_payload = blueprint["config"].merge(blueprint_payload)
347
+ # todo maybe: put name, description and category at the front.
348
+ # request_payload = blueprint_payload.merge(blueprint["config"].merge(blueprint_payload))
349
+ end
350
+
351
+ if options[:dry_run]
352
+ print_dry_run @blueprints_interface.dry.update(blueprint['id'], request_payload)
353
+ return
354
+ end
355
+
356
+ json_response = @blueprints_interface.update(blueprint['id'], request_payload)
357
+
358
+ if options[:json]
359
+ print JSON.pretty_generate(json_response)
360
+ print "\n"
361
+ else
362
+ unless options[:quiet]
363
+ blueprint = json_response['blueprint']
364
+ print_green_success "Updated blueprint #{blueprint['name']}"
365
+ details_options = [blueprint['id']]
366
+ get(details_options)
367
+ end
368
+ end
369
+
370
+ rescue RestClient::Exception => e
371
+ print_rest_exception(e, options)
372
+ exit 1
373
+ end
374
+ end
375
+
376
+ def update_permissions(args)
377
+ group_access_all = nil
378
+ group_access_list = nil
379
+ group_defaults_list = nil
380
+ options = {}
381
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
382
+ opts.banner = subcommand_usage("[id] [options]")
383
+ opts.on('--group-access-all [on|off]', String, "Toggle Access for all groups.") do |val|
384
+ group_access_all = val.to_s == 'on' || val.to_s == 'true' || val == '' || val.nil?
385
+ end
386
+ opts.on('--group-access LIST', Array, "Group Access, comma separated list of group IDs.") do |list|
387
+ if list.size == 1 && list[0] == 'null' # hacky way to clear it
388
+ group_access_list = []
389
+ else
390
+ group_access_list = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
391
+ end
392
+ end
393
+ opts.on('--visibility [private|public]', String, "Visibility") do |val|
394
+ options['visibility'] = val
395
+ end
396
+ build_option_type_options(opts, options, update_blueprint_option_types(false))
397
+ build_common_options(opts, options, [:options, :payload, :json, :dry_run, :quiet, :remote])
398
+ opts.footer = "Update a blueprint permissions.\n" +
399
+ "[id] is required. This is the name or id of a blueprint."
400
+ end
401
+ optparse.parse!(args)
402
+
403
+ if args.count != 1
404
+ puts optparse
405
+ return 1
406
+ end
407
+
408
+ connect(options)
409
+
410
+ begin
411
+
412
+ blueprint = find_blueprint_by_name_or_id(args[0])
413
+ return 1 if blueprint.nil?
414
+
415
+ payload = nil
416
+ if options[:payload]
417
+ payload = options[:payload]
418
+ else
419
+ payload = {
420
+ 'blueprint' => {
421
+ }
422
+ }
423
+
424
+ # allow arbitrary -O options
425
+ payload['blueprint'].deep_merge!(options[:options].reject {|k,v| k.is_a?(Symbol) }) if options[:options]
426
+
427
+ # Group Access
428
+ if group_access_all != nil
429
+ payload['resourcePermissions'] ||= {}
430
+ payload['resourcePermissions']['all'] = group_access_all
431
+ end
432
+ if group_access_list != nil
433
+ payload['resourcePermissions'] ||= {}
434
+ payload['resourcePermissions']['sites'] = group_access_list.collect do |site_id|
435
+ site = {"id" => site_id.to_i}
436
+ if group_defaults_list && group_defaults_list.include?(site_id)
437
+ site["default"] = true
438
+ end
439
+ site
440
+ end
441
+ end
442
+
443
+ # Tenants
444
+ # if options['tenants']
445
+ # payload['tenantPermissions'] = {}
446
+ # payload['tenantPermissions']['accounts'] = options['tenants']
447
+ # end
448
+
449
+ # Visibility
450
+ if options['visibility'] != nil
451
+ payload['blueprint']['visibility'] = options['visibility']
452
+ end
453
+ end
454
+
455
+ if options[:dry_run]
456
+ print_dry_run @blueprints_interface.dry.update_permissions(blueprint['id'], payload)
457
+ return
458
+ end
459
+
460
+ json_response = @blueprints_interface.update_permissions(blueprint['id'], payload)
461
+
462
+ if options[:json]
463
+ puts JSON.pretty_generate(json_response)
464
+ else
465
+ unless options[:quiet]
466
+ blueprint = json_response['blueprint']
467
+ print_green_success "Updated permissions for blueprint #{blueprint['name']}"
468
+ details_options = [blueprint['id']]
469
+ get(details_options)
470
+ end
471
+ end
472
+ return 0
473
+ rescue RestClient::Exception => e
474
+ print_rest_exception(e, options)
475
+ exit 1
476
+ end
477
+ end
478
+
479
+
480
+ def upload_image(args)
481
+ image_type_name = nil
482
+ options = {}
483
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
484
+ opts.banner = subcommand_usage("[id] [file]")
485
+ build_common_options(opts, options, [:json, :dry_run, :quiet, :remote])
486
+ opts.footer = "Upload an image file to be used as the icon for a blueprint.\n" +
487
+ "[id] is required. This is the name or id of a blueprint.\n" +
488
+ "[file] is required. This is the local path of a file to upload [png|jpg|svg]."
489
+ end
490
+ optparse.parse!(args)
491
+ if args.count != 2
492
+ print_error Morpheus::Terminal.angry_prompt
493
+ puts_error "#{command_name} upload-image expects 2 arguments and received #{args.count}: #{args.join(' ')}\n#{optparse}"
494
+ return 1
495
+ end
496
+ blueprint_name = args[0]
497
+ filename = File.expand_path(args[1].to_s)
498
+ image_file = nil
499
+ if filename && File.file?(filename)
500
+ # maybe validate it's an image file? [.png|jpg|svg]
501
+ image_file = File.new(filename, 'rb')
502
+ else
503
+ print_red_alert "File not found: #{filename}"
504
+ # print_error Morpheus::Terminal.angry_prompt
505
+ # puts_error "bad argument [file] - File not found: #{filename}\n#{optparse}"
506
+ return 1
507
+ end
508
+ connect(options)
509
+ begin
510
+ blueprint = find_blueprint_by_name_or_id(blueprint_name)
511
+ exit 1 if blueprint.nil?
512
+ if options[:dry_run]
513
+ print_dry_run @blueprints_interface.dry.save_image(blueprint['id'], image_file)
514
+ return 0
515
+ end
516
+ unless options[:quiet] || options[:json]
517
+ print cyan, "Uploading file #{filename} ...", reset, "\n"
518
+ end
519
+ json_response = @blueprints_interface.save_image(blueprint['id'], image_file)
520
+ if options[:json]
521
+ print JSON.pretty_generate(json_response)
522
+ elsif !options[:quiet]
523
+ blueprint = json_response['blueprint']
524
+ new_image_url = blueprint['image']
525
+ print cyan, "Updated blueprint #{blueprint['name']} image.\nNew image url is: #{new_image_url}", reset, "\n\n"
526
+ get([blueprint['id']])
527
+ end
528
+ return 0
529
+ rescue RestClient::Exception => e
530
+ print_rest_exception(e, options)
531
+ return 1
532
+ end
533
+ end
534
+
535
+ def duplicate(args)
536
+ options = {}
537
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
538
+ opts.banner = subcommand_usage("[id] [new name]")
539
+ build_common_options(opts, options, [:auto_confirm, :json, :dry_run, :remote])
540
+ opts.footer = "Duplicate a blueprint." + "\n" +
541
+ "[id] is required. This is the name or id of a blueprint." + "\n" +
542
+ "[new name] is required. This is the name for the clone."
543
+ end
544
+ optparse.parse!(args)
545
+
546
+ if args.count < 1
547
+ puts optparse
548
+ exit 1
549
+ end
550
+
551
+ request_payload = {"blueprint" => {}}
552
+ if args[1]
553
+ request_payload["blueprint"]["name"] = args[1]
554
+ end
555
+
556
+ connect(options)
557
+ begin
558
+ blueprint = find_blueprint_by_name_or_id(args[0])
559
+ exit 1 if blueprint.nil?
560
+ # unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to duplicate the blueprint #{blueprint['name']}?")
561
+ # exit
562
+ # end
563
+ if options[:dry_run]
564
+ print_dry_run @blueprints_interface.dry.duplicate(blueprint['id'], request_payload)
565
+ return
566
+ end
567
+ json_response = @blueprints_interface.duplicate(blueprint['id'], request_payload)
568
+
569
+ if options[:json]
570
+ print JSON.pretty_generate(json_response)
571
+ print "\n"
572
+ else
573
+ new_blueprint = json_response["blueprint"] || {}
574
+ print_green_success "Created duplicate blueprint '#{new_blueprint['name']}'"
575
+ #get([new_blueprint["id"]])
576
+ end
577
+
578
+ rescue RestClient::Exception => e
579
+ print_rest_exception(e, options)
580
+ exit 1
581
+ end
582
+ end
583
+
584
+ def remove(args)
585
+ options = {}
586
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
587
+ opts.banner = subcommand_usage("[id]")
588
+ build_common_options(opts, options, [:auto_confirm, :json, :dry_run, :remote])
589
+ opts.footer = "Delete a blueprint." + "\n" +
590
+ "[id] is required. This is the name or id of a blueprint."
591
+ end
592
+ optparse.parse!(args)
593
+
594
+ if args.count < 1
595
+ puts optparse
596
+ exit 1
597
+ end
598
+
599
+ connect(options)
600
+ begin
601
+ blueprint = find_blueprint_by_name_or_id(args[0])
602
+ exit 1 if blueprint.nil?
603
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete the blueprint #{blueprint['name']}?")
604
+ exit
605
+ end
606
+ if options[:dry_run]
607
+ print_dry_run @blueprints_interface.dry.destroy(blueprint['id'])
608
+ return
609
+ end
610
+ json_response = @blueprints_interface.destroy(blueprint['id'])
611
+
612
+ if options[:json]
613
+ print JSON.pretty_generate(json_response)
614
+ print "\n"
615
+ else
616
+ print_green_success "Removed blueprint #{blueprint['name']}"
617
+ end
618
+
619
+ rescue RestClient::Exception => e
620
+ print_rest_exception(e, options)
621
+ exit 1
622
+ end
623
+ end
624
+
625
+ def add_instance(args)
626
+ options = {}
627
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
628
+ opts.banner = subcommand_usage("[id] [tier] [instance-type]")
629
+ # opts.on( '-g', '--group GROUP', "Group" ) do |val|
630
+ # options[:group] = val
631
+ # end
632
+ # opts.on( '-c', '--cloud CLOUD', "Cloud" ) do |val|
633
+ # options[:cloud] = val
634
+ # end
635
+ opts.on('--name VALUE', String, "Instance Name") do |val|
636
+ options[:instance_name] = val
637
+ end
638
+ build_common_options(opts, options, [:options, :json, :dry_run, :remote])
639
+ opts.footer = "Update a blueprint, adding an instance." + "\n" +
640
+ "[id] is required. This is the name or id of a blueprint." + "\n" +
641
+ "[tier] is required and will be prompted for. This is the name of the tier." + "\n" +
642
+ "[instance-type] is required and will be prompted for. This is the type of instance."
643
+ end
644
+ optparse.parse!(args)
645
+
646
+ if args.count < 1
647
+ print_error Morpheus::Terminal.angry_prompt
648
+ puts_error "#{command_name} add-instance expects 3 arguments and received #{args.count}: #{args.join(' ')}\n#{optparse}"
649
+ return 1
650
+ end
651
+
652
+ connect(options)
653
+
654
+ begin
655
+ blueprint_name = args[0]
656
+ tier_name = args[1]
657
+ instance_type_code = args[2]
658
+ # we also need consider when there is multiple instances of the same type in
659
+ # a template/tier.. so maybe split instance_type_code as [type-code]:[index].. or...errr
660
+
661
+ blueprint = find_blueprint_by_name_or_id(blueprint_name)
662
+ return 1 if blueprint.nil?
663
+
664
+ if !tier_name
665
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'tierName', 'fieldLabel' => 'Tier Name', 'type' => 'text', 'required' => true, 'description' => 'Enter the name of the tier'}], options[:options])
666
+ tier_name = v_prompt['tierName']
667
+ end
668
+
669
+ if !instance_type_code
670
+ instance_type_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'type', 'type' => 'select', 'fieldLabel' => 'Type', 'optionSource' => 'instanceTypes', 'required' => true, 'description' => 'Select Instance Type.'}],options[:options],api_client,{})
671
+ instance_type_code = instance_type_prompt['type']
672
+ end
673
+ instance_type = find_instance_type_by_code(instance_type_code)
674
+ return 1 if instance_type.nil?
675
+
676
+ tier_config = nil
677
+ instance_config = nil
678
+
679
+ blueprint["config"] ||= {}
680
+ tiers = blueprint["config"]["tiers"]
681
+ tiers ||= {}
682
+ # tier identified by name, case sensitive...
683
+ if !tiers[tier_name]
684
+ tiers[tier_name] = {}
685
+ end
686
+ tier_config = tiers[tier_name]
687
+
688
+ tier_config['instances'] ||= []
689
+ instance_config = tier_config['instances'].find {|it| it["instance"] && it["instance"]["type"] && it["instance"]["type"] == instance_type["code"] }
690
+ if !instance_config
691
+ instance_config = {'instance' => {'type' => instance_type['code']} }
692
+ tier_config['instances'].push(instance_config)
693
+ end
694
+ instance_config['instance'] ||= {}
695
+
696
+ # just prompts for Instance Name (optional)
697
+ instance_name = nil
698
+ if options[:instance_name]
699
+ instance_name = options[:instance_name]
700
+ else
701
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'name', 'fieldLabel' => 'Instance Name', 'type' => 'text', 'defaultValue' => instance_config['instance']['name']}])
702
+ instance_name = v_prompt['name'] || ''
703
+ end
704
+
705
+ if instance_name
706
+ if instance_name.to_s == 'null'
707
+ instance_config['instance'].delete('name')
708
+ # instance_config['instance']['name'] = ''
709
+ else
710
+ instance_config['instance']['name'] = instance_name
711
+ end
712
+ end
713
+
714
+ # ok, make api request
715
+ blueprint["config"]["tiers"] = tiers
716
+ request_payload = {blueprint: blueprint}
717
+
718
+ if options[:dry_run]
719
+ print_dry_run @blueprints_interface.dry.update(blueprint['id'], request_payload)
720
+ return 0
721
+ end
722
+
723
+ json_response = @blueprints_interface.update(blueprint['id'], request_payload)
724
+
725
+ if options[:json]
726
+ puts JSON.pretty_generate(json_response)
727
+ elsif !options[:quiet]
728
+ print_green_success "Instance added to blueprint #{blueprint['name']}"
729
+ # prompt for new instance
730
+ if !options[:no_prompt]
731
+ if ::Morpheus::Cli::OptionTypes::confirm("Would you like to add a config now?", options.merge({default: true}))
732
+ # todo: this needs to work by index, because you can have multiple instances of the same type
733
+ add_instance_config([blueprint['id'], tier_name, instance_type['code']])
734
+ while ::Morpheus::Cli::OptionTypes::confirm("Add another config?", options.merge({default: false})) do
735
+ add_instance_config([blueprint['id'], tier_name, instance_type['code']])
736
+ end
737
+ else
738
+ # print details
739
+ get([blueprint['name']])
740
+ end
741
+ end
742
+ end
743
+ return 0
744
+
745
+ rescue RestClient::Exception => e
746
+ print_rest_exception(e, options)
747
+ return 1
748
+ end
749
+
750
+ end
751
+
752
+ def add_instance_config(args)
753
+ options = {}
754
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
755
+ opts.banner = subcommand_usage("[id] [tier] [instance]")
756
+ opts.on( '-g', '--group GROUP', "Group" ) do |val|
757
+ options[:group] = val
758
+ end
759
+ opts.on( '-c', '--cloud CLOUD', "Cloud" ) do |val|
760
+ options[:cloud] = val
761
+ end
762
+ opts.on( '-e', '--env ENVIRONMENT', "Environment" ) do |val|
763
+ options[:environment] = val
764
+ end
765
+ opts.on('--name VALUE', String, "Instance Name") do |val|
766
+ options[:instance_name] = val
767
+ end
768
+ build_common_options(opts, options, [:options, :json, :dry_run, :remote])
769
+ opts.footer = "Update a blueprint, adding an instance config." + "\n" +
770
+ "[id] is required. This is the name or id of a blueprint." + "\n" +
771
+ "[tier] is required. This is the name of the tier." + "\n" +
772
+ "[instance] is required. This is the type of instance."
773
+ end
774
+ optparse.parse!(args)
775
+
776
+ if args.count < 3
777
+ print_error Morpheus::Terminal.angry_prompt
778
+ puts_error "Wrong number of arguments"
779
+ puts_error optparse
780
+ return 1
781
+ end
782
+
783
+ connect(options)
784
+
785
+ begin
786
+
787
+ blueprint_name = args[0]
788
+ tier_name = args[1]
789
+ instance_type_code = args[2]
790
+ # we also need consider when there is multiple instances of the same type in
791
+ # a template/tier.. so maybe split instance_type_code as [type-code]:[index].. or...errr
792
+
793
+ blueprint = find_blueprint_by_name_or_id(blueprint_name)
794
+ return 1 if blueprint.nil?
795
+
796
+ instance_type = find_instance_type_by_code(instance_type_code)
797
+ return 1 if instance_type.nil?
798
+
799
+ tier_config = nil
800
+ instance_config = nil
801
+
802
+ blueprint["config"] ||= {}
803
+ tiers = blueprint["config"]["tiers"]
804
+ tiers ||= {}
805
+ # tier identified by name, case sensitive...
806
+ if !tiers[tier_name]
807
+ tiers[tier_name] = {}
808
+ end
809
+ tier_config = tiers[tier_name]
810
+
811
+ tier_config['instances'] ||= []
812
+ instance_config = tier_config['instances'].find {|it| it["instance"] && it["instance"]["type"] && it["instance"]["type"] == instance_type["code"] }
813
+ if !instance_config
814
+ instance_config = {'instance' => {'type' => instance_type['code']} }
815
+ tier_config['instances'].push(instance_config)
816
+ end
817
+ instance_config['instance'] ||= {}
818
+
819
+ # group prompt
820
+
821
+ # use active group by default
822
+ options[:group] ||= @active_group_id
823
+
824
+
825
+ # available_groups = get_available_groups()
826
+ # group_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'group', 'fieldLabel' => 'Group', 'type' => 'select', 'selectOptions' => get_available_groups(), 'required' => true, 'defaultValue' => @active_group_id}],options[:options],@api_client,{})
827
+
828
+ # group_id = group_prompt['group']
829
+ # the_group = find_group_by_name_or_id_for_provisioning(group_id)
830
+
831
+ # # cloud prompt
832
+ # cloud_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'cloud', 'type' => 'select', 'fieldLabel' => 'Cloud', 'optionSource' => 'clouds', 'required' => true, 'description' => 'Select Cloud.'}],options[:options],@api_client,{groupId: group_id})
833
+ # cloud_id = cloud_prompt['cloud']
834
+
835
+ # look for existing config for group + cloud
836
+
837
+ options[:name_required] = false
838
+ options[:instance_type_code] = instance_type["code"]
839
+
840
+ #options[:options].deep_merge!(specific_config)
841
+ # this provisioning helper method handles all (most) of the parsing and prompting
842
+ instance_config_payload = prompt_new_instance(options)
843
+
844
+ # strip all empty string and nil, would be problematic for update()
845
+ instance_config_payload.deep_compact!
846
+
847
+ # puts "INSTANCE CONFIG YAML:"
848
+ # puts as_yaml(instance_config_payload)
849
+
850
+ selected_environment = instance_config_payload.delete('instanceContext') || instance_config_payload.delete('environment')
851
+ # groom provision instance payload for template purposes
852
+ selected_cloud_id = instance_config_payload.delete('zoneId')
853
+ selected_site = instance_config_payload['instance'].delete('site')
854
+ selected_site_id = selected_site['id']
855
+
856
+ selected_group = find_group_by_name_or_id_for_provisioning(selected_site_id)
857
+ selected_cloud = find_cloud_by_name_or_id_for_provisioning(selected_group['id'], selected_cloud_id)
858
+
859
+ # store config in environments => env => groups => groupname => clouds => cloudname =>
860
+ current_config = instance_config
861
+ if selected_environment.to_s != ''
862
+ instance_config['environments'] ||= {}
863
+ instance_config['environments'][selected_environment] ||= {}
864
+ current_config = instance_config['environments'][selected_environment]
865
+ end
866
+
867
+ current_config['groups'] ||= {}
868
+ current_config['groups'][selected_group['name']] ||= {}
869
+ current_config['groups'][selected_group['name']]['clouds'] ||= {}
870
+ current_config['groups'][selected_group['name']]['clouds'][selected_cloud['name']] = instance_config_payload
871
+
872
+ # ok, make api request
873
+ blueprint["config"]["tiers"] = tiers
874
+ request_payload = {blueprint: blueprint}
875
+
876
+ if options[:dry_run]
877
+ print_dry_run @blueprints_interface.dry.update(blueprint['id'], request_payload)
878
+ return 0
879
+ end
880
+
881
+ json_response = @blueprints_interface.update(blueprint['id'], request_payload)
882
+
883
+ if options[:json]
884
+ puts JSON.pretty_generate(json_response)
885
+ else
886
+ print_green_success "Instance added to blueprint #{blueprint['name']}"
887
+ get([blueprint['name']])
888
+ end
889
+ return 0
890
+
891
+ rescue RestClient::Exception => e
892
+ print_rest_exception(e, options)
893
+ return 1
894
+ end
895
+
896
+ end
897
+
898
+ def remove_instance_config(args)
899
+ instance_index = nil
900
+ options = {}
901
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
902
+ opts.banner = subcommand_usage("[id] [tier] [instance] -g GROUP -c CLOUD")
903
+ opts.on( '-g', '--group GROUP', "Group" ) do |val|
904
+ options[:group] = val
905
+ end
906
+ opts.on( '-c', '--cloud CLOUD', "Cloud" ) do |val|
907
+ options[:cloud] = val
908
+ end
909
+ opts.on( '-e', '--env ENV', "Environment" ) do |val|
910
+ options[:environment] = val
911
+ end
912
+ # opts.on( nil, '--index NUMBER', "The index of the instance to remove, starting with 0." ) do |val|
913
+ # instance_index = val.to_i
914
+ # end
915
+ build_common_options(opts, options, [:auto_confirm, :json, :dry_run, :remote])
916
+ opts.footer = "Update a blueprint, removing a specified instance config." + "\n" +
917
+ "[id] is required. This is the name or id of a blueprint." + "\n" +
918
+ "[tier] is required. This is the name of the tier." + "\n" +
919
+ "[instance] is required. This is the type of instance." + "\n" +
920
+ "The config scope is specified with the -g GROUP, -c CLOUD and -e ENV. The -g and -c options are required."
921
+ end
922
+ optparse.parse!(args)
923
+
924
+ if args.count < 3
925
+ print_error Morpheus::Terminal.angry_prompt
926
+ puts_error "Wrong number of arguments"
927
+ puts_error optparse
928
+ return 1
929
+ end
930
+ if !options[:group]
931
+ print_error Morpheus::Terminal.angry_prompt
932
+ puts_error "Missing required argument -g GROUP"
933
+ puts_error optparse
934
+ return 1
935
+ end
936
+ if !options[:cloud]
937
+ print_error Morpheus::Terminal.angry_prompt
938
+ puts_error "Missing required argument -g CLOUD"
939
+ puts_error optparse
940
+ return 1
941
+ end
942
+ connect(options)
943
+
944
+ begin
945
+
946
+ blueprint_name = args[0]
947
+ tier_name = args[1]
948
+ instance_type_code = args[2]
949
+ # we also need consider when there is multiple instances of the same type in
950
+ # a template/tier.. so maybe split instance_type_code as [type-code]:[index].. or...errr
951
+
952
+ blueprint = find_blueprint_by_name_or_id(blueprint_name)
953
+ return 1 if blueprint.nil?
954
+
955
+ instance_type = find_instance_type_by_code(instance_type_code)
956
+ return 1 if instance_type.nil?
957
+
958
+ tier_config = nil
959
+ # instance_config = nil
960
+
961
+ blueprint["config"] ||= {}
962
+ tiers = blueprint["config"]["tiers"]
963
+ tiers ||= {}
964
+ # tier identified by name, case sensitive...
965
+ if !tiers[tier_name]
966
+ print_red_alert "Tier not found by name #{tier_name}"
967
+ return 1
968
+ end
969
+ tier_config = tiers[tier_name]
970
+
971
+ if !tier_config
972
+ print_red_alert "Tier not found by name #{tier1_name}!"
973
+ return 1
974
+ elsif tier_config['instances'].nil? || tier_config['instances'].empty?
975
+ print_red_alert "Tier #{tier_name} is empty!"
976
+ return 1
977
+ end
978
+
979
+ matching_indices = []
980
+ if tier_config['instances']
981
+ if instance_index
982
+ matching_indices = [instance_index].compact
983
+ else
984
+ tier_config['instances'].each_with_index do |instance_config, index|
985
+ is_match = instance_config['instance'] && instance_config['instance']['type'] == instance_type['code']
986
+ if is_match
987
+ matching_indices << index
988
+ end
989
+ end
990
+ end
991
+ end
992
+
993
+ if matching_indices.size == 0
994
+ print_red_alert "Instance not found by tier: #{tier_name}, type: #{instance_type_code}"
995
+ return 1
996
+ elsif matching_indices.size > 1
997
+ #print_error Morpheus::Terminal.angry_prompt
998
+ print_red_alert "More than one instance found by tier: #{tier_name}, type: #{instance_type_code}"
999
+ puts_error "Try using the --index option to identify the instance you wish to remove."
1000
+ puts_error optparse
1001
+ return 1
1002
+ end
1003
+
1004
+ # ok, find the specified config
1005
+ instance_config = tier_config['instances'][matching_indices[0]]
1006
+ parent_config = nil
1007
+ current_config = instance_config
1008
+ delete_key = nil
1009
+
1010
+ config_description = "type: #{instance_type['code']}"
1011
+ config_description << " environment: #{options[:environment]}" if options[:environment]
1012
+ config_description << " group: #{options[:group]}" if options[:group]
1013
+ config_description << " cloud: #{options[:cloud]}" if options[:cloud]
1014
+ config_description = config_description.strip
1015
+
1016
+
1017
+ # find config in environments => env => groups => groupname => clouds => cloudname =>
1018
+ if options[:environment]
1019
+ if current_config && current_config['environments'] && current_config['environments'][options[:environment]]
1020
+ parent_config = current_config['environments']
1021
+ delete_key = options[:environment]
1022
+ current_config = parent_config[delete_key]
1023
+ else
1024
+ print_red_alert "Instance config not found for scope #{config_description}"
1025
+ return 1
1026
+ end
1027
+ end
1028
+ if options[:group]
1029
+ if current_config && current_config['groups'] && current_config['groups'][options[:group]]
1030
+ parent_config = current_config['groups']
1031
+ delete_key = options[:group]
1032
+ current_config = parent_config[delete_key]
1033
+ else
1034
+ print_red_alert "Instance config not found for scope #{config_description}"
1035
+ return 1
1036
+ end
1037
+ end
1038
+ if options[:cloud]
1039
+ if current_config && current_config['clouds'] && current_config['clouds'][options[:cloud]]
1040
+ parent_config = current_config['clouds']
1041
+ delete_key = options[:cloud]
1042
+ current_config = parent_config[delete_key]
1043
+ else
1044
+ print_red_alert "Instance config not found for scope #{config_description}"
1045
+ return 1
1046
+ end
1047
+ end
1048
+
1049
+ # remove it
1050
+ parent_config.delete(delete_key)
1051
+
1052
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete this instance config #{config_description} ?")
1053
+ return 9
1054
+ end
1055
+
1056
+ # ok, make api request
1057
+ blueprint["config"]["tiers"] = tiers
1058
+ request_payload = {blueprint: blueprint}
1059
+
1060
+ if options[:dry_run]
1061
+ print_dry_run @blueprints_interface.dry.update(blueprint['id'], request_payload)
1062
+ return
1063
+ end
1064
+ json_response = @blueprints_interface.update(blueprint['id'], request_payload)
1065
+
1066
+ if options[:json]
1067
+ puts JSON.pretty_generate(json_response)
1068
+ else
1069
+ print_green_success "Removed instance from blueprint."
1070
+ get([blueprint['id']])
1071
+ end
1072
+ return 0
1073
+
1074
+ rescue RestClient::Exception => e
1075
+ print_rest_exception(e, options)
1076
+ exit 1
1077
+ end
1078
+ end
1079
+
1080
+ def update_instance(args)
1081
+ print_red_alert "NOT YET SUPPORTED"
1082
+ return 5
1083
+ end
1084
+
1085
+ def update_instance_config(args)
1086
+ print_red_alert "NOT YET SUPPORTED"
1087
+ return 5
1088
+ end
1089
+
1090
+ def remove_instance(args)
1091
+ instance_index = nil
1092
+ options = {}
1093
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
1094
+ opts.banner = subcommand_usage("[id] [tier] [instance]")
1095
+ # opts.on('--index NUMBER', Number, "Identify Instance by index within tier, starting with 0." ) do |val|
1096
+ # instance_index = val.to_i
1097
+ # end
1098
+ build_common_options(opts, options, [:auto_confirm, :json, :dry_run, :remote])
1099
+ end
1100
+ optparse.parse!(args)
1101
+
1102
+ if args.count < 3
1103
+ print_error Morpheus::Terminal.angry_prompt
1104
+ puts_error "Wrong number of arguments"
1105
+ puts_error optparse
1106
+ return 1
1107
+ end
1108
+
1109
+ connect(options)
1110
+
1111
+ begin
1112
+
1113
+ blueprint_name = args[0]
1114
+ tier_name = args[1]
1115
+ instance_identier = args[2]
1116
+
1117
+ # instance_type_code = args[2]
1118
+ # we also need consider when there is multiple instances of the same type in
1119
+ # a template/tier.. so maybe split instance_type_code as [type-code]:[index].. or...errr
1120
+
1121
+ blueprint = find_blueprint_by_name_or_id(blueprint_name)
1122
+ return 1 if blueprint.nil?
1123
+
1124
+ # instance_type = find_instance_type_by_code(instance_type_code)
1125
+ # return 1 if instance_type.nil?
1126
+
1127
+ tier_config = nil
1128
+ # instance_config = nil
1129
+
1130
+ blueprint["config"] ||= {}
1131
+ tiers = blueprint["config"]["tiers"]
1132
+ tiers ||= {}
1133
+ # tier identified by name, case sensitive...
1134
+ if !tiers[tier_name]
1135
+ print_red_alert "Tier not found by name #{tier_name}"
1136
+ return 1
1137
+ end
1138
+ tier_config = tiers[tier_name]
1139
+
1140
+ if tier_config['instances'].nil? || tier_config['instances'].empty?
1141
+ print_red_alert "Tier #{tier_name} is empty!"
1142
+ return 1
1143
+ end
1144
+
1145
+ # find instance
1146
+ matching_indices = []
1147
+ if tier_config['instances']
1148
+ if instance_identier.to_s =~ /\A\d{1,}\Z/
1149
+ matching_indices = [instance_identier.to_i].compact
1150
+ else
1151
+ tier_config['instances'].each_with_index do |instance_config, index|
1152
+ if instance_config['instance'] && instance_config['instance']['type'] == instance_identier
1153
+ matching_indices << index
1154
+ elsif instance_config['instance'] && instance_config['instance']['name'] == instance_identier
1155
+ matching_indices << index
1156
+ end
1157
+ end
1158
+ end
1159
+ end
1160
+ if matching_indices.size == 0
1161
+ print_red_alert "Instance not found by tier: #{tier_name}, instance: #{instance_identier}"
1162
+ return 1
1163
+ elsif matching_indices.size > 1
1164
+ #print_error Morpheus::Terminal.angry_prompt
1165
+ print_red_alert "More than one instance matched tier: #{tier_name}, instance: #{instance_identier}"
1166
+ puts_error "Instance can be identified type, name or index within the tier."
1167
+ puts_error optparse
1168
+ return 1
1169
+ end
1170
+
1171
+ # remove it
1172
+ tier_config['instances'].delete_at(matching_indices[0])
1173
+
1174
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete this instance #{instance_type_code} instance from tier: #{tier_name}?")
1175
+ return 9
1176
+ end
1177
+
1178
+ # ok, make api request
1179
+ blueprint["config"]["tiers"] = tiers
1180
+ request_payload = {blueprint: blueprint}
1181
+
1182
+ if options[:dry_run]
1183
+ print_dry_run @blueprints_interface.dry.update(blueprint['id'], request_payload)
1184
+ return
1185
+ end
1186
+ json_response = @blueprints_interface.update(blueprint['id'], request_payload)
1187
+
1188
+ if options[:json]
1189
+ puts JSON.pretty_generate(json_response)
1190
+ else
1191
+ print_green_success "Removed instance from blueprint."
1192
+ get([blueprint['id']])
1193
+ end
1194
+ return 0
1195
+
1196
+ rescue RestClient::Exception => e
1197
+ print_rest_exception(e, options)
1198
+ exit 1
1199
+ end
1200
+ end
1201
+
1202
+ def add_tier(args)
1203
+ options = {}
1204
+ boot_order = nil
1205
+ linked_tiers = nil
1206
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
1207
+ opts.banner = subcommand_usage("[id] [tier]")
1208
+ opts.on('--name VALUE', String, "Tier Name") do |val|
1209
+ options[:name] = val
1210
+ end
1211
+ opts.on('--bootOrder NUMBER', String, "Boot Order" ) do |val|
1212
+ boot_order = val
1213
+ end
1214
+ opts.on('--linkedTiers x,y,z', Array, "Connected Tiers.") do |val|
1215
+ linked_tiers = val
1216
+ end
1217
+ build_common_options(opts, options, [:options, :json, :dry_run, :remote])
1218
+ end
1219
+ optparse.parse!(args)
1220
+
1221
+ if args.count < 1
1222
+ print_error Morpheus::Terminal.angry_prompt
1223
+ puts_error "#{command_name} add-tier requires argument: [id]\n#{optparse}"
1224
+ # puts optparse
1225
+ return 1
1226
+ end
1227
+ blueprint_name = args[0]
1228
+ tier_name = args[1]
1229
+
1230
+ connect(options)
1231
+
1232
+ begin
1233
+ blueprint = find_blueprint_by_name_or_id(blueprint_name)
1234
+ return 1 if blueprint.nil?
1235
+
1236
+ blueprint["config"] ||= {}
1237
+ blueprint["config"]["tiers"] ||= {}
1238
+ tiers = blueprint["config"]["tiers"]
1239
+
1240
+ # prompt new tier
1241
+ # Name
1242
+ # {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true, 'displayOrder' => 1, 'description' => 'A unique name for the blueprint.'},
1243
+ # {'fieldName' => 'bootOrder', 'fieldLabel' => 'Boot Order', 'type' => 'text', 'required' => false, 'displayOrder' => 2, 'description' => 'Boot Order'}
1244
+ if !tier_name
1245
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'name', 'fieldLabel' => 'Tier Name', 'type' => 'text', 'required' => true, 'description' => 'Enter the name of the tier'}], options[:options])
1246
+ tier_name = v_prompt['name']
1247
+ end
1248
+ # case insensitive match
1249
+ existing_tier_names = tiers.keys
1250
+ matching_tier_name = existing_tier_names.find {|k| k.downcase == tier_name.downcase }
1251
+ if matching_tier_name
1252
+ # print_red_alert "Tier #{tier_name} already exists"
1253
+ # return 1
1254
+ print cyan,"Tier #{tier_name} already exists.",reset,"\n"
1255
+ return 0
1256
+ end
1257
+ # idempotent
1258
+ if !tiers[tier_name]
1259
+ tiers[tier_name] = {'instances' => []}
1260
+ end
1261
+ tier = tiers[tier_name]
1262
+
1263
+ # Boot Order
1264
+ if !boot_order
1265
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'bootOrder', 'fieldLabel' => 'Boot Order', 'type' => 'text', 'required' => false, 'description' => 'Sequence order for starting app instances by tier. 0-N', 'defaultValue' => tier['bootOrder']}], options[:options])
1266
+ boot_order = v_prompt['bootOrder']
1267
+ end
1268
+ if boot_order.to_s == 'null'
1269
+ tier.delete('bootOrder')
1270
+ elsif boot_order.to_s != ''
1271
+ tier['bootOrder'] = boot_order.to_i
1272
+ end
1273
+
1274
+ # Connected Tiers
1275
+ if !linked_tiers
1276
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'linkedTiers', 'fieldLabel' => 'Connected Tiers', 'type' => 'text', 'required' => false, 'description' => 'Names of connected tiers, comma separated', 'defaultValue' => (linked_tiers ? linked_tiers.join(',') : nil)}], options[:options])
1277
+ linked_tiers = v_prompt['linkedTiers'].to_s.split(',').collect {|it| it.strip }.select {|it| it != ''}
1278
+ end
1279
+ if linked_tiers && !linked_tiers.empty?
1280
+ linked_tiers.each do |other_tier_name|
1281
+ link_result = link_tiers(tiers, [tier_name, other_tier_name])
1282
+ # could just re-prompt unless options[:no_prompt]
1283
+ return 1 if !link_result
1284
+ end
1285
+ end
1286
+
1287
+ # ok, make api request
1288
+ blueprint["config"]["tiers"] = tiers
1289
+ request_payload = blueprint["config"]
1290
+ # request_payload = {blueprint: blueprint}
1291
+
1292
+ if options[:dry_run]
1293
+ print_dry_run @blueprints_interface.dry.update(blueprint['id'], request_payload)
1294
+ return
1295
+ end
1296
+ json_response = @blueprints_interface.update(blueprint['id'], request_payload)
1297
+
1298
+ if options[:json]
1299
+ puts JSON.pretty_generate(json_response)
1300
+ elsif !options[:quiet]
1301
+ print_green_success "Added tier #{tier_name}"
1302
+ # prompt for new instance
1303
+ if !options[:no_prompt]
1304
+ if ::Morpheus::Cli::OptionTypes::confirm("Would you like to add an instance now?", options.merge({default: true}))
1305
+ add_instance([blueprint['id'], tier_name])
1306
+ while ::Morpheus::Cli::OptionTypes::confirm("Add another instance now?", options.merge({default: false})) do
1307
+ add_instance([blueprint['id'], tier_name])
1308
+ end
1309
+ # if !add_instance_result
1310
+ # end
1311
+ end
1312
+ end
1313
+ # print details
1314
+ get([blueprint['name']])
1315
+ end
1316
+ return 0
1317
+ rescue RestClient::Exception => e
1318
+ print_rest_exception(e, options)
1319
+ exit 1
1320
+ end
1321
+ end
1322
+
1323
+ def update_tier(args)
1324
+ options = {}
1325
+ new_tier_name = nil
1326
+ boot_order = nil
1327
+ linked_tiers = nil
1328
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
1329
+ opts.banner = subcommand_usage("[id] [tier]")
1330
+ opts.on('--name VALUE', String, "Tier Name") do |val|
1331
+ new_tier_name = val
1332
+ end
1333
+ opts.on('--bootOrder NUMBER', String, "Boot Order" ) do |val|
1334
+ boot_order = val
1335
+ end
1336
+ opts.on('--linkedTiers x,y,z', Array, "Connected Tiers") do |val|
1337
+ linked_tiers = val
1338
+ end
1339
+ build_common_options(opts, options, [:options, :json, :dry_run, :remote])
1340
+ end
1341
+ optparse.parse!(args)
1342
+
1343
+ if args.count != 2
1344
+ print_error Morpheus::Terminal.angry_prompt
1345
+ puts_error "#{command_name} update-tier expects 2 arguments and received #{args.count}: #{args.join(' ')}\n#{optparse}"
1346
+ return 1
1347
+ end
1348
+ blueprint_name = args[0]
1349
+ tier_name = args[1]
1350
+
1351
+ connect(options)
1352
+
1353
+ begin
1354
+ blueprint = find_blueprint_by_name_or_id(blueprint_name)
1355
+ return 1 if blueprint.nil?
1356
+
1357
+ blueprint["config"] ||= {}
1358
+ blueprint["config"]["tiers"] ||= {}
1359
+ tiers = blueprint["config"]["tiers"]
1360
+
1361
+ if !tiers[tier_name]
1362
+ print_red_alert "Tier not found by name #{tier_name}"
1363
+ return 1
1364
+ end
1365
+ tier = tiers[tier_name]
1366
+
1367
+
1368
+ if options[:no_prompt]
1369
+ if !(new_tier_name || boot_order || linked_tiers)
1370
+ print_error Morpheus::Terminal.angry_prompt
1371
+ puts_error "#{command_name} update-tier requires an option to update.\n#{optparse}"
1372
+ return 1
1373
+ end
1374
+ end
1375
+
1376
+ # prompt update tier
1377
+ # Name
1378
+ if !new_tier_name
1379
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'name', 'fieldLabel' => 'Tier Name', 'type' => 'text', 'required' => true, 'description' => 'Rename the tier', 'defaultValue' => tier_name}], options[:options])
1380
+ new_tier_name = v_prompt['name']
1381
+ end
1382
+ if new_tier_name && new_tier_name != tier_name
1383
+ old_tier_name = tier_name
1384
+ if tiers[new_tier_name]
1385
+ print_red_alert "A tier named #{tier_name} already exists."
1386
+ return 1
1387
+ end
1388
+ tier = tiers.delete(tier_name)
1389
+ tiers[new_tier_name] = tier
1390
+ # Need to fix all the linkedTiers
1391
+ tiers.each do |k, v|
1392
+ if v['linkedTiers'] && v['linkedTiers'].include?(tier_name)
1393
+ v['linkedTiers'] = v['linkedTiers'].map {|it| it == tier_name ? new_tier_name : it }
1394
+ end
1395
+ end
1396
+ # old_tier_name = tier_name
1397
+ tier_name = new_tier_name
1398
+ end
1399
+
1400
+ # Boot Order
1401
+ if !boot_order
1402
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'bootOrder', 'fieldLabel' => 'Boot Order', 'type' => 'text', 'required' => false, 'description' => 'Sequence order for starting app instances by tier. 0-N', 'defaultValue' => tier['bootOrder']}], options[:options])
1403
+ boot_order = v_prompt['bootOrder']
1404
+ end
1405
+ if boot_order.to_s == 'null'
1406
+ tier.delete('bootOrder')
1407
+ elsif boot_order.to_s != ''
1408
+ tier['bootOrder'] = boot_order.to_i
1409
+ end
1410
+
1411
+ # Connected Tiers
1412
+ if !linked_tiers
1413
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'linkedTiers', 'fieldLabel' => 'Connected Tiers', 'type' => 'text', 'required' => false, 'description' => 'Names of connected tiers, comma separated', 'defaultValue' => (tier['linkedTiers'] ? tier['linkedTiers'].join(',') : nil)}], options[:options])
1414
+ linked_tiers = v_prompt['linkedTiers'].to_s.split(',').collect {|it| it.strip }.select {|it| it != ''}
1415
+ end
1416
+ current_linked_tiers = tier['linkedTiers'] || []
1417
+ if linked_tiers && linked_tiers != current_linked_tiers
1418
+ remove_tiers = current_linked_tiers - linked_tiers
1419
+ remove_tiers.each do |other_tier_name|
1420
+ unlink_result = unlink_tiers(tiers, [tier_name, other_tier_name])
1421
+ # could just re-prompt unless options[:no_prompt]
1422
+ return 1 if !unlink_result
1423
+ end
1424
+ add_tiers = linked_tiers - current_linked_tiers
1425
+ add_tiers.each do |other_tier_name|
1426
+ link_result = link_tiers(tiers, [tier_name, other_tier_name])
1427
+ # could just re-prompt unless options[:no_prompt]
1428
+ return 1 if !link_result
1429
+ end
1430
+ end
1431
+
1432
+ # ok, make api request
1433
+ blueprint["config"]["tiers"] = tiers
1434
+ request_payload = blueprint["config"]
1435
+ # request_payload = {blueprint: blueprint}
1436
+
1437
+ if options[:dry_run]
1438
+ print_dry_run @blueprints_interface.dry.update(blueprint['id'], request_payload)
1439
+ return
1440
+ end
1441
+ json_response = @blueprints_interface.update(blueprint['id'], request_payload)
1442
+
1443
+ if options[:json]
1444
+ puts JSON.pretty_generate(json_response)
1445
+ elsif !options[:quiet]
1446
+ print_green_success "Updated tier #{tier_name}"
1447
+ get([blueprint['id']])
1448
+ end
1449
+ return 0
1450
+ rescue RestClient::Exception => e
1451
+ print_rest_exception(e, options)
1452
+ exit 1
1453
+ end
1454
+ end
1455
+
1456
+ def remove_tier(args)
1457
+ options = {}
1458
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
1459
+ opts.banner = subcommand_usage("[id] [tier]")
1460
+ build_common_options(opts, options, [:auto_confirm, :json, :dry_run, :remote])
1461
+ end
1462
+ optparse.parse!(args)
1463
+
1464
+ if args.count < 2
1465
+ print_error Morpheus::Terminal.angry_prompt
1466
+ puts_error "#{command_name} remove-tier expects 2 arguments and received #{args.count}: #{args.join(' ')}\n#{optparse}"
1467
+ return 1
1468
+ end
1469
+ blueprint_name = args[0]
1470
+ tier_name = args[1]
1471
+
1472
+ connect(options)
1473
+
1474
+ begin
1475
+ blueprint = find_blueprint_by_name_or_id(blueprint_name)
1476
+ return 1 if blueprint.nil?
1477
+
1478
+ blueprint["config"] ||= {}
1479
+ blueprint["config"]["tiers"] ||= {}
1480
+ tiers = blueprint["config"]["tiers"]
1481
+
1482
+ if !tiers[tier_name]
1483
+ # print_red_alert "Tier not found by name #{tier_name}"
1484
+ # return 1
1485
+ print cyan,"Tier #{tier_name} does not exist.",reset,"\n"
1486
+ return 0
1487
+ end
1488
+
1489
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete the tier #{tier_name}?")
1490
+ exit
1491
+ end
1492
+
1493
+ # remove it
1494
+ tiers.delete(tier_name)
1495
+
1496
+ # ok, make api request
1497
+ blueprint["config"]["tiers"] = tiers
1498
+ request_payload = blueprint["config"]
1499
+ # request_payload = {blueprint: blueprint}
1500
+
1501
+ if options[:dry_run]
1502
+ print_dry_run @blueprints_interface.dry.update(blueprint['id'], request_payload)
1503
+ return
1504
+ end
1505
+
1506
+ json_response = @blueprints_interface.update(blueprint['id'], request_payload)
1507
+
1508
+
1509
+ if options[:json]
1510
+ print JSON.pretty_generate(json_response)
1511
+ print "\n"
1512
+ else
1513
+ print_green_success "Removed tier #{tier_name}"
1514
+ get([blueprint['name']])
1515
+ end
1516
+
1517
+ rescue RestClient::Exception => e
1518
+ print_rest_exception(e, options)
1519
+ exit 1
1520
+ end
1521
+ end
1522
+
1523
+ def connect_tiers(args)
1524
+ options = {}
1525
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
1526
+ opts.banner = subcommand_usage("[id] [Tier1] [Tier2]")
1527
+ build_common_options(opts, options, [:json, :dry_run, :remote])
1528
+ end
1529
+ optparse.parse!(args)
1530
+
1531
+ if args.count < 3
1532
+ print_error Morpheus::Terminal.angry_prompt
1533
+ puts_error "#{command_name} connect-tiers expects 3 arguments and received #{args.count}: #{args.join(' ')}\n#{optparse}"
1534
+ # puts optparse
1535
+ return 1
1536
+ end
1537
+ blueprint_name = args[0]
1538
+ tier1_name = args[1]
1539
+ tier2_name = args[2]
1540
+
1541
+ connect(options)
1542
+
1543
+ begin
1544
+ blueprint = find_blueprint_by_name_or_id(blueprint_name)
1545
+ return 1 if blueprint.nil?
1546
+
1547
+ blueprint["config"] ||= {}
1548
+ tiers = blueprint["config"]["tiers"]
1549
+
1550
+ if !tiers || tiers.keys.size == 0
1551
+ error_msg = "Blueprint #{blueprint['name']} has no tiers."
1552
+ # print_red_alert "Blueprint #{blueprint['name']} has no tiers."
1553
+ # raise_command_error "Blueprint #{blueprint['name']} has no tiers."
1554
+ print_error Morpheus::Terminal.angry_prompt
1555
+ puts_error "Blueprint #{blueprint['name']} has no tiers."
1556
+ return 1
1557
+ end
1558
+
1559
+ connect_tiers = []
1560
+ tier1 = tiers[tier1_name]
1561
+ tier2 = tiers[tier2_name]
1562
+ # uhh support N args
1563
+
1564
+ if tier1.nil?
1565
+ print_red_alert "Tier not found by name #{tier1_name}!"
1566
+ return 1
1567
+ end
1568
+
1569
+ if tier2.nil?
1570
+ print_red_alert "Tier not found by name #{tier2_name}!"
1571
+ return 1
1572
+ end
1573
+
1574
+ tier1["linkedTiers"] = tier1["linkedTiers"] || []
1575
+ tier2["linkedTiers"] = tier2["linkedTiers"] || []
1576
+
1577
+ found_edge = tier1["linkedTiers"].include?(tier2_name) || tier2["linkedTiers"].include?(tier1_name)
1578
+
1579
+ if found_edge
1580
+ puts cyan,"Tiers #{tier1_name} and #{tier2_name} are already connected.",reset
1581
+ return 0
1582
+ end
1583
+
1584
+ # ok to be connect the tiers
1585
+ # note: the ui doesn't hook up both sides eh?
1586
+
1587
+ if !tier1["linkedTiers"].include?(tier2_name)
1588
+ tier1["linkedTiers"].push(tier2_name)
1589
+ end
1590
+
1591
+ if !tier2["linkedTiers"].include?(tier1_name)
1592
+ tier2["linkedTiers"].push(tier1_name)
1593
+ end
1594
+
1595
+ # ok, make api request
1596
+ blueprint["config"]["tiers"] = tiers
1597
+ request_payload = blueprint["config"]
1598
+ # request_payload = {blueprint: blueprint}
1599
+
1600
+ if options[:dry_run]
1601
+ print_dry_run @blueprints_interface.dry.update(blueprint['id'], request_payload)
1602
+ return
1603
+ end
1604
+ json_response = @blueprints_interface.update(blueprint['id'], request_payload)
1605
+
1606
+
1607
+ if options[:json]
1608
+ print JSON.pretty_generate(json_response)
1609
+ print "\n"
1610
+ else
1611
+ print_green_success "Connected 2 tiers for blueprint #{blueprint['name']}"
1612
+ get([blueprint['name']])
1613
+ end
1614
+
1615
+ rescue RestClient::Exception => e
1616
+ print_rest_exception(e, options)
1617
+ exit 1
1618
+ end
1619
+ end
1620
+
1621
+ def disconnect_tiers(args)
1622
+ options = {}
1623
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
1624
+ opts.banner = subcommand_usage("[id] [Tier1] [Tier2]")
1625
+ build_common_options(opts, options, [:json, :dry_run, :remote])
1626
+ end
1627
+ optparse.parse!(args)
1628
+
1629
+ if args.count < 3
1630
+ print_error Morpheus::Terminal.angry_prompt
1631
+ puts_error "#{command_name} disconnect-tiers expects 3 arguments and received #{args.count}: #{args.join(' ')}\n#{optparse}"
1632
+ # puts optparse
1633
+ return 1
1634
+ end
1635
+ blueprint_name = args[0]
1636
+ tier1_name = args[1]
1637
+ tier2_name = args[2]
1638
+
1639
+ connect(options)
1640
+
1641
+ begin
1642
+ blueprint = find_blueprint_by_name_or_id(blueprint_name)
1643
+ return 1 if blueprint.nil?
1644
+
1645
+ blueprint["config"] ||= {}
1646
+ tiers = blueprint["config"]["tiers"]
1647
+
1648
+ if !tiers || tiers.keys.size == 0
1649
+ # print_red_alert "Blueprint #{blueprint['name']} has no tiers."
1650
+ # raise_command_error "Blueprint #{blueprint['name']} has no tiers."
1651
+ print_error Morpheus::Terminal.angry_prompt
1652
+ puts_error "Blueprint #{blueprint['name']} has no tiers."
1653
+ return 1
1654
+ end
1655
+
1656
+ connect_tiers = []
1657
+ tier1 = tiers[tier1_name]
1658
+ tier2 = tiers[tier2_name]
1659
+ # uhh support N args
1660
+
1661
+ if tier1.nil?
1662
+ print_red_alert "Tier not found by name #{tier1_name}!"
1663
+ return 1
1664
+ end
1665
+
1666
+ if tier2.nil?
1667
+ print_red_alert "Tier not found by name #{tier2_name}!"
1668
+ return 1
1669
+ end
1670
+
1671
+ tier1["linkedTiers"] = tier1["linkedTiers"] || []
1672
+ tier2["linkedTiers"] = tier2["linkedTiers"] || []
1673
+
1674
+ found_edge = tier1["linkedTiers"].include?(tier2_name) || tier2["linkedTiers"].include?(tier1_name)
1675
+
1676
+ if found_edge
1677
+ puts cyan,"Tiers #{tier1_name} and #{tier2_name} are not connected.",reset
1678
+ return 0
1679
+ end
1680
+
1681
+ # remove links
1682
+ tier1["linkedTiers"] = tier1["linkedTiers"].reject {|it| it == tier2_name }
1683
+ tier2["linkedTiers"] = tier2["linkedTiers"].reject {|it| it == tier1_name }
1684
+
1685
+ # ok, make api request
1686
+ blueprint["config"]["tiers"] = tiers
1687
+ request_payload = blueprint["config"]
1688
+ # request_payload = {blueprint: blueprint}
1689
+
1690
+ if options[:dry_run]
1691
+ print_dry_run @blueprints_interface.dry.update(blueprint['id'], request_payload)
1692
+ return
1693
+ end
1694
+ json_response = @blueprints_interface.update(blueprint['id'], request_payload)
1695
+
1696
+
1697
+ if options[:json]
1698
+ print JSON.pretty_generate(json_response)
1699
+ print "\n"
1700
+ else
1701
+ print_green_success "Connected 2 tiers for blueprint #{blueprint['name']}"
1702
+ get([blueprint['name']])
1703
+ end
1704
+
1705
+ rescue RestClient::Exception => e
1706
+ print_rest_exception(e, options)
1707
+ exit 1
1708
+ end
1709
+ end
1710
+
1711
+ def available_tiers(args)
1712
+ options = {}
1713
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
1714
+ opts.banner = subcommand_usage()
1715
+ build_common_options(opts, options, [:json, :dry_run, :remote])
1716
+ end
1717
+ optparse.parse!(args)
1718
+ connect(options)
1719
+ params = {}
1720
+
1721
+ begin
1722
+ if options[:dry_run]
1723
+ print_dry_run @blueprints_interface.dry.list_tiers(params)
1724
+ return
1725
+ end
1726
+ json_response = @blueprints_interface.list_tiers(params)
1727
+ tiers = json_response["tiers"] # just a list of names
1728
+ if options[:json]
1729
+ puts JSON.pretty_generate(json_response)
1730
+ else
1731
+ print_h1 "Available Tiers"
1732
+ if tiers.empty?
1733
+ print yellow,"No tiers found.",reset,"\n"
1734
+ else
1735
+ # rows = tiers.collect do |tier|
1736
+ # {
1737
+ # id: tier['id'],
1738
+ # name: tier['name'],
1739
+ # }
1740
+ # end
1741
+ # print cyan
1742
+ # tp rows, [:name]
1743
+ print cyan
1744
+ tiers.each do |tier_name|
1745
+ puts tier_name
1746
+ end
1747
+ end
1748
+ print reset,"\n"
1749
+ end
1750
+ return 0
1751
+ rescue RestClient::Exception => e
1752
+ print_rest_exception(e, options)
1753
+ exit 1
1754
+ end
1755
+
1756
+ end
1757
+
1758
+ private
1759
+
1760
+
1761
+ def add_blueprint_option_types(connected=true)
1762
+ [
1763
+ {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true, 'displayOrder' => 1},
1764
+ {'fieldName' => 'description', 'fieldLabel' => 'Description', 'type' => 'text', 'required' => false, 'displayOrder' => 2},
1765
+ {'fieldName' => 'category', 'fieldLabel' => 'Category', 'type' => 'text', 'required' => false, 'displayOrder' => 3},
1766
+ #{'fieldName' => 'group', 'fieldLabel' => 'Group', 'type' => 'select', 'selectOptions' => (connected ? get_available_groups() : []), 'required' => true}
1767
+ ]
1768
+ end
1769
+
1770
+ def update_blueprint_option_types(connected=true)
1771
+ list = add_blueprint_option_types(connected)
1772
+ list = list.reject {|it| ["group"].include? it['fieldName'] }
1773
+ list.each {|it| it['required'] = false }
1774
+ list
1775
+ end
1776
+
1777
+ def find_blueprint_by_name_or_id(val)
1778
+ if val.to_s =~ /\A\d{1,}\Z/
1779
+ return find_blueprint_by_id(val)
1780
+ else
1781
+ return find_blueprint_by_name(val)
1782
+ end
1783
+ end
1784
+
1785
+ def find_blueprint_by_id(id)
1786
+ begin
1787
+ json_response = @blueprints_interface.get(id.to_i)
1788
+ return json_response['blueprint']
1789
+ rescue RestClient::Exception => e
1790
+ if e.response && e.response.code == 404
1791
+ print_red_alert "Blueprint not found by id #{id}"
1792
+ else
1793
+ raise e
1794
+ end
1795
+ end
1796
+ end
1797
+
1798
+ def find_blueprint_by_name(name)
1799
+ blueprints = @blueprints_interface.list({name: name.to_s})['blueprints']
1800
+ if blueprints.empty?
1801
+ print_red_alert "Blueprint not found by name #{name}"
1802
+ return nil
1803
+ elsif blueprints.size > 1
1804
+ print_red_alert "#{blueprints.size} blueprints by name #{name}"
1805
+ print_blueprints_table(blueprints, {color: red})
1806
+ print reset,"\n"
1807
+ return nil
1808
+ else
1809
+ return blueprints[0]
1810
+ end
1811
+ end
1812
+
1813
+ def find_group_by_name(name)
1814
+ group_results = @groups_interface.get(name)
1815
+ if group_results['groups'].empty?
1816
+ print_red_alert "Group not found by name #{name}"
1817
+ return nil
1818
+ end
1819
+ return group_results['groups'][0]
1820
+ end
1821
+
1822
+ def find_cloud_by_name(group_id, name)
1823
+ option_results = @options_interface.options_for_source('clouds',{groupId: group_id})
1824
+ match = option_results['data'].find { |grp| grp['value'].to_s == name.to_s || grp['name'].downcase == name.downcase}
1825
+ if match.nil?
1826
+ print_red_alert "Cloud not found by name #{name}"
1827
+ return nil
1828
+ else
1829
+ return match['value']
1830
+ end
1831
+ end
1832
+
1833
+ def print_blueprints_table(blueprints, opts={})
1834
+ table_color = opts[:color] || cyan
1835
+ rows = blueprints.collect do |blueprint|
1836
+ #instance_type_names = (blueprint['instanceTypes'] || []).collect {|it| it['name'] }.join(', ')
1837
+ instance_type_names = []
1838
+ # if blueprint['config'] && blueprint['config']["tiers"]
1839
+ # blueprint['config']["tiers"]
1840
+ # end
1841
+ {
1842
+ id: blueprint['id'],
1843
+ name: blueprint['name'],
1844
+ description: blueprint['description'],
1845
+ category: blueprint['category'],
1846
+ tiers_summary: format_blueprint_tiers_summary(blueprint)
1847
+ }
1848
+ end
1849
+
1850
+ term_width = current_terminal_width()
1851
+ tiers_col_width = 60
1852
+ if term_width > 190
1853
+ tiers_col_width += 130
1854
+ end
1855
+ columns = [
1856
+ :id,
1857
+ :name,
1858
+ :description,
1859
+ :category,
1860
+ {:tiers_summary => {:display_name => "TIERS", :max_width => tiers_col_width} }
1861
+ ]
1862
+ if opts[:include_fields]
1863
+ columns = opts[:include_fields]
1864
+ end
1865
+ print table_color
1866
+ print as_pretty_table(rows, columns, opts)
1867
+ print reset
1868
+ end
1869
+
1870
+ def generate_id(len=16)
1871
+ id = ""
1872
+ len.times { id << (1 + rand(9)).to_s }
1873
+ id
1874
+ end
1875
+
1876
+ def format_blueprint_tiers_summary(blueprint)
1877
+ # don't use colors here, or cell truncation will not work
1878
+ str = ""
1879
+ if blueprint["config"] && blueprint["config"]["tiers"]
1880
+ tier_descriptions = blueprint["config"]["tiers"].collect do |tier_name, tier_config|
1881
+ # maybe do Tier Name (instance, instance2)
1882
+ instance_blurbs = []
1883
+ if tier_config["instances"]
1884
+ tier_config["instances"].each do |instance_config|
1885
+ if instance_config["instance"] && instance_config["instance"]["type"]
1886
+ # only have type: code in the config, rather not name fetch remotely right now..
1887
+ # instance_blurbs << instance_config["instance"]["type"]
1888
+ instance_name = instance_config["instance"]["name"] || ""
1889
+ instance_type_code = instance_config["instance"]["type"]
1890
+ instances_str = "#{instance_type_code}"
1891
+ if instance_name.to_s != ""
1892
+ instances_str << " - #{instance_name}"
1893
+ end
1894
+ begin
1895
+ config_list = parse_scoped_instance_configs(instance_config)
1896
+ if config_list.size == 0
1897
+ instances_str << " (No configs)"
1898
+ elsif config_list.size == 1
1899
+ # configs_str = config_list.collect {|it|
1900
+ # str = ""
1901
+ # it[:scope].to_s.inspect
1902
+ # }.join(", ")
1903
+ the_config = config_list[0]
1904
+ scope_str = the_config[:scope].collect {|k,v| v.to_s }.join("/")
1905
+ instances_str << " (#{scope_str})"
1906
+ else
1907
+ instances_str << " (#{config_list.size} configs)"
1908
+ end
1909
+ rescue => err
1910
+ puts_error "Failed to parse instance scoped instance configs: #{err.class} #{err.message}"
1911
+ raise err
1912
+ end
1913
+ instance_blurbs << instances_str
1914
+ end
1915
+ end
1916
+ end
1917
+ if instance_blurbs.size > 0
1918
+ tier_name + ": #{instance_blurbs.join(', ')}"
1919
+ else
1920
+ tier_name + ": (empty)"
1921
+ end
1922
+ end
1923
+ str += tier_descriptions.compact.join(", ")
1924
+ end
1925
+ str
1926
+ end
1927
+
1928
+ def print_blueprint_details(blueprint)
1929
+ print cyan
1930
+ description_cols = {
1931
+ "ID" => 'id',
1932
+ "Name" => 'name',
1933
+ "Description" => 'description',
1934
+ "Category" => 'category',
1935
+ "Image" => lambda {|it| it['config'] ? (it['config']['image'] == '/assets/apps/template.png' ? '(default)' : it['config']['image']) : '' },
1936
+ "Visibility" => 'visibility'
1937
+ }
1938
+ print_description_list(description_cols, blueprint)
1939
+ # print_h2 "Tiers"
1940
+ if blueprint["config"] && blueprint["config"]["tiers"] && blueprint["config"]["tiers"].keys.size != 0
1941
+ print cyan
1942
+ #puts as_yaml(blueprint["config"]["tiers"])
1943
+ blueprint["config"]["tiers"].each do |tier_name, tier_config|
1944
+ # print_h2 "Tier: #{tier_name}"
1945
+ print_h2 tier_name
1946
+ # puts " Instances:"
1947
+ if tier_config['instances'] && tier_config['instances'].size != 0
1948
+ # puts as_yaml(tier)
1949
+ tier_config['instances'].each_with_index do |instance_config, instance_index|
1950
+ instance_name = instance_config["instance"]["name"] || ""
1951
+ instance_type_code = ""
1952
+ if instance_config["instance"]["type"]
1953
+ instance_type_code = instance_config["instance"]["type"]
1954
+ end
1955
+ instance_bullet = ""
1956
+ # instance_bullet += "#{green} - #{bold}#{instance_type_code}#{reset}"
1957
+ instance_bullet += "#{green}#{bold}#{instance_type_code}#{reset}"
1958
+ if instance_name.to_s != ""
1959
+ instance_bullet += "#{green} - #{instance_name}#{reset}"
1960
+ end
1961
+ puts instance_bullet
1962
+ # print "\n"
1963
+ begin
1964
+ config_list = parse_scoped_instance_configs(instance_config)
1965
+ print cyan
1966
+ if config_list.size > 0
1967
+ print "\n"
1968
+ if config_list.size == 1
1969
+ puts " Config:"
1970
+ else
1971
+ puts " Configs (#{config_list.size}):"
1972
+ end
1973
+ config_list.each do |config_obj|
1974
+ # puts " = #{config_obj[:scope].inspect}"
1975
+ config_scope = config_obj[:scope]
1976
+ scoped_instance_config = config_obj[:config]
1977
+ config_description = ""
1978
+ config_items = []
1979
+ if config_scope[:environment]
1980
+ config_items << {label: "Environment", value: config_scope[:environment]}
1981
+ end
1982
+ if config_scope[:group]
1983
+ config_items << {label: "Group", value: config_scope[:group]}
1984
+ end
1985
+ if config_scope[:cloud]
1986
+ config_items << {label: "Cloud", value: config_scope[:cloud]}
1987
+ end
1988
+ # if scoped_instance_config['plan'] && scoped_instance_config['plan']['code']
1989
+ # config_items << {label: "Plan", value: scoped_instance_config['plan']['code']}
1990
+ # end
1991
+ config_description = config_items.collect {|item| "#{item[:label]}: #{item[:value]}"}.join(", ")
1992
+ puts " * #{config_description}"
1993
+ end
1994
+ else
1995
+ print white," Instance has no configs, see `app-templates add-instance-config \"#{blueprint['name']}\" \"#{tier_name}\" \"#{instance_type_code}\"`",reset,"\n"
1996
+ end
1997
+ rescue => err
1998
+ #puts_error "Failed to parse instance scoped instance configs for blueprint #{blueprint['id']} #{blueprint['name']} Exception: #{err.class} #{err.message}"
1999
+ end
2000
+ print "\n"
2001
+ #puts as_yaml(instance_config)
2002
+ # todo: iterate over
2003
+ # instance_config["groups"][group_name]["clouds"][cloud_name]
2004
+ end
2005
+
2006
+ print cyan
2007
+ if tier_config['bootOrder']
2008
+ puts "Boot Order: #{tier_config['bootOrder']}"
2009
+ end
2010
+ if tier_config['linkedTiers'] && !tier_config['linkedTiers'].empty?
2011
+ puts "Connected Tiers: #{tier_config['linkedTiers'].join(', ')}"
2012
+ end
2013
+
2014
+ else
2015
+ print white," Tier is empty, see `app-templates add-instance \"#{blueprint['name']}\" \"#{tier_name}\"`",reset,"\n"
2016
+ end
2017
+ # print "\n"
2018
+
2019
+ end
2020
+ # print "\n"
2021
+
2022
+ else
2023
+ print white,"\nTemplate is empty, see `app-templates add-tier \"#{blueprint['name']}\"`",reset,"\n"
2024
+ end
2025
+ end
2026
+
2027
+ # this parses the environments => groups => clouds tree structure
2028
+ # and returns a list of objects like {scope: {group:'thegroup'}, config: Map}
2029
+ # this would be be better as a recursive function, brute forced for now.
2030
+ def parse_scoped_instance_configs(instance_config)
2031
+ config_list = []
2032
+ if instance_config['environments'] && instance_config['environments'].keys.size > 0
2033
+ instance_config['environments'].each do |env_name, env_config|
2034
+ if env_config['groups']
2035
+ env_config['groups'].each do |group_name, group_config|
2036
+ if group_config['clouds'] && !group_config['clouds'].empty?
2037
+ group_config['clouds'].each do |cloud_name, cloud_config|
2038
+ config_list << {config: cloud_config, scope: {environment: env_name, group: group_name, cloud: cloud_name}}
2039
+ end
2040
+ end
2041
+ if (!group_config['clouds'] || group_config['clouds'].empty?)
2042
+ config_list << {config: group_config, scope: {environment: env_name, group: group_name}}
2043
+ end
2044
+ end
2045
+ end
2046
+ if env_config['clouds'] && !env_config['clouds'].empty?
2047
+ env_config['clouds'].each do |cloud_name, cloud_config|
2048
+ config_list << {config: cloud_config, scope: {environment: env_name, cloud: cloud_name}}
2049
+ end
2050
+ end
2051
+ if (!env_config['groups'] || env_config['groups'].empty?) && (!env_config['clouds'] || env_config['clouds'].empty?)
2052
+ config_list << {config: env_config, scope: {environment: env_name}}
2053
+ end
2054
+ end
2055
+ end
2056
+ if instance_config['groups']
2057
+ instance_config['groups'].each do |group_name, group_config|
2058
+ if group_config['clouds'] && !group_config['clouds'].empty?
2059
+ group_config['clouds'].each do |cloud_name, cloud_config|
2060
+ config_list << {config: cloud_config, scope: {group: group_name, cloud: cloud_name}}
2061
+ end
2062
+ end
2063
+ if (!group_config['clouds'] || group_config['clouds'].empty?)
2064
+ config_list << {config: group_config, scope: {group: group_name}}
2065
+ end
2066
+ end
2067
+ end
2068
+ if instance_config['clouds']
2069
+ instance_config['clouds'].each do |cloud_name, cloud_config|
2070
+ config_list << {config: cloud_config, scope: {cloud: cloud_name}}
2071
+ end
2072
+ end
2073
+ return config_list
2074
+ end
2075
+
2076
+ def link_tiers(tiers, tier_names)
2077
+ # tiers = blueprint["config"]["tiers"]
2078
+ tier_names = [tier_names].flatten.collect {|it| it }.compact.uniq
2079
+ if !tiers
2080
+ print_red_alert "No tiers found for template"
2081
+ return false
2082
+ end
2083
+
2084
+ existing_tier_names = tiers.keys
2085
+ matching_tier_names = tier_names.map {|tier_name|
2086
+ existing_tier_names.find {|k| k.downcase == tier_name.downcase }
2087
+ }.compact
2088
+ if matching_tier_names.size != tier_names.size
2089
+ print_red_alert "Template does not contain tiers: '#{tier_names}'"
2090
+ return false
2091
+ end
2092
+ matching_tier_names.each do |tier_name|
2093
+ tier = tiers[tier_name]
2094
+ tier['linkedTiers'] ||= []
2095
+ other_tier_names = matching_tier_names.select {|it| tier_name != it}
2096
+ other_tier_names.each do |other_tier_name|
2097
+ if !tier['linkedTiers'].include?(other_tier_name)
2098
+ tier['linkedTiers'].push(other_tier_name)
2099
+ end
2100
+ end
2101
+ end
2102
+ return true
2103
+ end
2104
+
2105
+ def unlink_tiers(tiers, tier_names)
2106
+ # tiers = blueprint["config"]["tiers"]
2107
+ tier_names = [tier_names].flatten.collect {|it| it }.compact.uniq
2108
+ if !tiers
2109
+ print_red_alert "No tiers found for template"
2110
+ return false
2111
+ end
2112
+
2113
+ existing_tier_names = tiers.keys
2114
+ matching_tier_names = tier_names.map {|tier_name|
2115
+ existing_tier_names.find {|k| k.downcase == tier_name.downcase }
2116
+ }.compact
2117
+ if matching_tier_names.size != tier_names.size
2118
+ print_red_alert "Template does not contain tiers: '#{tier_names}'"
2119
+ return false
2120
+ end
2121
+ matching_tier_names.each do |tier_name|
2122
+ tier = tiers[tier_name]
2123
+ tier['linkedTiers'] ||= []
2124
+ other_tier_names = matching_tier_names.select {|it| tier_name != it}
2125
+ other_tier_names.each do |other_tier_name|
2126
+ if tier['linkedTiers'].include?(other_tier_name)
2127
+ tier['linkedTiers'] = tier['linkedTiers'].reject {|it| it == other_tier_name }
2128
+ end
2129
+ end
2130
+ end
2131
+ return true
2132
+ end
2133
+
2134
+ end