morpheus-cli 3.5.2 → 3.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/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