morpheus-cli 4.2.21 → 5.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +1 -1
  3. data/lib/morpheus/api/api_client.rb +30 -0
  4. data/lib/morpheus/api/billing_interface.rb +34 -0
  5. data/lib/morpheus/api/catalog_item_types_interface.rb +9 -0
  6. data/lib/morpheus/api/deploy_interface.rb +1 -1
  7. data/lib/morpheus/api/deployments_interface.rb +20 -1
  8. data/lib/morpheus/api/forgot_password_interface.rb +17 -0
  9. data/lib/morpheus/api/instances_interface.rb +16 -2
  10. data/lib/morpheus/api/rest_interface.rb +0 -6
  11. data/lib/morpheus/api/roles_interface.rb +14 -0
  12. data/lib/morpheus/api/search_interface.rb +13 -0
  13. data/lib/morpheus/api/servers_interface.rb +14 -0
  14. data/lib/morpheus/api/service_catalog_interface.rb +89 -0
  15. data/lib/morpheus/api/usage_interface.rb +18 -0
  16. data/lib/morpheus/cli.rb +7 -3
  17. data/lib/morpheus/cli/apps.rb +6 -27
  18. data/lib/morpheus/cli/backup_jobs_command.rb +3 -0
  19. data/lib/morpheus/cli/backups_command.rb +3 -0
  20. data/lib/morpheus/cli/catalog_item_types_command.rb +622 -0
  21. data/lib/morpheus/cli/cli_command.rb +70 -21
  22. data/lib/morpheus/cli/commands/standard/curl_command.rb +26 -12
  23. data/lib/morpheus/cli/commands/standard/history_command.rb +3 -1
  24. data/lib/morpheus/cli/commands/standard/man_command.rb +74 -40
  25. data/lib/morpheus/cli/commands/standard/source_command.rb +1 -1
  26. data/lib/morpheus/cli/commands/standard/update_command.rb +76 -0
  27. data/lib/morpheus/cli/containers_command.rb +14 -24
  28. data/lib/morpheus/cli/cypher_command.rb +6 -2
  29. data/lib/morpheus/cli/deploy.rb +199 -90
  30. data/lib/morpheus/cli/deployments.rb +341 -28
  31. data/lib/morpheus/cli/deploys.rb +206 -41
  32. data/lib/morpheus/cli/error_handler.rb +7 -0
  33. data/lib/morpheus/cli/forgot_password.rb +133 -0
  34. data/lib/morpheus/cli/groups.rb +1 -1
  35. data/lib/morpheus/cli/health_command.rb +59 -2
  36. data/lib/morpheus/cli/hosts.rb +265 -34
  37. data/lib/morpheus/cli/instances.rb +186 -100
  38. data/lib/morpheus/cli/invoices_command.rb +33 -16
  39. data/lib/morpheus/cli/jobs_command.rb +28 -6
  40. data/lib/morpheus/cli/library_option_lists_command.rb +15 -7
  41. data/lib/morpheus/cli/library_option_types_command.rb +5 -2
  42. data/lib/morpheus/cli/logs_command.rb +9 -6
  43. data/lib/morpheus/cli/mixins/accounts_helper.rb +12 -7
  44. data/lib/morpheus/cli/mixins/backups_helper.rb +2 -4
  45. data/lib/morpheus/cli/mixins/deployments_helper.rb +31 -3
  46. data/lib/morpheus/cli/mixins/option_source_helper.rb +1 -1
  47. data/lib/morpheus/cli/mixins/print_helper.rb +46 -21
  48. data/lib/morpheus/cli/mixins/provisioning_helper.rb +100 -4
  49. data/lib/morpheus/cli/network_pools_command.rb +14 -6
  50. data/lib/morpheus/cli/option_types.rb +271 -22
  51. data/lib/morpheus/cli/ping.rb +0 -1
  52. data/lib/morpheus/cli/remote.rb +35 -12
  53. data/lib/morpheus/cli/reports_command.rb +99 -30
  54. data/lib/morpheus/cli/roles.rb +453 -113
  55. data/lib/morpheus/cli/search_command.rb +182 -0
  56. data/lib/morpheus/cli/service_catalog_command.rb +1474 -0
  57. data/lib/morpheus/cli/service_plans_command.rb +2 -2
  58. data/lib/morpheus/cli/setup.rb +1 -1
  59. data/lib/morpheus/cli/shell.rb +33 -11
  60. data/lib/morpheus/cli/storage_providers_command.rb +40 -56
  61. data/lib/morpheus/cli/tasks.rb +29 -32
  62. data/lib/morpheus/cli/usage_command.rb +203 -0
  63. data/lib/morpheus/cli/user_settings_command.rb +1 -0
  64. data/lib/morpheus/cli/users.rb +12 -1
  65. data/lib/morpheus/cli/version.rb +1 -1
  66. data/lib/morpheus/cli/virtual_images.rb +429 -254
  67. data/lib/morpheus/cli/whoami.rb +6 -6
  68. data/lib/morpheus/cli/workflows.rb +34 -41
  69. data/lib/morpheus/formatters.rb +75 -7
  70. data/lib/morpheus/terminal.rb +6 -2
  71. metadata +14 -2
@@ -0,0 +1,182 @@
1
+ require 'morpheus/cli/cli_command'
2
+
3
+ class Morpheus::Cli::SearchCommand
4
+ include Morpheus::Cli::CliCommand
5
+
6
+ set_command_name :search
7
+ set_command_description "Global search for finding all types of objects"
8
+
9
+ def connect(opts)
10
+ @api_client = establish_remote_appliance_connection(opts)
11
+ @search_interface = @api_client.search
12
+ end
13
+
14
+ def handle(args)
15
+ options = {}
16
+ params = {}
17
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
18
+ opts.banner = "Usage: #{prog_name} #{command_name} [phrase]"
19
+ opts.on("-g", "--go", "Go get details for the top search result instead of printing the list.") do
20
+ options[:go] = true
21
+ end
22
+ build_standard_list_options(opts, options)
23
+ opts.footer = <<-EOT
24
+ Global search for finding all types of objects
25
+ [phrase] is required. This is the phrase to search for.
26
+ Prints the list of search results with the most relevant first.
27
+ or use the --go option to get details about the top result instead.
28
+ EOT
29
+ end
30
+ optparse.parse!(args)
31
+ connect(options)
32
+ # verify_args!(args:args, optparse:optparse, min:1)
33
+ if args.count > 0
34
+ options[:phrase] = args.join(" ")
35
+ end
36
+ if options[:phrase].to_s.empty?
37
+ raise_command_error "[phrase] is required.", args, optparse
38
+ end
39
+ params.merge!(parse_list_options(options))
40
+ @search_interface.setopts(options)
41
+ if options[:dry_run]
42
+ print_dry_run @search_interface.dry.list(params)
43
+ return
44
+ end
45
+ json_response = @search_interface.list(params)
46
+ search_results = json_response["hits"] || json_response["results"] || []
47
+ top_result = search_results[0]
48
+ if options[:go]
49
+ if top_result
50
+ print cyan,"Loading top search result: #{format_morpheus_type(top_result['type'])} #{top_result['name'] || top_result['id']} (score: #{top_result['score']})",reset,"\n"
51
+ return go_to_search_result(top_result, options)
52
+ else
53
+ # print cyan,"No results found.",reset,"\n"
54
+ raise_command_error "No search results for phrase '#{options[:phrase]}'"
55
+ end
56
+ end
57
+ render_response(json_response, options, "hits") do
58
+ print_h1 "Morpheus Search", parse_list_subtitles(options), options
59
+ if search_results.empty?
60
+ print cyan,"No results found.",reset,"\n"
61
+ else
62
+ columns = {
63
+ "Type" => lambda {|it| format_morpheus_type(it['type']) },
64
+ "ID" => 'id',
65
+ # "UUID" => 'uuid',
66
+ "Name" => 'name',
67
+ "Decription" => 'description',
68
+ #"Score" => 'score',
69
+ "Date Created" => lambda {|it| format_local_dt(it['dateCreated']) },
70
+ }
71
+ print as_pretty_table(search_results, columns.upcase_keys!, options)
72
+ print_results_pagination(json_response)
73
+ end
74
+ print reset,"\n"
75
+ end
76
+ if search_results.empty?
77
+ return 1, "no results found"
78
+ else
79
+ return 0, nil
80
+ end
81
+ end
82
+
83
+ protected
84
+
85
+ def format_morpheus_type(val)
86
+ if val == "ComputeSite"
87
+ "Group"
88
+ elsif val == "ComputeZone"
89
+ "Cloud"
90
+ elsif val == "ComputeServer"
91
+ "Host"
92
+ elsif val == "ComputeServerGroup"
93
+ "Cluster"
94
+ elsif val == "ComputeZonePool"
95
+ "Pool"
96
+ else
97
+ val
98
+ end
99
+ end
100
+
101
+ def go_to_search_result(result, options)
102
+ result_type = result['type']
103
+ cmd_class = lookup_morpheus_command(result['type'])
104
+ if cmd_class
105
+ get_options = []
106
+ get_options += (options[:remote] ? ["-r",options[:remote]] : [])
107
+ get_options += (options[:json] ? ["--json"] : [])
108
+ get_options += (options[:yaml] ? ["--yaml"] : [])
109
+ return cmd_class.new.handle(["get", result['id']] + get_options)
110
+ else
111
+ raise_command_error "Sorry, result cannot be loaded. type: #{result_type}, id: #{result['id']}, name: #{result['name']}"
112
+ end
113
+ end
114
+
115
+
116
+ def lookup_morpheus_command(result_type)
117
+ case result_type.to_s.downcase
118
+ when "computezone","cloud","zone"
119
+ Morpheus::Cli::Clouds
120
+ when "computesite","group"
121
+ Morpheus::Cli::Groups
122
+ when "computeserver","server","host"
123
+ Morpheus::Cli::Hosts
124
+ when "computeservergroup","serverGroup","cluster"
125
+ Morpheus::Cli::Clusters
126
+ when "instance"
127
+ Morpheus::Cli::Instances
128
+ when "app"
129
+ Morpheus::Cli::Apps
130
+ when "container"
131
+ Morpheus::Cli::Containers
132
+ when "computezonefolder","zonefolder","resourcefolder"
133
+ # crap, need result.zoneId, or resource-folders should not require cloud actually, get by id
134
+ # Morpheus::Cli::CloudFoldersCommand
135
+ nil
136
+ when "computezonepool","zonepool","resourcepool"
137
+ # crap, need result.zoneId, or resource-pools should not require cloud actually, get by id
138
+ # Morpheus::Cli::CloudResourcePoolsCommand
139
+ nil
140
+ # when "chassis"
141
+ # Morpheus::Cli::ChassisCommand
142
+ when "network"
143
+ Morpheus::Cli::NetworksCommand
144
+ when "networkgroup"
145
+ Morpheus::Cli::NetworkGroupsCommand
146
+ when "networkpool"
147
+ Morpheus::Cli::NetworkPoolsCommand
148
+ when "networkdomain"
149
+ Morpheus::Cli::NetworkDomainsCommand
150
+ when "virtualimage"
151
+ Morpheus::Cli::VirtualImages
152
+ when "loadbalancer"
153
+ Morpheus::Cli::LoadBalancers
154
+ # when "virtualserver","loadbalancerinstance"
155
+ # Morpheus::Cli::LoadBalancerInstances
156
+ when "instancetype"
157
+ # Morpheus::Cli::LibraryInstanceTypesCommand
158
+ Morpheus::Cli::LibraryInstanceTypes
159
+ when "instancetypelayout","layout"
160
+ Morpheus::Cli::LibraryLayoutsCommand
161
+ when "certificate"
162
+ # todo: WHAT! didnt I write certs already!?
163
+ Morpheus::Cli::CertificatesCommand
164
+ when "keypair"
165
+ Morpheus::Cli::KeyPairs
166
+ when "integration"
167
+ Morpheus::Cli::IntegrationsCommand
168
+ when "account","tenant"
169
+ Morpheus::Cli::TenantsCommand
170
+ when "user"
171
+ Morpheus::Cli::Users
172
+ when "role"
173
+ Morpheus::Cli::Roles
174
+ when "wikipage","wiki"
175
+ Morpheus::Cli::WikiCommand
176
+ else
177
+ nil
178
+ end
179
+ end
180
+
181
+ end
182
+
@@ -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