morpheus-cli 5.0.2 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1474 @@
1
+ require 'morpheus/cli/cli_command'
2
+
3
+ # CLI command for the Service Catalog (Persona): Dashboard / Catalog / Inventory
4
+ # Inventory Items are the main actions, list, get, remove
5
+ # The add command adds to the cart and checkout places an order with the cart.
6
+ # The add-order command allows submitting a new order at once.
7
+ class Morpheus::Cli::CatalogCommand
8
+ include Morpheus::Cli::CliCommand
9
+ include Morpheus::Cli::ProvisioningHelper
10
+ include Morpheus::Cli::OptionSourceHelper
11
+
12
+ # set_command_name :'service-catalog'
13
+ set_command_name :'catalog'
14
+ set_command_description "Service Catalog Persona: View catalog and manage inventory"
15
+
16
+ # dashboard
17
+ register_subcommands :dashboard
18
+ # catalog (catalogItemTypes)
19
+ register_subcommands :'list-types' => :list_types
20
+ register_subcommands :'get-type' => :get_type
21
+ alias_subcommand :types, :'list-types'
22
+
23
+ # inventory (items) IS the main crud here
24
+ register_subcommands :list, :get, :remove
25
+
26
+ # cart / orders
27
+ register_subcommands :cart => :get_cart
28
+ register_subcommands :'update-cart' => :update_cart
29
+ register_subcommands :add
30
+ #register_subcommands :'update-cart-item' => :update_cart_item
31
+ register_subcommands :'remove-cart-item' => :remove_cart_item
32
+ register_subcommands :'clear-cart' => :clear_cart
33
+ register_subcommands :checkout
34
+
35
+ # create and submit cart in one action
36
+ # maybe call this place-order instead?
37
+ register_subcommands :'add-order' => :add_order
38
+
39
+ def default_sigdig
40
+ 4
41
+ end
42
+
43
+ def connect(opts)
44
+ @api_client = establish_remote_appliance_connection(opts)
45
+ @service_catalog_interface = @api_client.catalog
46
+ @instances_interface = @api_client.instances
47
+ @servers_interface = @api_client.servers # should not be required here!
48
+ @option_types_interface = @api_client.option_types
49
+ end
50
+
51
+ def handle(args)
52
+ handle_subcommand(args)
53
+ end
54
+
55
+ def dashboard(args)
56
+ params = {}
57
+ options = {}
58
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
59
+ opts.banner = subcommand_usage()
60
+ opts.add_hidden_option('--sigdig')
61
+ build_standard_get_options(opts, options, [:sigdig] || default_sigdig)
62
+ opts.footer = <<-EOT
63
+ View service catalog dashboard.
64
+ Provides an overview of available catalog item types, recent orders and inventory.
65
+ EOT
66
+ end
67
+ optparse.parse!(args)
68
+ verify_args!(args:args, optparse:optparse, max:0)
69
+ connect(options)
70
+
71
+ params.merge!(parse_list_options(options))
72
+ @service_catalog_interface.setopts(options)
73
+ if options[:dry_run]
74
+ print_dry_run @service_catalog_interface.dry.dashboard(params)
75
+ return
76
+ end
77
+ json_response = @service_catalog_interface.dashboard(params)
78
+ catalog_item_types = json_response['catalogItemTypes']
79
+ catalog_meta = json_response['catalogMeta'] || {}
80
+ recent_items = json_response['recentItems'] || {}
81
+ featured_items = json_response['featuredItems'] || []
82
+ inventory_items = json_response['inventoryItems'] || []
83
+ inventory_meta = json_response['inventoryMeta'] || {}
84
+ cart = json_response['cart'] || {}
85
+ cart_items = cart['items'] || []
86
+ cart_stats = cart['stats'] || {}
87
+ current_invoice = json_response['currentInvoice']
88
+
89
+ render_response(json_response, options, catalog_item_type_object_key) do
90
+ print_h1 "Catalog Dashboard", [], options
91
+ print cyan
92
+
93
+ # dashboard_columns = [
94
+ # {"TYPES" => lambda {|it| catalog_meta['total'] } },
95
+ # {"INVENTORY" => lambda {|it| inventory_items.size rescue '' } },
96
+ # {"CART" => lambda {|it| it['cart']['items'].size rescue '' } },
97
+ # ]
98
+ # print as_pretty_table([json_response], dashboard_columns, options)
99
+
100
+ print_h2 "Catalog Items"
101
+ print as_pretty_table(catalog_item_types, {
102
+ "NAME" => lambda {|it| it['name'] },
103
+ "DESCRIPTION" => lambda {|it| it['description'] },
104
+ "FEATURED" => lambda {|it| format_boolean it['featured'] },
105
+ }, options)
106
+ # print reset,"\n"
107
+
108
+ if recent_items && recent_items.size() > 0
109
+ print_h2 "Recently Ordered"
110
+ print as_pretty_table(recent_items, {
111
+ #"ID" => lambda {|it| it['id'] },
112
+ #"NAME" => lambda {|it| it['name'] },
113
+ "TYPE" => lambda {|it| it['type']['name'] rescue '' },
114
+ #"QTY" => lambda {|it| it['quantity'] },
115
+ "ORDER DATE" => lambda {|it| format_local_dt(it['orderDate']) },
116
+ # "STATUS" => lambda {|it| format_catalog_item_status(it) },
117
+ # "CONFIG" => lambda {|it| truncate_string(format_name_values(it['config']), 50) },
118
+ }, options)
119
+ # print reset,"\n"
120
+ end
121
+
122
+ if recent_items && recent_items.size() > 0
123
+ print_h2 "Inventory"
124
+ print as_pretty_table(inventory_items, {
125
+ "ID" => lambda {|it| it['id'] },
126
+ "NAME" => lambda {|it| it['name'] },
127
+ "TYPE" => lambda {|it| it['type']['name'] rescue '' },
128
+ #"QTY" => lambda {|it| it['quantity'] },
129
+ "ORDER DATE" => lambda {|it| format_local_dt(it['orderDate']) },
130
+ "STATUS" => lambda {|it| format_catalog_item_status(it) },
131
+ # "CONFIG" => lambda {|it| format_name_values(it['config']) },
132
+ }, options)
133
+ print_results_pagination(inventory_meta)
134
+ else
135
+ # print_h2 "Inventory"
136
+ # print cyan, "Inventory is empty", reset, "\n"
137
+ end
138
+
139
+ # print reset,"\n"
140
+
141
+ # problematic right now, invoice has all user activity, not just catalog
142
+ show_invoice = false
143
+ if current_invoice && show_invoice
144
+ print_h2 "Current Invoice"
145
+ print cyan
146
+ invoice_columns = {
147
+ # todo: invoice needs to return a currency!!!
148
+ "Compute" => lambda {|it| format_money(it['computePrice'], cart_stats['currency'], {sigdig:options[:sigdig] || default_sigdig}) },
149
+ "Storage" => lambda {|it| format_money(it['storagePrice'], cart_stats['currency'], {sigdig:options[:sigdig] || default_sigdig}) },
150
+ "Memory" => lambda {|it| format_money(it['memoryPrice'], cart_stats['currency'], {sigdig:options[:sigdig] || default_sigdig}) },
151
+ "Network" => lambda {|it| format_money(it['networkPrice'], cart_stats['currency'], {sigdig:options[:sigdig] || default_sigdig}) },
152
+ "Extra" => lambda {|it| format_money(it['extraPrice'], cart_stats['currency'], {sigdig:options[:sigdig] || default_sigdig}) },
153
+ "MTD" => lambda {|it| format_money(it['runningPrice'], cart_stats['currency'], {sigdig:options[:sigdig] || default_sigdig}) },
154
+ "Total (Projected)" => lambda {|it| format_money(it['totalPrice'], cart_stats['currency'], {sigdig:options[:sigdig] || default_sigdig}) },
155
+ #"Items" => lambda {|it| cart['items'].size },
156
+ # "Created" => lambda {|it| format_local_dt(it['dateCreated']) },
157
+ # "Updated" => lambda {|it| format_local_dt(it['lastUpdated']) },
158
+ }
159
+ invoice_columns.delete("Storage") unless current_invoice['storagePrice'] && current_invoice['storagePrice'].to_f > 0
160
+ invoice_columns.delete("Memory") unless current_invoice['memoryPrice'] && current_invoice['memoryPrice'].to_f > 0
161
+ invoice_columns.delete("Network") unless current_invoice['networkPrice'] && current_invoice['networkPrice'].to_f > 0
162
+ invoice_columns.delete("Extra") unless current_invoice['extraPrice'] && current_invoice['extraPrice'].to_f > 0
163
+ print as_pretty_table(current_invoice, invoice_columns.upcase_keys!, options)
164
+ end
165
+
166
+ show_cart = cart && cart['items'] && cart['items'].size() > 0
167
+ if show_cart
168
+ if cart
169
+
170
+ # get_cart([] + (options[:remote] ? ["-r",options[:remote]] : []))
171
+
172
+ print_h2 "Cart"
173
+ print cyan
174
+ if cart['items'].size() > 0
175
+ # cart_columns = {
176
+ # "Qty" => lambda {|it| cart['items'].sum {|cart_item| cart_item['quantity'] } },
177
+ # "Total" => lambda {|it|
178
+ # begin
179
+ # format_money(cart_stats['price'], cart_stats['currency'], {sigdig:options[:sigdig] || default_sigdig}) + (cart_stats['unit'].to_s.empty? ? "" : " / #{cart_stats['unit'] || 'month'}")
180
+ # rescue => ex
181
+ # raise ex
182
+ # # no cart stats eh?
183
+ # end
184
+ # },
185
+ # }
186
+ # print as_pretty_table(cart, cart_columns.upcase_keys!, options)
187
+
188
+
189
+ cart_item_columns = [
190
+ {"ID" => lambda {|it| it['id'] } },
191
+ #{"NAME" => lambda {|it| it['name'] } },
192
+ {"TYPE" => lambda {|it| it['type']['name'] rescue '' } },
193
+ #{"QTY" => lambda {|it| it['quantity'] } },
194
+ {"PRICE" => lambda {|it| it['price'] ? format_money(it['price'] , it['currency'], {sigdig:options[:sigdig] || default_sigdig}) : "No pricing configured" } },
195
+ {"STATUS" => lambda {|it|
196
+ status_string = format_catalog_item_status(it)
197
+ if it['errorMessage'].to_s != ""
198
+ status_string << " - #{it['errorMessage']}"
199
+ end
200
+ status_string
201
+ } },
202
+ # {"CONFIG" => lambda {|it|
203
+ # truncate_string(format_name_values(it['config']), 50)
204
+ # } },
205
+ ]
206
+ print as_pretty_table(cart_items, cart_item_columns)
207
+
208
+ print reset,"\n"
209
+ print cyan
210
+ if cart_stats['price']
211
+ puts "Total: " + format_money(cart_stats['price'], cart_stats['currency'], {sigdig:options[:sigdig] || default_sigdig}) + " / #{cart_stats['unit'].to_s.empty? ? 'month' : cart_stats['unit']}"
212
+ else
213
+ puts "Total: " + "No pricing configured"
214
+ end
215
+ # print reset,"\n"
216
+
217
+ else
218
+ print cyan, "Cart is empty", reset, "\n"
219
+ end
220
+ end
221
+
222
+ end
223
+
224
+ print reset,"\n"
225
+
226
+ end
227
+ return 0, nil
228
+ end
229
+
230
+ def list_types(args)
231
+ options = {}
232
+ params = {}
233
+ ref_ids = []
234
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
235
+ opts.banner = subcommand_usage("[search]")
236
+ opts.on( '--featured [on|off]', String, "Filter by featured" ) do |val|
237
+ params['featured'] = (val.to_s != 'false' && val.to_s != 'off')
238
+ end
239
+ build_standard_list_options(opts, options, [:sigdig])
240
+ opts.footer = "List available catalog item types."
241
+ end
242
+ optparse.parse!(args)
243
+ connect(options)
244
+ # verify_args!(args:args, optparse:optparse, count:0)
245
+ if args.count > 0
246
+ options[:phrase] = args.join(" ")
247
+ end
248
+ params.merge!(parse_list_options(options))
249
+ @service_catalog_interface.setopts(options)
250
+ if options[:dry_run]
251
+ print_dry_run @service_catalog_interface.dry.list_types(params)
252
+ return
253
+ end
254
+ json_response = @service_catalog_interface.list_types(params)
255
+ catalog_item_types = json_response[catalog_item_type_list_key]
256
+ render_response(json_response, options, catalog_item_type_list_key) do
257
+ print_h1 "Morpheus Catalog Types", parse_list_subtitles(options), options
258
+ if catalog_item_types.empty?
259
+ print cyan,"No catalog item types found.",reset,"\n"
260
+ else
261
+ list_columns = catalog_item_type_column_definitions.upcase_keys!
262
+ #list_columns["Config"] = lambda {|it| truncate_string(it['config'], 100) }
263
+ print as_pretty_table(catalog_item_types, list_columns.upcase_keys!, options)
264
+ print_results_pagination(json_response)
265
+ end
266
+ print reset,"\n"
267
+ end
268
+ if catalog_item_types.empty?
269
+ return 1, "no catalog item types found"
270
+ else
271
+ return 0, nil
272
+ end
273
+ end
274
+
275
+ def get_type(args)
276
+ params = {}
277
+ options = {}
278
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
279
+ opts.banner = subcommand_usage("[name]")
280
+ build_standard_get_options(opts, options, [:sigdig])
281
+ opts.footer = <<-EOT
282
+ Get details about a specific catalog item type.
283
+ [name] is required. This is the name or id of a catalog item type.
284
+ EOT
285
+ end
286
+ optparse.parse!(args)
287
+ verify_args!(args:args, optparse:optparse, min:1)
288
+ connect(options)
289
+ id_list = parse_id_list(args)
290
+ return run_command_for_each_arg(id_list) do |arg|
291
+ _get_type(arg, params, options)
292
+ end
293
+ end
294
+
295
+ def _get_type(id, params, options)
296
+ catalog_item_type = nil
297
+ if id.to_s !~ /\A\d{1,}\Z/
298
+ catalog_item_type = find_catalog_item_type_by_name(id)
299
+ return 1, "catalog item type not found for #{id}" if catalog_item_type.nil?
300
+ id = catalog_item_type['id']
301
+ end
302
+ @service_catalog_interface.setopts(options)
303
+ if options[:dry_run]
304
+ print_dry_run @service_catalog_interface.dry.get_type(id, params)
305
+ return
306
+ end
307
+ json_response = @service_catalog_interface.get_type(id, params)
308
+ catalog_item_type = json_response[catalog_item_type_object_key]
309
+ # need to load by id to get optionTypes
310
+ # maybe do ?name=foo&includeOptionTypes=true
311
+ if catalog_item_type['optionTypes'].nil?
312
+ catalog_item_type = find_catalog_item_type_by_id(catalog_item_type['id'])
313
+ return [1, "catalog item type not found"] if catalog_item_type.nil?
314
+ end
315
+ render_response(json_response, options, catalog_item_type_object_key) do
316
+ print_h1 "Catalog Type Details", [], options
317
+ print cyan
318
+ show_columns = catalog_item_type_column_definitions
319
+ print_description_list(show_columns, catalog_item_type)
320
+
321
+ if catalog_item_type['optionTypes'] && catalog_item_type['optionTypes'].size > 0
322
+ print_h2 "Configuration Options"
323
+ print as_pretty_table(catalog_item_type['optionTypes'], {
324
+ "LABEL" => lambda {|it| it['fieldLabel'] },
325
+ "NAME" => lambda {|it| it['fieldName'] },
326
+ "TYPE" => lambda {|it| it['type'] },
327
+ "REQUIRED" => lambda {|it| format_boolean it['required'] },
328
+ })
329
+ else
330
+ # print cyan,"No option types found for this catalog item.","\n",reset
331
+ end
332
+
333
+ print reset,"\n"
334
+ end
335
+ return 0, nil
336
+ end
337
+
338
+ # inventory actions
339
+
340
+ def list(args)
341
+ options = {}
342
+ params = {}
343
+ ref_ids = []
344
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
345
+ opts.banner = subcommand_usage("[search]")
346
+ opts.on('-t', '--type TYPE', String, "Catalog Item Type Name or ID") do |val|
347
+ type_id = val.to_s
348
+ end
349
+ build_standard_list_options(opts, options, [:sigdig])
350
+ opts.footer = "List catalog inventory."
351
+ end
352
+ optparse.parse!(args)
353
+ connect(options)
354
+ # verify_args!(args:args, optparse:optparse, count:0)
355
+ if args.count > 0
356
+ options[:phrase] = args.join(" ")
357
+ end
358
+ params.merge!(parse_list_options(options))
359
+ @service_catalog_interface.setopts(options)
360
+ if options[:dry_run]
361
+ print_dry_run @service_catalog_interface.dry.list_inventory(params)
362
+ return
363
+ end
364
+ json_response = @service_catalog_interface.list_inventory(params)
365
+ catalog_items = json_response[catalog_item_list_key]
366
+ render_response(json_response, options, catalog_item_list_key) do
367
+ print_h1 "Morpheus Catalog Inventory", parse_list_subtitles(options), options
368
+ if catalog_items.empty?
369
+ print cyan,"No catalog items found.",reset,"\n"
370
+ else
371
+ list_columns = catalog_item_column_definitions.upcase_keys!
372
+ #list_columns["Config"] = lambda {|it| truncate_string(it['config'], 100) }
373
+ print as_pretty_table(catalog_items, list_columns.upcase_keys!, options)
374
+ print_results_pagination(json_response)
375
+ end
376
+ print reset,"\n"
377
+ end
378
+ if catalog_items.empty?
379
+ return 1, "no catalog items found"
380
+ else
381
+ return 0, nil
382
+ end
383
+ end
384
+
385
+ def get(args)
386
+ params = {}
387
+ options = {}
388
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
389
+ opts.banner = subcommand_usage("[item]")
390
+ opts.on( '-c', '--config', "Display raw config only. Default is YAML. Combine with -j for JSON instead." ) do
391
+ options[:show_config] = true
392
+ end
393
+ # opts.on('--no-config', "Do not display config content." ) do
394
+ # options[:no_config] = true
395
+ # end
396
+ build_standard_get_options(opts, options, [:sigdig])
397
+ opts.footer = <<-EOT
398
+ Get details about a specific catalog inventory item.
399
+ [item] is required. This is the name or id of a catalog inventory item.
400
+ EOT
401
+ end
402
+ optparse.parse!(args)
403
+ verify_args!(args:args, optparse:optparse, min:1)
404
+ connect(options)
405
+ id_list = parse_id_list(args)
406
+ return run_command_for_each_arg(id_list) do |arg|
407
+ _get(arg, params, options)
408
+ end
409
+ end
410
+
411
+ def _get(id, params, options)
412
+ catalog_item = find_catalog_item_by_name_or_id(id)
413
+ return 1, "catalog item not found for name or id '#{id}'" if catalog_item.nil?
414
+ @service_catalog_interface.setopts(options)
415
+ if options[:dry_run]
416
+ print_dry_run @service_catalog_interface.dry.get_inventory(catalog_item['id'], params)
417
+ return
418
+ end
419
+ # skip redundant request
420
+ json_response = {catalog_item_object_key => catalog_item}
421
+ # if id !~ /\A\d{1,}\Z/
422
+ # json_response = @service_catalog_interface.get_inventory(catalog_item['id'], params)
423
+ # end
424
+ catalog_item = json_response[catalog_item_object_key]
425
+ item_config = catalog_item['config']
426
+ item_type_code = catalog_item['type']['type'].to_s.downcase rescue nil
427
+ item_instance = catalog_item['instance']
428
+ item_app = catalog_item['app']
429
+ item_execution = catalog_item['execution']
430
+ render_response(json_response, options, catalog_item_object_key) do
431
+ print_h1 "Catalog Item Details", [], options
432
+ print cyan
433
+ show_columns = catalog_item_column_definitions
434
+ # show_columns.delete("Status") if catalog_item['status'].to_s.lowercase == 'ORDERED'
435
+ show_columns.delete("Status") if item_instance || item_app || item_execution
436
+ print_description_list(show_columns, catalog_item)
437
+
438
+ if item_config && !item_config.empty?
439
+ # print_h2 "Configuration", options
440
+ # print cyan
441
+ # print as_description_list(item_config, item_config.keys, options)
442
+ # print "\n", reset
443
+ end
444
+
445
+ if item_type_code == 'instance'
446
+ if item_instance
447
+ print_h2 "Instance", options
448
+ print cyan
449
+ item_instance_columns = [
450
+ {"ID" => lambda {|it| it['id'] } },
451
+ {"NAME" => lambda {|it| it['name'] } },
452
+ {"STATUS" => lambda {|it| format_instance_status(it) } },
453
+ ]
454
+ #print as_description_list(item_instance, item_instance_columns, options)
455
+ print as_pretty_table([item_instance], item_instance_columns, options)
456
+ # print "\n", reset
457
+ else
458
+ print "\n"
459
+ print yellow, "No instance found", reset, "\n"
460
+ end
461
+ end
462
+
463
+ if item_type_code == 'app' || item_type_code == 'blueprint' || item_type_code == 'apptemplate'
464
+ if item_app
465
+ print_h2 "App", options
466
+ print cyan
467
+ item_app_columns = [
468
+ {"ID" => lambda {|it| it['id'] } },
469
+ {"NAME" => lambda {|it| it['name'] } },
470
+ {"STATUS" => lambda {|it| format_app_status(it) } },
471
+ ]
472
+ #print as_description_list(item_app, item_app_columns, options)
473
+ print as_pretty_table([item_app], item_app_columns, options)
474
+ # print "\n", reset
475
+ else
476
+ print "\n"
477
+ print yellow, "No app found", reset, "\n"
478
+ end
479
+ end
480
+
481
+ if item_type_code == 'workflow' || item_type_code == 'operationalworkflow' || item_type_code == 'taskset'
482
+ if item_execution
483
+ print_h2 "Workflow Results", options
484
+ print cyan
485
+ item_workflow_columns = [
486
+ {"EXECUTION ID" => lambda {|it| item_execution ? item_execution['id'] : '' } },
487
+ {"CONTEXT TYPE" => lambda {|it| it['name'] } },
488
+ {"RESOURCE" => lambda {|it| (it['targets'] ? it['targets'].collect { |target| target['name'] }.join(', ') : '') rescue '' } },
489
+ {"STATUS" => lambda {|it| item_execution ? format_job_execution_status(item_execution) : 'N/A' } },
490
+ ]
491
+ #print as_description_list(catalog_item, item_workflow_columns, options)
492
+ print as_pretty_table([catalog_item], item_workflow_columns, options)
493
+ # print "\n", reset
494
+ else
495
+ print "\n"
496
+ print yellow, "No execution found", reset, "\n"
497
+ end
498
+ end
499
+
500
+ print reset,"\n"
501
+ end
502
+ return 0, nil
503
+ end
504
+
505
+ def get_cart(args)
506
+ params = {}
507
+ options = {}
508
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
509
+ opts.banner = subcommand_usage()
510
+ opts.on('-a', '--details', "Display all details: item configuration." ) do
511
+ options[:details] = true
512
+ end
513
+ build_standard_get_options(opts, options, [:sigdig])
514
+ opts.footer = <<-EOT
515
+ Get details of current cart and the items in it.
516
+ Exits non-zero if cart is empty.
517
+ EOT
518
+ end
519
+ optparse.parse!(args)
520
+ verify_args!(args:args, optparse:optparse, count:0)
521
+ connect(options)
522
+
523
+ @service_catalog_interface.setopts(options)
524
+ if options[:dry_run]
525
+ print_dry_run @service_catalog_interface.dry.get_cart(params)
526
+ return 0, nil
527
+ end
528
+ # skip extra query, list has same data as show right now
529
+ json_response = @service_catalog_interface.get_cart(params)
530
+ cart = json_response['cart']
531
+ cart_items = cart['items'] || []
532
+ cart_stats = cart['stats'] || {}
533
+ render_response(json_response, options, 'cart') do
534
+ print_h1 "Catalog Cart", [], options
535
+ print_order_details(cart, options)
536
+ end
537
+ if cart_items.empty?
538
+ return 1, "cart is empty"
539
+ else
540
+ return 0, nil
541
+ end
542
+ end
543
+
544
+ def update_cart(args)
545
+ options = {}
546
+ params = {}
547
+ payload = {}
548
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
549
+ opts.banner = subcommand_usage("--name [name]")
550
+ opts.on('--name [NAME]', String, "Set an optional name for your catalog order") do |val|
551
+ options[:options]['name'] = val.to_s
552
+ end
553
+ build_standard_update_options(opts, options, [:sigdig])
554
+ opts.footer = <<-EOT
555
+ Update your cart settings, such as name.
556
+ EOT
557
+ end
558
+ optparse.parse!(args)
559
+ verify_args!(args:args, optparse:optparse, count:0)
560
+ connect(options)
561
+ # fetch current cart
562
+ # cart = @service_catalog_interface.get_cart()['cart']
563
+ payload = {}
564
+ update_cart_object_key = 'order'
565
+ if options[:payload]
566
+ payload = options[:payload]
567
+ payload.deep_merge!({update_cart_object_key => parse_passed_options(options)})
568
+ else
569
+ payload.deep_merge!({update_cart_object_key => parse_passed_options(options)})
570
+ payload.deep_merge!({update_cart_object_key => params})
571
+ if payload[update_cart_object_key].empty? # || options[:no_prompt]
572
+ raise_command_error "Specify at least one option to update.\n#{optparse}"
573
+ end
574
+ end
575
+ @service_catalog_interface.setopts(options)
576
+ if options[:dry_run]
577
+ print_dry_run @service_catalog_interface.dry.update_cart(payload)
578
+ return
579
+ end
580
+ json_response = @service_catalog_interface.update_cart(payload)
581
+ #cart = json_response['cart']
582
+ #cart = @service_catalog_interface.get_cart()['cart']
583
+ render_response(json_response, options, 'cart') do
584
+ print_green_success "Updated cart"
585
+ get_cart([] + (options[:remote] ? ["-r",options[:remote]] : []))
586
+ end
587
+ return 0, nil
588
+ end
589
+
590
+ def add(args)
591
+ options = {}
592
+ params = {}
593
+ payload = {}
594
+ type_id = nil
595
+ workflow_context = nil
596
+ workflow_target = nil
597
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
598
+ opts.banner = subcommand_usage("[type] [options]")
599
+ opts.on('-t', '--type TYPE', String, "Catalog Item Type Name or ID") do |val|
600
+ type_id = val.to_s
601
+ end
602
+ opts.on('--validate','--validate', "Validate Only. Validates the configuration and skips adding the item.") do
603
+ options[:validate_only] = true
604
+ end
605
+ opts.on('--context [instance|server]', String, "Context Type for operational workflow types") do |val|
606
+ workflow_context = val.to_s
607
+ end
608
+ opts.on('--target ID', String, "Target Resource (Instance or Server) for operational workflow types") do |val|
609
+ workflow_target = val.to_s
610
+ end
611
+ opts.add_hidden_option('--sigdig')
612
+ build_standard_update_options(opts, options, [:sigdig])
613
+ opts.footer = <<-EOT
614
+ Add an item to your cart
615
+ [type] is required, this is name or id of a catalog item type.
616
+ Catalog item types may require additional configuration.
617
+ EOT
618
+ end
619
+ optparse.parse!(args)
620
+ verify_args!(args:args, optparse:optparse, min:0)
621
+ connect(options)
622
+ if args.count > 0
623
+ type_id = args.join(" ")
624
+ end
625
+ payload = {}
626
+ add_item_object_key = 'item'
627
+ payload = {add_item_object_key => {} }
628
+ if options[:payload]
629
+ payload = options[:payload]
630
+ payload.deep_merge!({add_item_object_key => parse_passed_options(options)})
631
+ else
632
+ payload.deep_merge!({add_item_object_key => parse_passed_options(options)})
633
+ # prompt for Type
634
+ if type_id
635
+ catalog_item_type = find_catalog_item_type_by_name_or_id(type_id)
636
+ return [1, "catalog item type not found"] if catalog_item_type.nil?
637
+ elsif
638
+ catalog_type_option_type = {'fieldName' => 'type', 'fieldLabel' => 'Type', 'type' => 'select', 'optionSource' => lambda { |api_client, api_params|
639
+ # @options_interface.options_for_source("licenseTypes", {})['data']
640
+ @service_catalog_interface.list_types({max:10000})['catalogItemTypes'].collect {|it|
641
+ {'name' => it['name'], 'value' => it['id']}
642
+ } }, 'required' => true, 'description' => 'Catalog Item Type name or id'}
643
+ type_id = Morpheus::Cli::OptionTypes.prompt([catalog_type_option_type], options[:options], @api_client, options[:params])['type']
644
+ catalog_item_type = find_catalog_item_type_by_name_or_id(type_id.to_s)
645
+ return [1, "catalog item type not found"] if catalog_item_type.nil?
646
+ end
647
+ # use name instead of id
648
+ payload[add_item_object_key]['type'] = {'name' => catalog_item_type['name']}
649
+ #payload[add_item_object_key]['type'] = {'id' => catalog_item_type['id']}
650
+
651
+ # this is silly, need to load by id to get optionTypes
652
+ # maybe do ?name=foo&includeOptionTypes=true
653
+ if catalog_item_type['optionTypes'].nil?
654
+ catalog_item_type = find_catalog_item_type_by_id(catalog_item_type['id'])
655
+ return [1, "catalog item type not found"] if catalog_item_type.nil?
656
+ end
657
+ catalog_option_types = catalog_item_type['optionTypes']
658
+ # instead of config.customOptions just use config...
659
+ catalog_option_types = catalog_option_types.collect {|it|
660
+ it['fieldContext'] = 'config'
661
+ it
662
+ }
663
+ if catalog_option_types && !catalog_option_types.empty?
664
+ config_prompt = Morpheus::Cli::OptionTypes.prompt(catalog_option_types, options[:options], @api_client, {})['config']
665
+ payload[add_item_object_key].deep_merge!({'config' => config_prompt})
666
+ end
667
+ if workflow_context
668
+ payload[add_item_object_key]['context'] = workflow_context
669
+ else
670
+ # the catalog item type determines if context selection is required
671
+ # only blank string means you can choose? err
672
+ if catalog_item_type['context'] == ''
673
+ context_option_type = {'fieldName' => 'context', 'fieldLabel' => 'Context Type', 'type' => 'select', 'optionSource' => lambda { |api_client, api_params|
674
+ [{'name' => "none", 'value' => "appliance"}, {'name' => "Instance", 'value' => "instance"}, {'name' => "Server", 'value' => "server"}]
675
+ }, 'required' => true, 'description' => 'Context for operational workflow, determines target type', 'defaultValue' => 'instance'}
676
+ workflow_context = Morpheus::Cli::OptionTypes.prompt([context_option_type], options[:options], @api_client, options[:params])['context']
677
+ elsif !catalog_item_type['context'].nil?
678
+ workflow_context = catalog_item_type['context']
679
+ end
680
+ payload[add_item_object_key]['context'] = workflow_context
681
+ end
682
+
683
+ if workflow_target
684
+ payload[add_item_object_key]['targets'] = [{id: workflow_target}]
685
+ else
686
+ # prompt for Resource (target)
687
+ if workflow_context == 'instance'
688
+ target_option_type = {'fieldName' => 'target', 'fieldLabel' => 'Resource (Instance)', 'type' => 'select', 'optionSource' => lambda { |api_client, api_params|
689
+ # todo: @instances_interface should not be required here
690
+ # @options_interface.options_for_source("instances", {})['data']
691
+ @instances_interface.list({max:10000})['instances'].collect {|it|
692
+ {'name' => it['name'], 'value' => it['id']}
693
+ } }, 'required' => true, 'description' => 'Target Instance'}
694
+ workflow_target = Morpheus::Cli::OptionTypes.prompt([target_option_type], options[:options], @api_client, options[:params])['target']
695
+ payload[add_item_object_key]['targets'] = [{id: workflow_target}]
696
+ payload[add_item_object_key].delete('target')
697
+ elsif workflow_context == 'server'
698
+ target_option_type = {'fieldName' => 'target', 'fieldLabel' => 'Resource (Server)', 'type' => 'select', 'optionSource' => lambda { |api_client, api_params|
699
+ # todo: @servers_interface should not be required here
700
+ # @options_interface.options_for_source("searchServers", {})['data']
701
+ @servers_interface.list({max:10000})['servers'].collect {|it|
702
+ {'name' => it['name'], 'value' => it['id']}
703
+ } }, 'required' => true, 'description' => 'Target Server'}
704
+ workflow_target = Morpheus::Cli::OptionTypes.prompt([target_option_type], options[:options], @api_client, options[:params])['target']
705
+ payload[add_item_object_key]['targets'] = [{id: workflow_target}]
706
+ payload[add_item_object_key].delete('target')
707
+ end
708
+ end
709
+ end
710
+ if options[:validate_only]
711
+ params['validate'] = true
712
+ end
713
+ @service_catalog_interface.setopts(options)
714
+ if options[:dry_run]
715
+ print_dry_run @service_catalog_interface.dry.create_cart_item(payload, params)
716
+ return
717
+ end
718
+ json_response = @service_catalog_interface.create_cart_item(payload, params)
719
+ cart_item = json_response['item']
720
+ render_response(json_response, options) do
721
+ if options[:validate_only]
722
+ if json_response['success']
723
+ print_h2 "Validated Cart Item", [], options
724
+ cart_item_columns = {
725
+ "Type" => lambda {|it| it['type']['name'] rescue '' },
726
+ #"Qty" => lambda {|it| it['quantity'] },
727
+ "Price" => lambda {|it| it['price'] ? format_money(it['price'] , it['currency'], {sigdig:options[:sigdig] || default_sigdig}) : "No pricing configured" },
728
+ "Status" => lambda {|it|
729
+ status_string = format_catalog_item_status(it)
730
+ if it['errorMessage'].to_s != ""
731
+ status_string << " - #{it['errorMessage']}"
732
+ end
733
+ status_string
734
+ },
735
+ #"Config" => lambda {|it| truncate_string(format_name_values(it['config']), 50) }
736
+ }
737
+ print as_pretty_table([cart_item], cart_item_columns.upcase_keys!)
738
+ print reset, "\n"
739
+ print_green_success(json_response['msg'] || "Item is valid")
740
+ print reset, "\n"
741
+ else
742
+ # not needed because it will be http 400
743
+ print_rest_errors(json_response, options)
744
+ end
745
+ else
746
+ print_green_success "Added item to cart"
747
+ get_cart([] + (options[:remote] ? ["-r",options[:remote]] : []))
748
+ end
749
+ end
750
+ if json_response['success']
751
+ return 0, nil
752
+ else
753
+ # not needed because it will be http 400
754
+ return 1, json_response['msg'] || 'request failed'
755
+ end
756
+ end
757
+
758
+ def update_cart_item(args)
759
+ #todo
760
+ raise_command_error "Not yet implemented"
761
+ end
762
+
763
+ def remove_cart_item(args)
764
+ options = {}
765
+ params = {}
766
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
767
+ opts.banner = subcommand_usage("[id]")
768
+ build_standard_remove_options(opts, options, [:sigdig])
769
+ opts.footer = <<-EOT
770
+ Delete an item from the cart.
771
+ [id] is required. This is the id of a cart item (also matches on type)
772
+ EOT
773
+ end
774
+ optparse.parse!(args)
775
+ verify_args!(args:args, optparse:optparse, max:1)
776
+ connect(options)
777
+
778
+ # fetch current cart
779
+ cart = @service_catalog_interface.get_cart()['cart']
780
+ cart_items = cart['items'] || []
781
+ cart_item = nil
782
+ item_id = args[0]
783
+ # match cart item on id OR type.name
784
+ if item_id.nil?
785
+ cart_item_option_type = {'fieldName' => 'id', 'fieldLabel' => 'Cart Item', 'type' => 'select', 'optionSource' => lambda { |api_client, api_params|
786
+ # cart_items.collect {|ci| {'name' => ci['name'], 'value' => ci['id']} }
787
+ cart_items.collect {|ci| {'name' => (ci['type']['name'] rescue ci['name']), 'value' => ci['id']} }
788
+ }, 'required' => true, 'description' => 'Cart Item to be removed'}
789
+ item_id = Morpheus::Cli::OptionTypes.prompt([cart_item_option_type], options[:options], @api_client)['id']
790
+ end
791
+ if item_id
792
+ cart_item = cart_items.find { |ci| ci['id'] == item_id.to_i }
793
+ if cart_item.nil?
794
+ matching_items = cart_items.select { |ci| (ci['type']['name'] rescue nil) == item_id.to_s }
795
+ if matching_items.size > 1
796
+ print_red_alert "#{matching_items.size} cart items matched '#{item_id}'"
797
+ cart_item_columns = [
798
+ {"ID" => lambda {|it| it['id'] } },
799
+ #{"NAME" => lambda {|it| it['name'] } },
800
+ {"Type" => lambda {|it| it['type']['name'] rescue '' } },
801
+ #{"Qty" => lambda {|it| it['quantity'] } },
802
+ {"Price" => lambda {|it| it['price'] ? format_money(it['price'] , it['currency'], {sigdig:options[:sigdig] || default_sigdig}) : "No pricing configured" } },
803
+ ]
804
+ puts_error as_pretty_table(matching_items, cart_item_columns, {color:red})
805
+ print_red_alert "Try using ID instead"
806
+ print reset,"\n"
807
+ return nil
808
+ end
809
+ cart_item = matching_items[0]
810
+ end
811
+ end
812
+ if cart_item.nil?
813
+ err = "Cart item not found for '#{item_id}'"
814
+ print_red_alert err
815
+ return 1, err
816
+ end
817
+
818
+ @service_catalog_interface.setopts(options)
819
+ if options[:dry_run]
820
+ print_dry_run @service_catalog_interface.dry.destroy_cart_item(cart_item['id'], params)
821
+ return
822
+ end
823
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to remove item '#{cart_item['type']['name'] rescue cart_item['id']}' from your cart?")
824
+ return 9, "aborted command"
825
+ end
826
+ json_response = @service_catalog_interface.destroy_cart_item(cart_item['id'], params)
827
+ render_response(json_response, options) do
828
+ print_green_success "Removed item from cart"
829
+ get_cart([] + (options[:remote] ? ["-r",options[:remote]] : []))
830
+ end
831
+ return 0, nil
832
+ end
833
+
834
+ def clear_cart(args)
835
+ options = {}
836
+ params = {}
837
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
838
+ opts.banner = subcommand_usage("--name [name]")
839
+ build_standard_remove_options(opts, options, [:sigdig])
840
+ opts.footer = <<-EOT
841
+ Clear your cart.
842
+ This will empty the cart, deleting all items.
843
+ EOT
844
+ end
845
+ optparse.parse!(args)
846
+ verify_args!(args:args, optparse:optparse, count:0)
847
+ connect(options)
848
+ # fetch current cart
849
+ # cart = @service_catalog_interfaceg.get_cart()['cart']
850
+ params.merge!(parse_query_options(options))
851
+ @service_catalog_interface.setopts(options)
852
+ if options[:dry_run]
853
+ print_dry_run @service_catalog_interface.dry.clear_cart(params)
854
+ return
855
+ end
856
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to clear your cart?")
857
+ return 9, "aborted command"
858
+ end
859
+ json_response = @service_catalog_interface.clear_cart(params)
860
+ render_response(json_response, options, 'cart') do
861
+ print_green_success "Cleared cart"
862
+ get_cart([] + (options[:remote] ? ["-r",options[:remote]] : []))
863
+ end
864
+ return 0, nil
865
+ end
866
+
867
+ def checkout(args)
868
+ options = {}
869
+ params = {}
870
+ payload = {}
871
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
872
+ opts.banner = subcommand_usage()
873
+ build_standard_add_options(opts, options, [:auto_confirm, :sigdig])
874
+ opts.footer = <<-EOT
875
+ Checkout to complete your cart and place an order.
876
+ EOT
877
+ end
878
+ optparse.parse!(args)
879
+ verify_args!(args:args, optparse:optparse, count:0)
880
+ connect(options)
881
+ # fetch current cart
882
+ # cart = @service_catalog_interface.get_cart()['cart']
883
+ params.merge!(parse_query_options(options))
884
+ payload = {}
885
+ if options[:payload]
886
+ payload = options[:payload]
887
+ end
888
+ update_cart_object_key = 'order'
889
+ passed_options = parse_passed_options(options)
890
+ payload.deep_merge!({update_cart_object_key => passed_options}) unless passed_options.empty?
891
+
892
+ @service_catalog_interface.setopts(options)
893
+ if options[:dry_run]
894
+ print_dry_run @service_catalog_interface.dry.checkout(payload)
895
+ return
896
+ end
897
+
898
+ # checkout
899
+ print_h1 "Checkout"
900
+
901
+ # review cart
902
+ # should load this first, but do this to avoid double load
903
+ cmd_result, cmd_err = get_cart(["--thin"] + (options[:remote] ? ["-r",options[:remote]] : []))
904
+ if cmd_result != 0
905
+ print_red_alert "You must add items before you can checkout. Try `catalog add`"
906
+ return cmd_result, cmd_err
907
+ end
908
+
909
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to checkout and place an order?")
910
+ return 9, "aborted command"
911
+ end
912
+ json_response = @service_catalog_interface.checkout(payload, params)
913
+ render_response(json_response, options) do
914
+ print_green_success "Order placed"
915
+ # ok so this is delayed because list does not return all statuses right now..
916
+ #list([] + (options[:remote] ? ["-r",options[:remote]] : []))
917
+ end
918
+ return 0, nil
919
+ end
920
+
921
+ def add_order(args)
922
+ options = {}
923
+ params = {}
924
+ payload = {}
925
+ type_id = nil
926
+ workflow_context = nil
927
+ workflow_target = nil
928
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
929
+ opts.banner = subcommand_usage("[type] [options]")
930
+ opts.on('-t', '--type TYPE', String, "Catalog Item Type Name or ID") do |val|
931
+ type_id = val.to_s
932
+ end
933
+ opts.on('--validate','--validate', "Validate Only. Validates the configuration and skips creating the order.") do
934
+ options[:validate_only] = true
935
+ end
936
+ opts.on('-a', '--details', "Display all details: item configuration." ) do
937
+ options[:details] = true
938
+ end
939
+ opts.on('--context [instance|server]', String, "Context Type for operational workflow types") do |val|
940
+ workflow_context = val.to_s
941
+ end
942
+ opts.on('--target ID', String, "Target Resource (Instance or Server) for operational workflow types") do |val|
943
+ workflow_target = val.to_s
944
+ end
945
+ build_standard_add_options(opts, options, [:sigdig])
946
+ opts.footer = <<-EOT
947
+ Place an order for new inventory.
948
+ This allows creating a new order without using the cart.
949
+ The order must contain one or more items, each with a valid type and configuration.
950
+ By default the order is placed right away.
951
+ Use the --validate option to validate and review the order without actually placing it.
952
+ EOT
953
+ end
954
+ optparse.parse!(args)
955
+ verify_args!(args:args, optparse:optparse, min:0)
956
+ connect(options)
957
+ if args.count > 0
958
+ type_id = args.join(" ")
959
+ end
960
+ payload = {}
961
+ order_object_key = 'order'
962
+ payload = {order_object_key => {} }
963
+ passed_options = parse_passed_options(options)
964
+ if options[:payload]
965
+ payload = options[:payload]
966
+ payload.deep_merge!({order_object_key => passed_options}) unless passed_options.empty?
967
+ else
968
+ payload.deep_merge!({order_object_key => passed_options}) unless passed_options.empty?
969
+
970
+ # Prompt for 1-N Types
971
+ # still_prompting = options[:no_prompt] != true
972
+ still_prompting = true
973
+ available_catalog_item_types = @service_catalog_interface.list_types({max:10000})['catalogItemTypes'].collect {|it|
974
+ {'name' => it['name'], 'value' => it['id']}
975
+ }
976
+ type_cache = {} # prevent repeat lookups
977
+ while still_prompting do
978
+ item_payload = {}
979
+ # prompt for Type
980
+ if type_id
981
+ catalog_item_type = type_cache[type_id.to_s] || find_catalog_item_type_by_name_or_id(type_id)
982
+ return [1, "catalog item type not found"] if catalog_item_type.nil?
983
+ elsif
984
+ type_id = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'type', 'fieldLabel' => 'Type', 'type' => 'select', 'selectOptions' => available_catalog_item_types, 'required' => true, 'description' => 'Catalog Item Type name or id'}], options[:options], @api_client, options[:params])['type']
985
+ catalog_item_type = type_cache[type_id.to_s] || find_catalog_item_type_by_name_or_id(type_id.to_s)
986
+ return [1, "catalog item type not found"] if catalog_item_type.nil?
987
+ end
988
+ type_cache[type_id.to_s] = catalog_item_type
989
+ # use name instead of id
990
+ item_payload['type'] = {'name' => catalog_item_type['name']}
991
+ #payload[add_item_object_key]['type'] = {'id' => catalog_item_type['id']}
992
+
993
+ # this is silly, need to load by id to get optionTypes
994
+ # maybe do ?name=foo&includeOptionTypes=true
995
+ if catalog_item_type['optionTypes'].nil?
996
+ catalog_item_type = find_catalog_item_type_by_id(catalog_item_type['id'])
997
+ return [1, "catalog item type not found"] if catalog_item_type.nil?
998
+ end
999
+ catalog_option_types = catalog_item_type['optionTypes']
1000
+ # instead of config.customOptions just use config...
1001
+ catalog_option_types = catalog_option_types.collect {|it|
1002
+ it['fieldContext'] = 'config'
1003
+ it
1004
+ }
1005
+ if catalog_option_types && !catalog_option_types.empty?
1006
+ config_prompt = Morpheus::Cli::OptionTypes.prompt(catalog_option_types, options[:options], @api_client, {})['config']
1007
+ item_payload.deep_merge!({'config' => config_prompt})
1008
+ end
1009
+
1010
+ if workflow_context
1011
+ item_payload['context'] = workflow_context
1012
+ else
1013
+ # the catalog item type determines if context selection is required
1014
+ # only blank string means you can choose? err
1015
+ if catalog_item_type['context'] == ''
1016
+ context_option_type = {'fieldName' => 'context', 'fieldLabel' => 'Context Type', 'type' => 'select', 'optionSource' => lambda { |api_client, api_params|
1017
+ [{'name' => "Instance", 'value' => "instance"}, {'name' => "Server", 'value' => "server"}]
1018
+ }, 'required' => true, 'description' => 'Context for operational workflow, determines target type', 'defaultValue' => 'instance'}
1019
+ workflow_context = Morpheus::Cli::OptionTypes.prompt([context_option_type], options[:options], @api_client, options[:params])['context']
1020
+ elsif !catalog_item_type['context'].nil?
1021
+ workflow_context = catalog_item_type['context']
1022
+ end
1023
+ item_payload['context'] = workflow_context if workflow_context
1024
+ end
1025
+
1026
+ if workflow_target
1027
+ item_payload['targets'] = [{id: workflow_target}]
1028
+ else
1029
+ # prompt for Resource (target)
1030
+ if workflow_context == 'instance'
1031
+ target_option_type = {'fieldName' => 'target', 'fieldLabel' => 'Resource (Instance)', 'type' => 'select', 'optionSource' => lambda { |api_client, api_params|
1032
+ # todo: @instances_interface should not be required here
1033
+ # @options_interface.options_for_source("instances", {})['data']
1034
+ @instances_interface.list({max:10000})['instances'].collect {|it|
1035
+ {'name' => it['name'], 'value' => it['id']}
1036
+ } }, 'required' => true, 'description' => 'Target Instance'}
1037
+ workflow_target = Morpheus::Cli::OptionTypes.prompt([target_option_type], options[:options], @api_client, options[:params])['target']
1038
+ item_payload['targets'] = [{id: workflow_target}]
1039
+ item_payload.delete('target')
1040
+ elsif workflow_context == 'server'
1041
+ target_option_type = {'fieldName' => 'target', 'fieldLabel' => 'Resource (Server)', 'type' => 'select', 'optionSource' => lambda { |api_client, api_params|
1042
+ # todo: @servers_interface should not be required here
1043
+ # @options_interface.options_for_source("searchServers", {})['data']
1044
+ @servers_interface.list({max:10000})['servers'].collect {|it|
1045
+ {'name' => it['name'], 'value' => it['id']}
1046
+ } }, 'required' => true, 'description' => 'Target Server'}
1047
+ workflow_target = Morpheus::Cli::OptionTypes.prompt([target_option_type], options[:options], @api_client, options[:params])['target']
1048
+ item_payload['targets'] = [{id: workflow_target}]
1049
+ item_payload.delete('target')
1050
+ end
1051
+ end
1052
+
1053
+ payload[order_object_key]['items'] ||= []
1054
+ payload[order_object_key]['items'] << item_payload
1055
+
1056
+ if options[:no_prompt]
1057
+ still_prompting = false
1058
+ else
1059
+ if Morpheus::Cli::OptionTypes.confirm("Add another item?", {default: false})
1060
+ still_prompting = true
1061
+ # clear values for subsequent items, should just use for a different fieldContext instead..
1062
+ type_id = nil
1063
+ options[:options] = {}
1064
+ else
1065
+ still_prompting = false
1066
+ end
1067
+ end
1068
+
1069
+ end
1070
+
1071
+
1072
+ end
1073
+ if options[:validate_only]
1074
+ params['validate'] = true
1075
+ #payload['validate'] = true
1076
+ end
1077
+ @service_catalog_interface.setopts(options)
1078
+ if options[:dry_run]
1079
+ print_dry_run @service_catalog_interface.dry.create_order(payload, params)
1080
+ return
1081
+ end
1082
+ json_response = @service_catalog_interface.create_order(payload, params)
1083
+ order = json_response['order'] || json_response['cart']
1084
+ render_response(json_response, options) do
1085
+ if options[:validate_only]
1086
+ if json_response['success']
1087
+ print_h2 "Review Order", [], options
1088
+ print_order_details(order, options)
1089
+ print_green_success(json_response['msg'] || "Order is valid")
1090
+ print reset, "\n"
1091
+ else
1092
+ # not needed because it will be http 400
1093
+ print_rest_errors(json_response, options)
1094
+ end
1095
+ else
1096
+ print_green_success "Order placed"
1097
+ print_h2 "Order Details", [], options
1098
+ print_order_details(order, options)
1099
+ end
1100
+ end
1101
+ if json_response['success']
1102
+ return 0, nil
1103
+ else
1104
+ # not needed because it will be http 400
1105
+ return 1, json_response['msg'] || 'request failed'
1106
+ end
1107
+ end
1108
+
1109
+ def remove(args)
1110
+ options = {}
1111
+ params = {}
1112
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
1113
+ opts.banner = subcommand_usage("[item] [options]")
1114
+ opts.on('--remove-instances [true|false]', String, "Remove instances. Default is true. Applies to apps only.") do |val|
1115
+ params[:removeInstances] = ['true','on','1',''].include?(val.to_s.downcase)
1116
+ end
1117
+ opts.on( '-B', '--keep-backups [true|false]', "Preserve copy of backups. Default is false." ) do
1118
+ params[:keepBackups] = ['true','on','1',''].include?(val.to_s.downcase)
1119
+ end
1120
+ opts.on('--preserve-volumes [on|off]', String, "Preserve Volumes. Default is off. Applies to certain types only.") do |val|
1121
+ params[:preserveVolumes] = ['true','on','1',''].include?(val.to_s.downcase)
1122
+ end
1123
+ opts.on('--releaseEIPs [true|false]', String, "Release EIPs. Default is on. Applies to Amazon only.") do |val|
1124
+ params[:releaseEIPs] = ['true','on','1',''].include?(val.to_s.downcase)
1125
+ end
1126
+ opts.on( '-f', '--force', "Force Delete" ) do
1127
+ params[:force] = true
1128
+ end
1129
+ build_standard_remove_options(opts, options, [:sigdig])
1130
+ opts.footer = <<-EOT
1131
+ Delete a catalog inventory item.
1132
+ This removes the item from the inventory and deprovisions the associated instance(s).
1133
+ [item] is required. This is the name or id of a catalog inventory item.
1134
+ EOT
1135
+ end
1136
+ optparse.parse!(args)
1137
+ verify_args!(args:args, optparse:optparse, count:1)
1138
+ connect(options)
1139
+
1140
+ catalog_item = find_catalog_item_by_name_or_id(args[0])
1141
+ return 1 if catalog_item.nil?
1142
+
1143
+ is_app = (catalog_item['type']['type'] == 'app' || catalog_item['type']['type'] == 'blueprint' || catalog_item['type']['type'] == 'apptemplate') rescue false
1144
+
1145
+ params.merge!(parse_query_options(options))
1146
+ # delete dialog
1147
+ # we do not have provisioning settings right now to know if we can prompt for volumes / eips
1148
+ # skip force because it is excessive prompting...
1149
+ delete_prompt_options = [
1150
+ {'fieldName' => 'removeInstances', 'fieldLabel' => 'Remove Instances', 'type' => 'checkbox', 'defaultValue' => true},
1151
+ {'fieldName' => 'keepBackups', 'fieldLabel' => 'Preserve Backups', 'type' => 'checkbox', 'defaultValue' => false},
1152
+ #{'fieldName' => 'preserveVolumes', 'fieldLabel' => 'Preserve Volumes', 'type' => 'checkbox', 'defaultValue' => false},
1153
+ # {'fieldName' => 'releaseEIPs', 'fieldLabel' => 'Release EIPs. Default is on. Applies to Amazon only.', 'type' => 'checkbox', 'defaultValue' => true},
1154
+ #{'fieldName' => 'force', 'fieldLabel' => 'Force Delete', 'type' => 'checkbox', 'defaultValue' => false},
1155
+ ]
1156
+ if !is_app
1157
+ delete_prompt_options.reject! {|it| it['fieldName'] == 'removeInstances'}
1158
+ end
1159
+ options[:options][:no_prompt] = true if options[:yes] # -y could always mean do not prompt too..
1160
+ v_prompt = Morpheus::Cli::OptionTypes.prompt(delete_prompt_options, options[:options], @api_client)
1161
+ v_prompt.booleanize! # 'on' => true
1162
+ params.deep_merge!(v_prompt)
1163
+
1164
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete the inventory item #{catalog_item['id']} '#{catalog_item['name']}'?")
1165
+ return 9, "aborted command"
1166
+ end
1167
+ @service_catalog_interface.setopts(options)
1168
+ if options[:dry_run]
1169
+ print_dry_run @service_catalog_interface.dry.destroy_inventory(catalog_item['id'], params)
1170
+ return
1171
+ end
1172
+ json_response = @service_catalog_interface.destroy_inventory(catalog_item['id'], params)
1173
+ render_response(json_response, options) do
1174
+ print_green_success "Removing catalog item"
1175
+ end
1176
+ return 0, nil
1177
+ end
1178
+
1179
+ private
1180
+
1181
+ # Catalog Item Types helpers
1182
+
1183
+ def catalog_item_type_column_definitions()
1184
+ {
1185
+ "ID" => 'id',
1186
+ "Name" => 'name',
1187
+ "Description" => 'description',
1188
+ # "Type" => lambda {|it| format_catalog_type(it) },
1189
+ # "Blueprint" => lambda {|it| it['blueprint'] ? it['blueprint']['name'] : nil },
1190
+ # "Enabled" => lambda {|it| format_boolean(it['enabled']) },
1191
+ "Featured" => lambda {|it| format_boolean(it['featured']) },
1192
+ #"Config" => lambda {|it| it['config'] },
1193
+ # "Created" => lambda {|it| format_local_dt(it['dateCreated']) },
1194
+ # "Updated" => lambda {|it| format_local_dt(it['lastUpdated']) },
1195
+ }
1196
+ end
1197
+
1198
+ def catalog_item_type_object_key
1199
+ 'catalogItemType'
1200
+ end
1201
+
1202
+ def catalog_item_type_list_key
1203
+ 'catalogItemTypes'
1204
+ end
1205
+
1206
+ def find_catalog_item_type_by_name_or_id(val)
1207
+ if val.to_s =~ /\A\d{1,}\Z/
1208
+ return find_catalog_item_type_by_id(val)
1209
+ else
1210
+ return find_catalog_item_type_by_name(val)
1211
+ end
1212
+ end
1213
+
1214
+ # this returns optionTypes and list does not..
1215
+ def find_catalog_item_type_by_id(id)
1216
+ begin
1217
+ json_response = @service_catalog_interface.get_type(id.to_i)
1218
+ return json_response[catalog_item_type_object_key]
1219
+ rescue RestClient::Exception => e
1220
+ if e.response && e.response.code == 404
1221
+ print_red_alert "Catalog item type not found by id '#{id}'"
1222
+ else
1223
+ raise e
1224
+ end
1225
+ end
1226
+ end
1227
+
1228
+ def find_catalog_item_type_by_name(name)
1229
+ json_response = @service_catalog_interface.list_types({name: name.to_s})
1230
+ catalog_item_types = json_response[catalog_item_type_list_key]
1231
+ if catalog_item_types.empty?
1232
+ print_red_alert "Catalog item type not found by name '#{name}'"
1233
+ return nil
1234
+ elsif catalog_item_types.size > 1
1235
+ print_red_alert "#{catalog_item_types.size} catalog item types found by name '#{name}'"
1236
+ puts_error as_pretty_table(catalog_item_types, [:id, :name], {color:red})
1237
+ print_red_alert "Try using ID instead"
1238
+ print reset,"\n"
1239
+ return nil
1240
+ else
1241
+ return catalog_item_types[0]
1242
+ end
1243
+ end
1244
+
1245
+ def catalog_item_column_definitions()
1246
+ {
1247
+ "ID" => 'id',
1248
+ "Name" => 'name',
1249
+ #"Description" => 'description',
1250
+ "Type" => lambda {|it| it['type']['name'] rescue '' },
1251
+ #"Qty" => lambda {|it| it['quantity'] },
1252
+ # "Enabled" => lambda {|it| format_boolean(it['enabled']) },
1253
+
1254
+ # "Created" => lambda {|it| format_local_dt(it['dateCreated']) },
1255
+ # "Updated" => lambda {|it| format_local_dt(it['lastUpdated']) },
1256
+ "Order Date" => lambda {|it| format_local_dt(it['orderDate']) },
1257
+ "Status" => lambda {|it| format_catalog_item_status(it) },
1258
+ # "Config" => lambda {|it| format_name_values(it['config']) },
1259
+ }
1260
+ end
1261
+
1262
+ # Catalog Items (Inventory) helpers
1263
+
1264
+ def catalog_item_object_key
1265
+ 'item'
1266
+ end
1267
+
1268
+ def catalog_item_list_key
1269
+ 'items'
1270
+ end
1271
+
1272
+ def find_catalog_item_by_name_or_id(val)
1273
+ if val.to_s =~ /\A\d{1,}\Z/
1274
+ return find_catalog_item_by_id(val)
1275
+ else
1276
+ return find_catalog_item_by_name(val)
1277
+ end
1278
+ end
1279
+
1280
+ def find_catalog_item_by_id(id)
1281
+ begin
1282
+ json_response = @service_catalog_interface.get_inventory(id.to_i)
1283
+ return json_response[catalog_item_object_key]
1284
+ rescue RestClient::Exception => e
1285
+ if e.response && e.response.code == 404
1286
+ print_red_alert "Catalog item not found by id '#{id}'"
1287
+ else
1288
+ raise e
1289
+ end
1290
+ end
1291
+ end
1292
+
1293
+ # find by name not yet supported, items do not have a name
1294
+ def find_catalog_item_by_name(name)
1295
+ json_response = @service_catalog_interface.list_inventory({name: name.to_s})
1296
+ catalog_items = json_response[catalog_item_list_key]
1297
+ if catalog_items.empty?
1298
+ print_red_alert "Catalog item not found by name '#{name}'"
1299
+ return nil
1300
+ elsif catalog_items.size > 1
1301
+ print_red_alert "#{catalog_items.size} catalog items found by name '#{name}'"
1302
+ puts_error as_pretty_table(catalog_items, [:id, :name], {color:red})
1303
+ print_red_alert "Try using ID instead"
1304
+ print reset,"\n"
1305
+ return nil
1306
+ else
1307
+ return catalog_items[0]
1308
+ end
1309
+ end
1310
+
1311
+ def format_catalog_item_status(item, return_color=cyan)
1312
+ out = ""
1313
+ status_string = item['status'].to_s.upcase
1314
+ if status_string == 'IN_CART' || status_string == 'IN CART'
1315
+ out << "#{cyan}IN CART#{return_color}"
1316
+ elsif status_string == 'ORDERED'
1317
+ #out << "#{cyan}#{status_string.upcase}#{return_color}"
1318
+ # show the instance/app/execution status instead of the item status ORDERED
1319
+ if item['instance']
1320
+ out << format_instance_status(item['instance'], return_color)
1321
+ elsif item['app']
1322
+ out << format_app_status(item['app'], return_color)
1323
+ elsif item['execution']
1324
+ out << format_job_execution_status(item['execution'], return_color)
1325
+ else
1326
+ out << "#{cyan}#{status_string.upcase}#{return_color}"
1327
+ end
1328
+ elsif status_string == 'VALID'
1329
+ out << "#{green}#{status_string.upcase}#{return_color}"
1330
+ elsif status_string == 'INVALID'
1331
+ out << "#{red}#{status_string.upcase}#{return_color}"
1332
+ elsif status_string == 'FAILED'
1333
+ out << "#{red}#{status_string.upcase}#{return_color}"
1334
+ elsif status_string == 'DELETED'
1335
+ out << "#{red}#{status_string.upcase}#{return_color}" # cyan maybe?
1336
+ else
1337
+ out << "#{yellow}#{status_string.upcase}#{return_color}"
1338
+ end
1339
+ out
1340
+ end
1341
+
1342
+ def format_order_status(cart, return_color=cyan)
1343
+ out = ""
1344
+ cart_items = cart['items']
1345
+ if cart_items.nil? || cart_items.empty?
1346
+ out << "#{yellow}EMPTY#{return_color}"
1347
+ else
1348
+ # status of first item in cart will work i guess...
1349
+ item = cart_items.first
1350
+ status_string = item['status'].to_s.upcase
1351
+ if status_string == "IN_CART"
1352
+ # out << "#{cyan}CART (#{cart_items.size()})#{return_color}"
1353
+ out << "#{cyan}CART#{return_color}"
1354
+ else
1355
+ out << format_catalog_item_status(item, return_color)
1356
+ end
1357
+ end
1358
+ out
1359
+ end
1360
+
1361
+ def print_order_details(cart, options)
1362
+ cart_items = cart['items'] || []
1363
+ cart_stats = cart['stats'] || {}
1364
+ if cart_items && cart_items.size > 0
1365
+ print cyan
1366
+ cart_show_columns = {
1367
+ #"Order ID" => 'id',
1368
+ "Order Name" => 'name',
1369
+ "Order Items" => lambda {|it| cart['items'].size },
1370
+ "Order Qty" => lambda {|it| cart['items'].sum {|cart_item| cart_item['quantity'] } },
1371
+ "Order Status" => lambda {|it| format_order_status(it) },
1372
+ #"Order Total" => lambda {|it| format_money(cart_stats['price'], cart_stats['currency'], {sigdig:options[:sigdig] || default_sigdig}) + " / #{cart_stats['unit'].to_s.empty? ? 'month' : cart_stats['unit']}" },
1373
+ #"Items" => lambda {|it| cart['items'].size },
1374
+ # "Created" => lambda {|it| format_local_dt(it['dateCreated']) },
1375
+ # "Updated" => lambda {|it| format_local_dt(it['lastUpdated']) },
1376
+ }
1377
+ if options[:details] != true
1378
+ cart_show_columns.delete("Order Items")
1379
+ cart_show_columns.delete("Order Qty")
1380
+ cart_show_columns.delete("Order Status")
1381
+ end
1382
+ cart_show_columns.delete("Order Name") if cart['name'].to_s.empty?
1383
+ # if !cart_show_columns.empty?
1384
+ # print_description_list(cart_show_columns, cart)
1385
+ # print reset, "\n"
1386
+ # end
1387
+
1388
+ if options[:details]
1389
+ if !cart_show_columns.empty?
1390
+ print_description_list(cart_show_columns, cart)
1391
+ # print reset, "\n"
1392
+ end
1393
+ #print_h2 "Cart Items"
1394
+ cart_items.each_with_index do |cart_item, index|
1395
+ item_config = cart_item['config']
1396
+ cart_item_columns = [
1397
+ {"ID" => lambda {|it| it['id'] } },
1398
+ #{"NAME" => lambda {|it| it['name'] } },
1399
+ {"Type" => lambda {|it| it['type']['name'] rescue '' } },
1400
+ #{"Qty" => lambda {|it| it['quantity'] } },
1401
+ {"Price" => lambda {|it| it['price'] ? format_money(it['price'] , it['currency'], {sigdig:options[:sigdig] || default_sigdig}) : "No pricing configured" } },
1402
+ {"Status" => lambda {|it|
1403
+ status_string = format_catalog_item_status(it)
1404
+ if it['errorMessage'].to_s != ""
1405
+ status_string << " - #{it['errorMessage']}"
1406
+ end
1407
+ status_string
1408
+ } },
1409
+ # {"Config" => lambda {|it| format_name_values(it['config']) } },
1410
+ ]
1411
+ print_h2(index == 0 ? "Item" : "Item #{index+1}", options)
1412
+ print as_description_list(cart_item, cart_item_columns, options)
1413
+ # print "\n", reset
1414
+ if item_config && !item_config.keys.empty?
1415
+ print_h2("Configuration", options)
1416
+ print as_description_list(item_config, item_config.keys, options)
1417
+ print "\n", reset
1418
+ end
1419
+ end
1420
+ else
1421
+ if !cart_show_columns.empty?
1422
+ print_description_list(cart_show_columns, cart)
1423
+ print reset, "\n"
1424
+ end
1425
+ #print_h2 "Cart Items"
1426
+ cart_item_columns = [
1427
+ {"ID" => lambda {|it| it['id'] } },
1428
+ #{"NAME" => lambda {|it| it['name'] } },
1429
+ {"TYPE" => lambda {|it| it['type']['name'] rescue '' } },
1430
+ #{"QTY" => lambda {|it| it['quantity'] } },
1431
+ {"PRICE" => lambda {|it| it['price'] ? format_money(it['price'] , it['currency'], {sigdig:options[:sigdig] || default_sigdig}) : "No pricing configured" } },
1432
+ {"STATUS" => lambda {|it|
1433
+ status_string = format_catalog_item_status(it)
1434
+ if it['errorMessage'].to_s != ""
1435
+ status_string << " - #{it['errorMessage']}"
1436
+ end
1437
+ status_string
1438
+ } },
1439
+ # {"CONFIG" => lambda {|it|
1440
+ # truncate_string(format_name_values(it['config']), 50)
1441
+ # } },
1442
+ ]
1443
+ print as_pretty_table(cart_items, cart_item_columns)
1444
+ end
1445
+ print reset,"\n"
1446
+ print cyan
1447
+ if cart_stats['price']
1448
+ puts "Total: " + format_money(cart_stats['price'], cart_stats['currency'], {sigdig:options[:sigdig] || default_sigdig}) + " / #{cart_stats['unit'].to_s.empty? ? 'month' : cart_stats['unit']}"
1449
+ else
1450
+ puts "Total: " + "No pricing configured"
1451
+ end
1452
+ print reset,"\n"
1453
+ else
1454
+ print cyan,"Cart is empty","\n",reset
1455
+ print reset,"\n"
1456
+ end
1457
+ end
1458
+
1459
+ def format_job_execution_status(execution, return_color=cyan)
1460
+ out = ""
1461
+ status_string = execution['status'].to_s.downcase
1462
+ if status_string
1463
+ if ['complete','success', 'successful', 'ok'].include?(status_string)
1464
+ out << "#{green}#{status_string.upcase}"
1465
+ elsif ['error', 'offline', 'failed', 'failure'].include?(status_string)
1466
+ out << "#{red}#{status_string.upcase}"
1467
+ else
1468
+ out << "#{yellow}#{status_string.upcase}"
1469
+ end
1470
+ end
1471
+ out + return_color
1472
+ end
1473
+
1474
+ end