morpheus-cli 8.1.2 → 9.0.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.
- checksums.yaml +4 -4
- data/Dockerfile +2 -2
- data/lib/morpheus/api/api_client.rb +8 -0
- data/lib/morpheus/api/groups_interface.rb +2 -1
- data/lib/morpheus/api/servers_interface.rb +0 -1
- data/lib/morpheus/api/support_bundles_interface.rb +46 -0
- data/lib/morpheus/api/systems_interface.rb +32 -0
- data/lib/morpheus/api/tokens_interface.rb +39 -0
- data/lib/morpheus/cli/cli_command.rb +6 -1
- data/lib/morpheus/cli/commands/clients_command.rb +60 -74
- data/lib/morpheus/cli/commands/clouds.rb +30 -2
- data/lib/morpheus/cli/commands/clusters.rb +5 -0
- data/lib/morpheus/cli/commands/execution_request_command.rb +6 -2
- data/lib/morpheus/cli/commands/groups.rb +46 -23
- data/lib/morpheus/cli/commands/hosts.rb +21 -14
- data/lib/morpheus/cli/commands/instances.rb +32 -6
- data/lib/morpheus/cli/commands/storage_volumes.rb +1 -1
- data/lib/morpheus/cli/commands/support_bundles_command.rb +606 -0
- data/lib/morpheus/cli/commands/systems.rb +606 -2
- data/lib/morpheus/cli/commands/tokens_command.rb +391 -0
- data/lib/morpheus/cli/commands/workflows.rb +16 -3
- data/lib/morpheus/cli/mixins/infrastructure_helper.rb +30 -14
- data/lib/morpheus/cli/mixins/provisioning_helper.rb +21 -7
- data/lib/morpheus/cli/version.rb +1 -1
- data/test/api/systems_interface_test.rb +26 -0
- data/test/cli/systems_test.rb +206 -0
- metadata +10 -2
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
require 'morpheus/cli/cli_command'
|
|
2
|
+
|
|
3
|
+
class Morpheus::Cli::SupportBundlesCommand
|
|
4
|
+
include Morpheus::Cli::CliCommand
|
|
5
|
+
|
|
6
|
+
set_command_description "View and manage support bundles"
|
|
7
|
+
set_command_name :'support-bundles'
|
|
8
|
+
register_subcommands :list, :get, :generate, :remove, :cancel, :download
|
|
9
|
+
|
|
10
|
+
def connect(opts)
|
|
11
|
+
@api_client = establish_remote_appliance_connection(opts)
|
|
12
|
+
@support_bundles_interface = @api_client.support_bundles
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def handle(args)
|
|
16
|
+
handle_subcommand(args)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def list(args)
|
|
20
|
+
options = {}
|
|
21
|
+
params = {}
|
|
22
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
|
23
|
+
opts.banner = subcommand_usage("[search]")
|
|
24
|
+
build_standard_list_options(opts, options)
|
|
25
|
+
opts.footer = "List support bundles."
|
|
26
|
+
end
|
|
27
|
+
optparse.parse!(args)
|
|
28
|
+
if args.count > 0
|
|
29
|
+
options[:phrase] = args.join(" ")
|
|
30
|
+
end
|
|
31
|
+
params.merge!(parse_list_options(options))
|
|
32
|
+
connect(options)
|
|
33
|
+
@support_bundles_interface.setopts(options)
|
|
34
|
+
if options[:dry_run]
|
|
35
|
+
print_dry_run @support_bundles_interface.dry.list(params)
|
|
36
|
+
return 0
|
|
37
|
+
end
|
|
38
|
+
json_response = @support_bundles_interface.list(params)
|
|
39
|
+
support_bundles = json_response['supportBundles']
|
|
40
|
+
render_response(json_response, options, 'supportBundles') do
|
|
41
|
+
print_h1 "Morpheus Support Bundles", parse_list_subtitles(options), options
|
|
42
|
+
if support_bundles.empty?
|
|
43
|
+
print cyan, "No support bundles found.", reset, "\n"
|
|
44
|
+
else
|
|
45
|
+
columns = [
|
|
46
|
+
{"ID" => lambda {|it| it['id'] } },
|
|
47
|
+
{"NAME" => lambda {|it| it['name'] } },
|
|
48
|
+
{"STATUS" => lambda {|it| format_support_bundle_status(it) } },
|
|
49
|
+
{"DELIVERY" => lambda {|it| it['deliveryStatus'] ? format_support_bundle_delivery_status(it) : '' } },
|
|
50
|
+
{"SIZE" => lambda {|it| it['contentLength'] ? format_bytes(it['contentLength']) : '' } },
|
|
51
|
+
{"CREATED" => lambda {|it| format_local_dt(it['startedAt']) } },
|
|
52
|
+
]
|
|
53
|
+
print as_pretty_table(support_bundles, columns, options)
|
|
54
|
+
print_results_pagination(json_response)
|
|
55
|
+
end
|
|
56
|
+
print reset, "\n"
|
|
57
|
+
end
|
|
58
|
+
return support_bundles.empty? ? [3, "no support bundles found"] : [0, nil]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def get(args)
|
|
62
|
+
options = {}
|
|
63
|
+
params = {}
|
|
64
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
|
65
|
+
opts.banner = subcommand_usage("[id]")
|
|
66
|
+
build_standard_get_options(opts, options)
|
|
67
|
+
opts.footer = <<-EOT
|
|
68
|
+
Get details about a support bundle.
|
|
69
|
+
[id] is required. This is the id of a support bundle.
|
|
70
|
+
EOT
|
|
71
|
+
end
|
|
72
|
+
optparse.parse!(args)
|
|
73
|
+
verify_args!(args: args, optparse: optparse, count: 1)
|
|
74
|
+
connect(options)
|
|
75
|
+
return _get(args[0], params, options)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def _get(id, params, options)
|
|
79
|
+
@support_bundles_interface.setopts(options)
|
|
80
|
+
if options[:dry_run]
|
|
81
|
+
print_dry_run @support_bundles_interface.dry.get(id, params)
|
|
82
|
+
return 0
|
|
83
|
+
end
|
|
84
|
+
bundle = find_support_bundle_by_id(id)
|
|
85
|
+
return 1 if bundle.nil?
|
|
86
|
+
json_response = {'supportBundle' => bundle}
|
|
87
|
+
render_response(json_response, options, 'supportBundle') do
|
|
88
|
+
print_h1 "Support Bundle Details", [], options
|
|
89
|
+
print cyan
|
|
90
|
+
columns = {
|
|
91
|
+
"ID" => 'id',
|
|
92
|
+
"Name" => 'name',
|
|
93
|
+
"UUID" => 'uuid',
|
|
94
|
+
"Status" => lambda {|it| format_support_bundle_status(it) },
|
|
95
|
+
"Status Message" => 'statusMessage',
|
|
96
|
+
"Categories" => 'categories',
|
|
97
|
+
"Log Range Start" => lambda {|it| format_local_dt(it['startDate']) },
|
|
98
|
+
"Log Range End" => lambda {|it| format_local_dt(it['endDate']) },
|
|
99
|
+
"Started At" => lambda {|it| format_local_dt(it['startedAt']) },
|
|
100
|
+
"Completed At" => lambda {|it| format_local_dt(it['completedAt']) },
|
|
101
|
+
"File Path" => 'filePath',
|
|
102
|
+
"Size" => lambda {|it| it['contentLength'] ? format_bytes(it['contentLength']) : '' },
|
|
103
|
+
"Content Type" => 'contentType',
|
|
104
|
+
"Checksum" => 'checksum',
|
|
105
|
+
"Storage Provider" => lambda {|it| it['storageProvider'] ? it['storageProvider']['name'] : '' },
|
|
106
|
+
"Delivery Status" => lambda {|it| it['deliveryStatus'] ? format_support_bundle_delivery_status(it) : nil },
|
|
107
|
+
"Delivered File" => lambda {|it| it['deliveredFilename'] },
|
|
108
|
+
}
|
|
109
|
+
print_description_list(columns, bundle, options)
|
|
110
|
+
print reset, "\n"
|
|
111
|
+
end
|
|
112
|
+
return 0
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def generate(args)
|
|
116
|
+
options = {}
|
|
117
|
+
params = {}
|
|
118
|
+
payload = {}
|
|
119
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
|
120
|
+
opts.banner = subcommand_usage("[options]")
|
|
121
|
+
opts.on('--all', "Include all eligible contents without being prompted to select them.") do
|
|
122
|
+
options[:all] = true
|
|
123
|
+
end
|
|
124
|
+
opts.on('--storage-provider ID', String, "Storage bucket to write the bundle to.") do |val|
|
|
125
|
+
options[:storage_provider_id] = val
|
|
126
|
+
end
|
|
127
|
+
opts.on('--log-range-start DATE', String, "Start of the log collection window (required). Accepts ISO 8601 formats like '2026-01-15' or '2026-01-15T00:00:00Z'.") do |val|
|
|
128
|
+
options[:start_date] = val
|
|
129
|
+
end
|
|
130
|
+
opts.on('--log-range-end DATE', String, "End of the log collection window. Accepts formats like '2026-01-15' or '2026-01-15T23:59:59'. Defaults to now.") do |val|
|
|
131
|
+
options[:end_date] = val
|
|
132
|
+
end
|
|
133
|
+
opts.on('--refresh [SECONDS]', String, "Poll until bundle generation is complete. Default interval is #{default_refresh_interval} seconds.") do |val|
|
|
134
|
+
options[:refresh_until_finished] = true
|
|
135
|
+
if !val.to_s.empty?
|
|
136
|
+
options[:refresh_interval] = val.to_f
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
build_standard_add_options(opts, options, [:auto_confirm])
|
|
140
|
+
opts.footer = <<-EOT
|
|
141
|
+
Generate a new support bundle. Bundle generation is asynchronous -- the bundle
|
|
142
|
+
will be queued and processed in the background.
|
|
143
|
+
|
|
144
|
+
Without --all, you will be prompted to select one or more categories,
|
|
145
|
+
then for each category you will select either specific content types
|
|
146
|
+
(standalone categories) or specific resource instances (resource-backed
|
|
147
|
+
categories). Pass --all to include every eligible content type
|
|
148
|
+
automatically without being prompted.
|
|
149
|
+
|
|
150
|
+
Use --log-range-start and --log-range-end to restrict the log collection window.
|
|
151
|
+
EOT
|
|
152
|
+
end
|
|
153
|
+
optparse.parse!(args)
|
|
154
|
+
verify_args!(args: args, optparse: optparse, count: 0)
|
|
155
|
+
connect(options)
|
|
156
|
+
@support_bundles_interface.setopts(options)
|
|
157
|
+
payload = parse_payload(options) || {}
|
|
158
|
+
|
|
159
|
+
# startDate is required — either via --log-range-start flag or payload
|
|
160
|
+
if options[:start_date].nil? && payload['startDate'].to_s.empty?
|
|
161
|
+
print_red_alert "--log-range-start is required"
|
|
162
|
+
return 1
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Optional storage provider -- prompt unless supplied via flag
|
|
166
|
+
if options[:storage_provider_id]
|
|
167
|
+
payload['storageProviderId'] = options[:storage_provider_id].to_i
|
|
168
|
+
elsif payload['storageProviderId'].to_s != ''
|
|
169
|
+
# honor payload-provided storage provider
|
|
170
|
+
elsif options[:options] && options[:options]['storageProviderId'].to_s != ''
|
|
171
|
+
payload['storageProviderId'] = options[:options]['storageProviderId'].to_i
|
|
172
|
+
else
|
|
173
|
+
buckets = @api_client.storage_providers.list({max: 10000})['storageBuckets'] rescue []
|
|
174
|
+
if buckets && !buckets.empty?
|
|
175
|
+
bucket_choices = buckets.collect { |it| {'name' => it['name'], 'value' => it['id']} }
|
|
176
|
+
storage_opt_type = {
|
|
177
|
+
'fieldName' => 'storageProviderId',
|
|
178
|
+
'fieldLabel' => 'Storage Provider',
|
|
179
|
+
'type' => 'select',
|
|
180
|
+
'description' => 'Storage bucket to write the bundle to. Leave blank to use the default.',
|
|
181
|
+
'required' => false,
|
|
182
|
+
'selectOptions' => bucket_choices,
|
|
183
|
+
}
|
|
184
|
+
storage_prompt = Morpheus::Cli::OptionTypes.prompt([storage_opt_type], options[:options], @api_client)
|
|
185
|
+
payload['storageProviderId'] = storage_prompt['storageProviderId'].to_i if storage_prompt['storageProviderId'].to_s != ''
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
if options[:start_date]
|
|
190
|
+
t = parse_time(options[:start_date])
|
|
191
|
+
if t.nil?
|
|
192
|
+
print_red_alert "Invalid --log-range-start value: #{options[:start_date]}"
|
|
193
|
+
return 1
|
|
194
|
+
end
|
|
195
|
+
payload['startDate'] = t.utc.iso8601
|
|
196
|
+
end
|
|
197
|
+
if options[:end_date]
|
|
198
|
+
t = parse_time(options[:end_date])
|
|
199
|
+
if t.nil?
|
|
200
|
+
print_red_alert "Invalid --log-range-end value: #{options[:end_date]}"
|
|
201
|
+
return 1
|
|
202
|
+
end
|
|
203
|
+
payload['endDate'] = t.utc.iso8601
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
if options[:all]
|
|
207
|
+
# empty contents = include everything; no payload key needed
|
|
208
|
+
elsif payload['contents'].is_a?(Array)
|
|
209
|
+
# honor payload-provided contents and skip interactive selection
|
|
210
|
+
else
|
|
211
|
+
# Fetch categories once up front
|
|
212
|
+
category_options = @api_client.options.options_for_source('supportBundles/supportBundleCategories', {})['data'] || []
|
|
213
|
+
if category_options.empty?
|
|
214
|
+
print yellow, "No support bundle categories are available.", reset, "\n"
|
|
215
|
+
return 1
|
|
216
|
+
end
|
|
217
|
+
category_select_options = category_options.map { |it| {'name' => it['label'] || it['name'], 'value' => it['value']} }
|
|
218
|
+
|
|
219
|
+
# Cache combined item lists per category so repeated visits don't re-fetch
|
|
220
|
+
combined_options_cache = {}
|
|
221
|
+
|
|
222
|
+
payload_contents = []
|
|
223
|
+
add_another_row = true
|
|
224
|
+
|
|
225
|
+
while add_another_row do
|
|
226
|
+
# Step 1: pick a category for this row
|
|
227
|
+
category_opt_type = {
|
|
228
|
+
'fieldName' => 'row_category',
|
|
229
|
+
'fieldLabel' => 'Category',
|
|
230
|
+
'type' => 'select',
|
|
231
|
+
'required' => true,
|
|
232
|
+
'description' => 'Select a category.',
|
|
233
|
+
'selectOptions' => category_select_options,
|
|
234
|
+
}
|
|
235
|
+
category_result = Morpheus::Cli::OptionTypes.prompt([category_opt_type], options[:options], @api_client)
|
|
236
|
+
category_value = category_result['row_category'].to_s.strip
|
|
237
|
+
break if category_value.empty?
|
|
238
|
+
|
|
239
|
+
category_label = (category_options.find { |c| c['value'].to_s == category_value } || {})['label'] || category_value
|
|
240
|
+
|
|
241
|
+
# Step 2: build (or retrieve cached) combined item list for this category
|
|
242
|
+
unless combined_options_cache.key?(category_value)
|
|
243
|
+
content_type_data = @api_client.options.options_for_source('supportBundles/contentTypes', {category: category_value})['data'] || []
|
|
244
|
+
opts = [] # select options shown to the user
|
|
245
|
+
full_value_map = {} # user-visible value -> full internal value
|
|
246
|
+
# Standalone entries: user types/sees the code
|
|
247
|
+
content_type_data.reject { |ct| ct['isResourceBacked'] }.each do |ct|
|
|
248
|
+
code = ct['code'] || ct['value']
|
|
249
|
+
opts << {'name' => ct['label'] || ct['name'], 'value' => code}
|
|
250
|
+
full_value_map[code] = code
|
|
251
|
+
end
|
|
252
|
+
# Resource instances: user types/sees the numeric resourceId
|
|
253
|
+
if content_type_data.any? { |ct| ct['isResourceBacked'] }
|
|
254
|
+
resources = @api_client.options.options_for_source('supportBundles/contentTypeResources', {category: category_value})['data'] || []
|
|
255
|
+
resources.each do |r|
|
|
256
|
+
rid = r['resourceId'].to_s
|
|
257
|
+
opts << {'name' => r['label'], 'value' => rid}
|
|
258
|
+
full_value_map[rid] = "#{r['code']}|#{r['resourceId']}"
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
combined_options_cache[category_value] = {opts: opts, map: full_value_map}
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
cached = combined_options_cache[category_value]
|
|
265
|
+
combined_opts = cached[:opts]
|
|
266
|
+
full_value_map = cached[:map]
|
|
267
|
+
if combined_opts.empty?
|
|
268
|
+
print yellow, "No items found for category '#{category_label}', skipping.", reset, "\n"
|
|
269
|
+
else
|
|
270
|
+
# Step 3: pick one item from the combined list
|
|
271
|
+
item_opt_type = {
|
|
272
|
+
'fieldName' => 'row_item',
|
|
273
|
+
'fieldLabel' => category_label,
|
|
274
|
+
'type' => 'select',
|
|
275
|
+
'required' => true,
|
|
276
|
+
'description' => "Select an item from '#{category_label}'.",
|
|
277
|
+
'selectOptions' => combined_opts,
|
|
278
|
+
}
|
|
279
|
+
item_result = Morpheus::Cli::OptionTypes.prompt([item_opt_type], options[:options], @api_client)
|
|
280
|
+
item_key = item_result['row_item'].to_s.strip
|
|
281
|
+
item_value = full_value_map[item_key] || item_key
|
|
282
|
+
|
|
283
|
+
unless item_value.empty?
|
|
284
|
+
if item_value.include?('|')
|
|
285
|
+
# Resource-backed: "code|resourceId"
|
|
286
|
+
code, resource_id = item_value.split('|', 2)
|
|
287
|
+
payload_contents << {'code' => code, 'resourceId' => resource_id.to_i} unless code.to_s.empty? || resource_id.to_s.strip.empty?
|
|
288
|
+
else
|
|
289
|
+
# Standalone
|
|
290
|
+
payload_contents << {'code' => item_value}
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
add_another_row = Morpheus::Cli::OptionTypes.confirm("Add another item?", {default: false})
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
if payload_contents.empty?
|
|
299
|
+
print yellow, "No items selected.", reset, "\n"
|
|
300
|
+
return 1
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
payload['contents'] = payload_contents
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
if options[:dry_run]
|
|
307
|
+
print_dry_run @support_bundles_interface.dry.create(payload, params)
|
|
308
|
+
return 0
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
confirm!("Are you sure you want to generate a support bundle?", options)
|
|
312
|
+
|
|
313
|
+
json_response = @support_bundles_interface.create(payload, params)
|
|
314
|
+
bundle = json_response['supportBundle']
|
|
315
|
+
render_response(json_response, options, 'supportBundle') do
|
|
316
|
+
print_green_success "Support bundle generation queued (ID: #{bundle['id']}, Status: #{bundle['status']})"
|
|
317
|
+
unless options[:refresh_until_finished]
|
|
318
|
+
print cyan, "Use `support-bundles get #{bundle['id']}` to check status.", reset, "\n"
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
if options[:refresh_until_finished]
|
|
323
|
+
bundle = refresh_until_bundle_complete(bundle, options)
|
|
324
|
+
return 0 if bundle.nil?
|
|
325
|
+
_get(bundle['id'], {}, options)
|
|
326
|
+
end
|
|
327
|
+
return 0
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def remove(args)
|
|
331
|
+
options = {}
|
|
332
|
+
params = {}
|
|
333
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
|
334
|
+
opts.banner = subcommand_usage("[id]")
|
|
335
|
+
opts.on('--force', "Force delete even if the bundle is active (PENDING, IN_PROGRESS, or CANCELLING).") do
|
|
336
|
+
params['force'] = true
|
|
337
|
+
end
|
|
338
|
+
build_standard_remove_options(opts, options)
|
|
339
|
+
opts.footer = <<-EOT
|
|
340
|
+
Delete a support bundle.
|
|
341
|
+
[id] is required. This is the id of the support bundle to delete.
|
|
342
|
+
Active bundles are rejected unless --force is passed.
|
|
343
|
+
EOT
|
|
344
|
+
end
|
|
345
|
+
optparse.parse!(args)
|
|
346
|
+
verify_args!(args: args, optparse: optparse, count: 1)
|
|
347
|
+
connect(options)
|
|
348
|
+
@support_bundles_interface.setopts(options)
|
|
349
|
+
if options[:dry_run]
|
|
350
|
+
print_dry_run @support_bundles_interface.dry.destroy(args[0], params)
|
|
351
|
+
return 0
|
|
352
|
+
end
|
|
353
|
+
bundle = find_support_bundle_by_id(args[0])
|
|
354
|
+
return 1 if bundle.nil?
|
|
355
|
+
confirm!("Are you sure you want to delete the support bundle #{bundle['name']} (#{bundle['id']})?", options)
|
|
356
|
+
json_response = @support_bundles_interface.destroy(bundle['id'], params)
|
|
357
|
+
render_response(json_response, options) do
|
|
358
|
+
print_green_success "Removed support bundle #{bundle['name']} (#{bundle['id']})"
|
|
359
|
+
end
|
|
360
|
+
return 0
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def cancel(args)
|
|
364
|
+
options = {}
|
|
365
|
+
params = {}
|
|
366
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
|
367
|
+
opts.banner = subcommand_usage("[id]")
|
|
368
|
+
build_standard_update_options(opts, options, [:auto_confirm])
|
|
369
|
+
opts.footer = <<-EOT
|
|
370
|
+
Cancel a support bundle.
|
|
371
|
+
[id] is required. This is the id of the support bundle to cancel.
|
|
372
|
+
EOT
|
|
373
|
+
end
|
|
374
|
+
optparse.parse!(args)
|
|
375
|
+
verify_args!(args: args, optparse: optparse, count: 1)
|
|
376
|
+
connect(options)
|
|
377
|
+
@support_bundles_interface.setopts(options)
|
|
378
|
+
if options[:dry_run]
|
|
379
|
+
print_dry_run @support_bundles_interface.dry.cancel(args[0], {}, params)
|
|
380
|
+
return 0
|
|
381
|
+
end
|
|
382
|
+
bundle = find_support_bundle_by_id(args[0])
|
|
383
|
+
return 1 if bundle.nil?
|
|
384
|
+
confirm!("Are you sure you want to cancel support bundle #{bundle['name']} (#{bundle['id']})?", options)
|
|
385
|
+
json_response = @support_bundles_interface.cancel(bundle['id'], {}, params)
|
|
386
|
+
render_response(json_response, options) do
|
|
387
|
+
print_green_success "Support bundle #{bundle['id']} cancellation requested."
|
|
388
|
+
end
|
|
389
|
+
return 0
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def download(args)
|
|
393
|
+
options = {}
|
|
394
|
+
params = {}
|
|
395
|
+
outfile = nil
|
|
396
|
+
do_overwrite = false
|
|
397
|
+
do_mkdir = false
|
|
398
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
|
399
|
+
opts.banner = subcommand_usage("[id] [file]")
|
|
400
|
+
opts.on('--file FILE', String, "Local destination path for the downloaded file.") do |val|
|
|
401
|
+
outfile = val
|
|
402
|
+
end
|
|
403
|
+
opts.on('-f', '--force', "Overwrite existing [file] if it exists.") do
|
|
404
|
+
do_overwrite = true
|
|
405
|
+
end
|
|
406
|
+
opts.on('-p', '--mkdir', "Create missing directories for [file] if they do not exist.") do
|
|
407
|
+
do_mkdir = true
|
|
408
|
+
end
|
|
409
|
+
build_common_options(opts, options, [:options, :json, :dry_run, :quiet, :remote])
|
|
410
|
+
opts.footer = <<-EOT
|
|
411
|
+
Download a support bundle file.
|
|
412
|
+
[id] is required. This is the id of a support bundle.
|
|
413
|
+
[file] is the destination filepath. Defaults to the bundle name in the current directory.
|
|
414
|
+
EOT
|
|
415
|
+
end
|
|
416
|
+
optparse.parse!(args)
|
|
417
|
+
if args.count < 1 || args.count > 2
|
|
418
|
+
print_error Morpheus::Terminal.angry_prompt
|
|
419
|
+
puts_error "wrong number of arguments, expected 1-2 and got #{args.count}\n#{optparse}"
|
|
420
|
+
return 1
|
|
421
|
+
end
|
|
422
|
+
bundle_id = args[0]
|
|
423
|
+
outfile = args[1] if args[1]
|
|
424
|
+
connect(options)
|
|
425
|
+
@support_bundles_interface.setopts(options)
|
|
426
|
+
|
|
427
|
+
if options[:dry_run]
|
|
428
|
+
print_dry_run @support_bundles_interface.dry.download(bundle_id, outfile || "support-bundle-#{bundle_id}.zip", params)
|
|
429
|
+
return 0
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
bundle = find_support_bundle_by_id(bundle_id)
|
|
433
|
+
return 1 if bundle.nil?
|
|
434
|
+
|
|
435
|
+
bundle_status = bundle['status'].to_s.downcase
|
|
436
|
+
unless ['completed', 'warning'].include?(bundle_status)
|
|
437
|
+
print_red_alert "Support bundle is not ready for download (status: #{bundle['status']})"
|
|
438
|
+
return 1
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# Resolve output filepath. Use the basename of filePath from the server (preserves
|
|
442
|
+
# the correct extension, e.g. .zip or .tar.gz). Fall back to deriving a name from
|
|
443
|
+
# the bundle name or id with an extension inferred from contentType.
|
|
444
|
+
if outfile.nil?
|
|
445
|
+
if bundle['filePath'].to_s != ''
|
|
446
|
+
outfile = File.basename(bundle['filePath'])
|
|
447
|
+
else
|
|
448
|
+
ext = case bundle['contentType'].to_s
|
|
449
|
+
when 'application/x-gzip' then '.tar.gz'
|
|
450
|
+
else '.zip'
|
|
451
|
+
end
|
|
452
|
+
outfile = "#{bundle['name'] || "support-bundle-#{bundle_id}"}#{ext}"
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
outfile = File.expand_path(outfile)
|
|
456
|
+
|
|
457
|
+
if Dir.exist?(outfile)
|
|
458
|
+
print_red_alert "[file] is invalid. It is the name of an existing directory: #{outfile}"
|
|
459
|
+
return 1
|
|
460
|
+
end
|
|
461
|
+
destination_dir = File.dirname(outfile)
|
|
462
|
+
if !Dir.exist?(destination_dir)
|
|
463
|
+
if do_mkdir
|
|
464
|
+
print cyan, "Creating local directory #{destination_dir}", reset, "\n"
|
|
465
|
+
FileUtils.mkdir_p(destination_dir)
|
|
466
|
+
else
|
|
467
|
+
print_red_alert "[file] is invalid. Directory not found: #{destination_dir}"
|
|
468
|
+
return 1
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
if File.exist?(outfile)
|
|
472
|
+
unless do_overwrite
|
|
473
|
+
print_error Morpheus::Terminal.angry_prompt
|
|
474
|
+
puts_error "[file] is invalid. File already exists: #{outfile}", "Use -f to overwrite the existing file."
|
|
475
|
+
return 1
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
unless options[:quiet]
|
|
480
|
+
print cyan, "Downloading support bundle #{bundle_id} to #{outfile} ... "
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
begin
|
|
484
|
+
http_response = @support_bundles_interface.download(bundle_id, outfile, params)
|
|
485
|
+
rescue RestClient::Exception => e
|
|
486
|
+
print_rest_exception(e, options)
|
|
487
|
+
return 1
|
|
488
|
+
rescue => e
|
|
489
|
+
if File.exist?(outfile) && File.file?(outfile)
|
|
490
|
+
Morpheus::Logging::DarkPrinter.puts "Deleting bad file download: #{outfile}" if Morpheus::Logging.debug?
|
|
491
|
+
File.delete(outfile)
|
|
492
|
+
end
|
|
493
|
+
raise e
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
success = http_response.code.to_i == 200
|
|
497
|
+
if success
|
|
498
|
+
unless options[:quiet]
|
|
499
|
+
print green, "SUCCESS", reset, "\n"
|
|
500
|
+
end
|
|
501
|
+
return 0
|
|
502
|
+
else
|
|
503
|
+
unless options[:quiet]
|
|
504
|
+
print red, "ERROR", reset, " HTTP #{http_response.code}", "\n"
|
|
505
|
+
end
|
|
506
|
+
if File.exist?(outfile) && File.file?(outfile)
|
|
507
|
+
Morpheus::Logging::DarkPrinter.puts "Deleting bad file download: #{outfile}" if Morpheus::Logging.debug?
|
|
508
|
+
File.delete(outfile)
|
|
509
|
+
end
|
|
510
|
+
if options[:debug]
|
|
511
|
+
puts_error http_response.inspect
|
|
512
|
+
end
|
|
513
|
+
return 1, "Error downloading file"
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
private
|
|
518
|
+
|
|
519
|
+
def default_refresh_interval
|
|
520
|
+
5
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
def default_refresh_timeout
|
|
524
|
+
300
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def refresh_until_bundle_complete(bundle, options)
|
|
528
|
+
if options[:refresh_interval].nil? || options[:refresh_interval].to_f <= 0
|
|
529
|
+
options[:refresh_interval] = default_refresh_interval
|
|
530
|
+
end
|
|
531
|
+
refresh_display_seconds = options[:refresh_interval] % 1.0 == 0 ? options[:refresh_interval].to_i : options[:refresh_interval]
|
|
532
|
+
print cyan, "Refreshing every #{refresh_display_seconds} seconds until complete...", reset, "\n" unless options[:quiet]
|
|
533
|
+
max_attempts = (default_refresh_timeout / options[:refresh_interval]).ceil
|
|
534
|
+
attempt = 0
|
|
535
|
+
while ['pending', 'in_progress', 'cancelling'].include?(bundle['status'].to_s.downcase) do
|
|
536
|
+
sleep(options[:refresh_interval])
|
|
537
|
+
print cyan, ".", reset unless options[:quiet]
|
|
538
|
+
bundle = @support_bundles_interface.get(bundle['id'])['supportBundle']
|
|
539
|
+
attempt += 1
|
|
540
|
+
if attempt >= max_attempts
|
|
541
|
+
print "\n" unless options[:quiet]
|
|
542
|
+
print yellow, "Timed out after #{default_refresh_timeout} seconds. Bundle is still #{bundle['status']}.", reset, "\n" unless options[:quiet]
|
|
543
|
+
print cyan, "Use `support-bundles get #{bundle['id']}` to check status.", reset, "\n" unless options[:quiet]
|
|
544
|
+
return nil
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
print "\n" unless options[:quiet]
|
|
548
|
+
bundle
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def find_support_bundle_by_id(id)
|
|
552
|
+
begin
|
|
553
|
+
json_response = @support_bundles_interface.get(id.to_i)
|
|
554
|
+
return json_response['supportBundle']
|
|
555
|
+
rescue RestClient::Exception => e
|
|
556
|
+
if e.response && e.response.code == 404
|
|
557
|
+
print_red_alert "Support bundle not found by id '#{id}'"
|
|
558
|
+
else
|
|
559
|
+
raise e
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def format_support_bundle_delivery_status(bundle, return_color = cyan)
|
|
565
|
+
out = ""
|
|
566
|
+
status_str = bundle['deliveryStatus'].to_s.upcase
|
|
567
|
+
case status_str
|
|
568
|
+
when 'DELIVERED'
|
|
569
|
+
out << "#{green}DELIVERED#{return_color}"
|
|
570
|
+
when 'IN_PROGRESS'
|
|
571
|
+
out << "#{cyan}IN PROGRESS#{return_color}"
|
|
572
|
+
when 'FAILED'
|
|
573
|
+
out << "#{red}FAILED#{return_color}"
|
|
574
|
+
when 'SUPERSEDED'
|
|
575
|
+
out << "#{yellow}SUPERSEDED#{return_color}"
|
|
576
|
+
else
|
|
577
|
+
out << "#{yellow}#{bundle['deliveryStatus']}#{return_color}"
|
|
578
|
+
end
|
|
579
|
+
out
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
def format_support_bundle_status(bundle, return_color = cyan)
|
|
583
|
+
out = ""
|
|
584
|
+
status_str = bundle['status'].to_s.downcase
|
|
585
|
+
case status_str
|
|
586
|
+
when 'completed'
|
|
587
|
+
out << "#{green}COMPLETED#{return_color}"
|
|
588
|
+
when 'warning'
|
|
589
|
+
out << "#{yellow}COMPLETED WITH WARNINGS#{return_color}"
|
|
590
|
+
when 'in_progress'
|
|
591
|
+
out << "#{cyan}IN PROGRESS#{return_color}"
|
|
592
|
+
when 'pending'
|
|
593
|
+
out << "#{cyan}PENDING#{return_color}"
|
|
594
|
+
when 'failed'
|
|
595
|
+
out << "#{red}FAILED#{return_color}"
|
|
596
|
+
when 'cancelling'
|
|
597
|
+
out << "#{yellow}CANCELLING#{return_color}"
|
|
598
|
+
when 'cancelled'
|
|
599
|
+
out << "#{yellow}CANCELLED#{return_color}"
|
|
600
|
+
else
|
|
601
|
+
out << "#{yellow}#{bundle['status']}#{return_color}"
|
|
602
|
+
end
|
|
603
|
+
out
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
end
|