morpheus-cli 5.2.1 → 5.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Dockerfile +1 -1
- data/lib/morpheus/api/invoices_interface.rb +12 -3
- data/lib/morpheus/cli/budgets_command.rb +389 -319
- data/lib/morpheus/cli/commands/standard/curl_command.rb +25 -10
- data/lib/morpheus/cli/commands/standard/history_command.rb +6 -2
- data/lib/morpheus/cli/dashboard_command.rb +260 -20
- data/lib/morpheus/cli/invoices_command.rb +63 -1
- data/lib/morpheus/cli/jobs_command.rb +94 -92
- data/lib/morpheus/cli/library_option_types_command.rb +5 -3
- data/lib/morpheus/cli/mixins/print_helper.rb +13 -6
- data/lib/morpheus/cli/version.rb +1 -1
- data/lib/morpheus/formatters.rb +21 -11
- metadata +2 -2
@@ -26,8 +26,8 @@ class Morpheus::Cli::CurlCommand
|
|
26
26
|
options = {}
|
27
27
|
optparse = Morpheus::Cli::OptionParser.new do|opts|
|
28
28
|
opts.banner = "Usage: morpheus curl [path] -- [*args]"
|
29
|
-
opts.on( '-p', '--pretty', "Print result as parsed JSON." ) do
|
30
|
-
options[:
|
29
|
+
opts.on( '-p', '--pretty', "Print result as parsed JSON. Alias for -j" ) do
|
30
|
+
options[:json] = true
|
31
31
|
end
|
32
32
|
opts.on( '-X', '--request METHOD', "HTTP request method. Default is GET" ) do |val|
|
33
33
|
curl_method = val
|
@@ -134,29 +134,44 @@ EOT
|
|
134
134
|
print reset
|
135
135
|
return 0
|
136
136
|
end
|
137
|
+
exit_code, err = 0, nil
|
137
138
|
# print cyan
|
138
139
|
# print "#{cyan}#{curl_cmd_str}#{reset}"
|
139
140
|
# print "\n\n"
|
140
141
|
print reset
|
141
142
|
# print result
|
142
143
|
curl_output = `#{curl_cmd}`
|
143
|
-
|
144
|
+
|
145
|
+
if $?.success? != true
|
146
|
+
exit_code = $?.exitstatus
|
147
|
+
err = "curl command exited non-zero"
|
148
|
+
end
|
149
|
+
json_response = {}
|
150
|
+
other_output = nil
|
151
|
+
if options[:json] || options[:yaml] || options[:csv]
|
144
152
|
output_lines = curl_output.split("\n")
|
145
153
|
last_line = output_lines.pop
|
146
154
|
if output_lines.size > 0
|
147
|
-
|
155
|
+
other_output = output_lines.join("\n")
|
148
156
|
end
|
149
157
|
begin
|
150
|
-
|
158
|
+
json_response = JSON.parse(last_line)
|
151
159
|
rescue => ex
|
152
|
-
|
153
|
-
|
160
|
+
puts_error curl_output
|
161
|
+
print_red_alert "failed to parse curl result as JSON data Error: #{ex.message}"
|
162
|
+
|
163
|
+
exit_code = 2
|
164
|
+
err = "failed to parse curl result as JSON data Error: #{ex.message}"
|
165
|
+
return exit_code, err
|
154
166
|
end
|
155
167
|
else
|
156
|
-
|
168
|
+
other_output = curl_output
|
157
169
|
end
|
158
|
-
|
159
|
-
|
170
|
+
curl_object_key = nil # json_response.keys.first
|
171
|
+
render_response(json_response, options, curl_object_key) do
|
172
|
+
puts other_output
|
173
|
+
end
|
174
|
+
return exit_code, err
|
160
175
|
end
|
161
176
|
|
162
177
|
def command_available?(cmd)
|
@@ -15,7 +15,7 @@ class Morpheus::Cli::HistoryCommand
|
|
15
15
|
def handle(args)
|
16
16
|
options = {show_pagination:false}
|
17
17
|
optparse = Morpheus::Cli::OptionParser.new do|opts|
|
18
|
-
opts.banner = "Usage: morpheus #{command_name}"
|
18
|
+
opts.banner = "Usage: morpheus #{command_name} [search]"
|
19
19
|
# -n is a hidden alias for -m
|
20
20
|
opts.on( '-n', '--max-commands MAX', "Alias for -m, --max option." ) do |val|
|
21
21
|
options[:max] = val
|
@@ -35,6 +35,7 @@ The --flush option can be used to purge the history.
|
|
35
35
|
Examples:
|
36
36
|
history
|
37
37
|
history -m 100
|
38
|
+
history "instances list"
|
38
39
|
history --flush
|
39
40
|
|
40
41
|
The most recently executed commands are seen by default. Use --reverse to see the oldest commands.
|
@@ -42,7 +43,10 @@ EOT
|
|
42
43
|
end
|
43
44
|
raw_cmd = "#{command_name} #{args.join(' ')}"
|
44
45
|
optparse.parse!(args)
|
45
|
-
verify_args!(args:args, count: 0, optparse:optparse)
|
46
|
+
# verify_args!(args:args, count: 0, optparse:optparse)
|
47
|
+
if args.count > 0
|
48
|
+
options[:phrase] = args.join(" ")
|
49
|
+
end
|
46
50
|
if options[:do_flush]
|
47
51
|
command_count = Morpheus::Cli::Shell.instance.history_commands_count
|
48
52
|
unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to flush your command history (#{format_number(command_count)} #{command_count == 1 ? 'command' : 'commands'})?")
|
@@ -4,8 +4,9 @@ require 'json'
|
|
4
4
|
|
5
5
|
class Morpheus::Cli::DashboardCommand
|
6
6
|
include Morpheus::Cli::CliCommand
|
7
|
+
include Morpheus::Cli::ProvisioningHelper
|
7
8
|
set_command_name :dashboard
|
8
|
-
|
9
|
+
set_command_description "View Morpheus Dashboard"
|
9
10
|
|
10
11
|
def initialize()
|
11
12
|
# @appliance_name, @appliance_url = Morpheus::Cli::Remote.active_appliance
|
@@ -28,34 +29,273 @@ class Morpheus::Cli::DashboardCommand
|
|
28
29
|
options = {}
|
29
30
|
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
30
31
|
opts.banner = usage
|
31
|
-
|
32
|
+
opts.on('-a', '--details', "Display all details: more instance usage stats, etc" ) do
|
33
|
+
options[:details] = true
|
34
|
+
end
|
35
|
+
build_standard_list_options(opts, options)
|
36
|
+
opts.footer = <<-EOT
|
37
|
+
View Morpheus Dashboard.
|
38
|
+
This includes instance and backup counts, favorite instances, monitoring and recent activity.
|
39
|
+
EOT
|
32
40
|
end
|
33
41
|
optparse.parse!(args)
|
34
|
-
|
42
|
+
verify_args!(args:args, optparse:optparse, count:0)
|
35
43
|
connect(options)
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
44
|
+
params = {}
|
45
|
+
params.merge!(parse_list_options(options))
|
46
|
+
@dashboard_interface.setopts(options)
|
47
|
+
if options[:dry_run]
|
48
|
+
print_dry_run @dashboard_interface.dry.get(params)
|
49
|
+
return
|
50
|
+
end
|
51
|
+
json_response = @dashboard_interface.get(params)
|
52
|
+
render_response(json_response, options) do
|
53
|
+
print_h1 "Morpheus Dashboard", [], options
|
54
|
+
|
55
|
+
## STATUS
|
56
|
+
|
57
|
+
status_column_definitions = {
|
58
|
+
"Instances" => lambda {|it|
|
59
|
+
format_number(it['instanceStats']['total']) rescue nil
|
60
|
+
},
|
61
|
+
"Running" => lambda {|it|
|
62
|
+
format_number(it['instanceStats']['running']) rescue nil
|
63
|
+
},
|
64
|
+
# "Used Storage" => lambda {|it|
|
65
|
+
# ((it['instanceStats']['maxStorage'].to_i > 0) ? ((it['instanceStats']['usedStorage'].to_f / it['instanceStats']['maxStorage'].to_f) * 100).round(1) : 0).to_s + '%' rescue nil
|
66
|
+
# },
|
67
|
+
}
|
68
|
+
print as_description_list(json_response, status_column_definitions, options)
|
69
|
+
# print reset,"\n"
|
70
|
+
|
71
|
+
stats = json_response['instanceStats']
|
72
|
+
if stats
|
73
|
+
print_h2 "Instance Usage", options
|
74
|
+
print_stats_usage(stats, {include: [:max_cpu, :avg_cpu, :memory, :storage]})
|
42
75
|
end
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
76
|
+
|
77
|
+
|
78
|
+
|
79
|
+
open_incident_count = json_response['monitoring']['openIncidents'] rescue (json_response['appStatus']['openIncidents'] rescue nil)
|
80
|
+
|
81
|
+
avg_response_time = json_response['monitoring']['avgResponseTime'] rescue nil
|
82
|
+
warning_apps = json_response['monitoring']['warningApps'] rescue 0
|
83
|
+
warning_checks = json_response['monitoring']['warningChecks'] rescue 0
|
84
|
+
fail_checks = json_response['monitoring']['failChecks'] rescue 0
|
85
|
+
fail_apps = json_response['monitoring']['failApps'] rescue 0
|
86
|
+
success_checks = json_response['monitoring']['successChecks'] rescue 0
|
87
|
+
success_apps = json_response['monitoring']['successApps'] rescue 0
|
88
|
+
monitoring_status_color = cyan
|
89
|
+
if fail_checks > 0 || fail_apps > 0
|
90
|
+
monitoring_status_color = red
|
91
|
+
elsif warning_checks > 0 || warning_apps > 0
|
92
|
+
monitoring_status_color = yellow
|
93
|
+
end
|
94
|
+
|
95
|
+
print_h2 "Monitoring"
|
96
|
+
|
97
|
+
monitoring_column_definitions = {
|
98
|
+
"Status" => lambda {|it|
|
99
|
+
if fail_apps > 0 # || fail_checks > 0
|
100
|
+
# check_summary = [fail_apps > 0 ? "#{fail_apps} Apps" : nil,fail_checks > 0 ? "#{fail_checks} Checks" : nil].compact.join(", ")
|
101
|
+
# red + "ERROR" + " (" + check_summary + ")" + cyan
|
102
|
+
red + "ERROR" + cyan
|
103
|
+
elsif warning_apps > 0 || warning_checks > 0
|
104
|
+
# check_summary = [warning_apps > 0 ? "#{warning_apps} Apps" : nil,warning_checks > 0 ? "#{warning_checks} Checks" : nil].compact.join(", ")
|
105
|
+
# red + "WARNING" + " (" + check_summary + ")" + cyan
|
106
|
+
yellow + "WARNING" + cyan
|
107
|
+
else
|
108
|
+
cyan + "HEALTHY" + cyan
|
109
|
+
end
|
110
|
+
},
|
111
|
+
# "Availability" => lambda {|it|
|
112
|
+
# # todo
|
113
|
+
# },
|
114
|
+
"Response Time" => lambda {|it|
|
115
|
+
# format_number(avg_response_time).to_s + "ms"
|
116
|
+
(avg_response_time.round).to_s + "ms"
|
117
|
+
},
|
118
|
+
"Open Incidents" => lambda {|it|
|
119
|
+
monitoring_status_color = cyan
|
120
|
+
# if fail_checks > 0 || fail_apps > 0
|
121
|
+
# monitoring_status_color = red
|
122
|
+
# elsif warning_checks > 0 || warning_apps > 0
|
123
|
+
# monitoring_status_color = yellow
|
124
|
+
# end
|
125
|
+
if open_incident_count.nil?
|
126
|
+
yellow + "n/a" + cyan + "\n"
|
127
|
+
elsif open_incident_count == 0
|
128
|
+
monitoring_status_color + "0 Open Incidents" + cyan
|
129
|
+
elsif open_incident_count == 1
|
130
|
+
monitoring_status_color + "1 Open Incident" + cyan
|
131
|
+
else
|
132
|
+
monitoring_status_color + "#{open_incident_count} Open Incidents" + cyan
|
133
|
+
end
|
134
|
+
}
|
135
|
+
}
|
136
|
+
#print as_description_list(json_response, monitoring_column_definitions, options)
|
137
|
+
print as_pretty_table([json_response], monitoring_column_definitions.upcase_keys!, options)
|
138
|
+
|
139
|
+
|
140
|
+
if json_response['logStats']
|
141
|
+
# todo: should come from monitoring.startMs-endMs
|
142
|
+
log_period_display = "7 Days"
|
143
|
+
print_h2 "Logs (#{log_period_display})", options
|
144
|
+
error_log_data = json_response['logStats']['data'].find {|it| it['key'].to_s.upcase == 'ERROR' }
|
145
|
+
error_count = error_log_data["count"] rescue 0
|
146
|
+
fatal_log_data = json_response['logStats']['data'].find {|it| it['key'].to_s.upcase == 'FATAL' }
|
147
|
+
fatal_count = fatal_log_data["count"] rescue 0
|
148
|
+
# total error is actaully error + fatal
|
149
|
+
total_error_count = error_count + fatal_count
|
150
|
+
# if total_error_count.nil?
|
151
|
+
# print yellow + "n/a" + cyan + "\n"
|
152
|
+
# elsif total_error_count == 0
|
153
|
+
# print cyan + "0 Errors" + cyan + "\n"
|
154
|
+
# elsif total_error_count == 1
|
155
|
+
# print red + "1 Error" + cyan + "\n"
|
156
|
+
# else
|
157
|
+
# print red + "#{total_error_count} Errors" + cyan + "\n"
|
158
|
+
# end
|
159
|
+
if total_error_count == 0
|
160
|
+
print cyan + "(0 Errors)" + cyan + "\n"
|
161
|
+
#print cyan + "0-0-0-0-0-0-0-0 (0 Errors)" + cyan + "\n"
|
162
|
+
end
|
163
|
+
if error_count > 0
|
164
|
+
if error_log_data["values"]
|
165
|
+
log_plot = ""
|
166
|
+
plot_index = 0
|
167
|
+
error_log_data["values"].each do |k, v|
|
168
|
+
if v.to_i == 0
|
169
|
+
log_plot << cyan + v.to_s
|
170
|
+
else
|
171
|
+
log_plot << red + v.to_s
|
172
|
+
end
|
173
|
+
if plot_index != error_log_data["values"].size - 1
|
174
|
+
log_plot << cyan + "-"
|
175
|
+
end
|
176
|
+
plot_index +=1
|
177
|
+
end
|
178
|
+
print log_plot
|
179
|
+
print " "
|
180
|
+
if error_count == 0
|
181
|
+
print cyan + "(0 Errors)" + cyan
|
182
|
+
elsif error_count == 1
|
183
|
+
print red + "(1 Errors)" + cyan
|
184
|
+
else
|
185
|
+
print red + "(#{error_count} Errors)" + cyan
|
186
|
+
end
|
187
|
+
print reset + "\n"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
if fatal_count > 0
|
191
|
+
if fatal_log_data["values"]
|
192
|
+
log_plot = ""
|
193
|
+
plot_index = 0
|
194
|
+
fatal_log_data["values"].each do |k, v|
|
195
|
+
if v.to_i == 0
|
196
|
+
log_plot << cyan + v.to_s
|
197
|
+
else
|
198
|
+
log_plot << red + v.to_s
|
199
|
+
end
|
200
|
+
if plot_index != fatal_log_data["values"].size - 1
|
201
|
+
log_plot << cyan + "-"
|
202
|
+
end
|
203
|
+
plot_index +=1
|
204
|
+
end
|
205
|
+
print log_plot
|
206
|
+
print " "
|
207
|
+
if fatal_count == 0
|
208
|
+
print cyan + "(0 FATAL)" + cyan
|
209
|
+
elsif fatal_count == 1
|
210
|
+
print red + "(1 FATAL)" + cyan
|
211
|
+
else
|
212
|
+
print red + "(#{fatal_count} FATAL)" + cyan
|
213
|
+
end
|
214
|
+
print reset + "\n"
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
print_h2 "Backups (7 Days)"
|
220
|
+
backup_status_column_definitions = {
|
221
|
+
# "Total" => lambda {|it|
|
222
|
+
# it['backups']['accountStats']['lastSevenDays']['completed'] rescue nil
|
223
|
+
# },
|
224
|
+
"Successful" => lambda {|it|
|
225
|
+
it['backups']['accountStats']['lastSevenDays']['successful'] rescue nil
|
226
|
+
},
|
227
|
+
"Failed" => lambda {|it|
|
228
|
+
n = it['backups']['accountStats']['lastSevenDays']['failed'] rescue nil
|
229
|
+
if n == 0
|
230
|
+
cyan + n.to_s + reset
|
231
|
+
else
|
232
|
+
red + n.to_s + reset
|
233
|
+
end
|
234
|
+
}
|
235
|
+
}
|
236
|
+
print as_description_list(json_response, backup_status_column_definitions, options)
|
237
|
+
#print as_pretty_table([json_response], backup_status_column_definitions, options)
|
238
|
+
# print reset,"\n"
|
239
|
+
|
240
|
+
favorite_instances = json_response["provisioning"]["favoriteInstances"] || [] rescue []
|
241
|
+
if favorite_instances.empty?
|
242
|
+
# print cyan, "No favorite instances.",reset,"\n"
|
47
243
|
else
|
244
|
+
print_h2 "My Instances"
|
245
|
+
favorite_instances_columns = {
|
246
|
+
"ID" => lambda {|instance|
|
247
|
+
instance['id']
|
248
|
+
},
|
249
|
+
"Name" => lambda {|instance|
|
250
|
+
instance['name']
|
251
|
+
},
|
252
|
+
"Type" => lambda {|instance|
|
253
|
+
instance['instanceType']['name'] rescue nil
|
254
|
+
},
|
255
|
+
"IP/PORT" => lambda {|instance|
|
256
|
+
format_instance_connection_string(instance)
|
257
|
+
},
|
258
|
+
"Status" => lambda {|it| format_instance_status(it) }
|
259
|
+
}
|
260
|
+
#print as_description_list(json_response, status_column_definitions, options)
|
261
|
+
print as_pretty_table(favorite_instances, favorite_instances_columns, options)
|
262
|
+
# print reset,"\n"
|
263
|
+
end
|
48
264
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
265
|
+
# RECENT ACTIVITY
|
266
|
+
activity = json_response["activity"] || json_response["recentActivity"] || []
|
267
|
+
print_h2 "Recent Activity", [], options
|
268
|
+
if activity.empty?
|
269
|
+
print cyan, "No activity found.",reset,"\n"
|
270
|
+
else
|
271
|
+
columns = [
|
272
|
+
# {"SEVERITY" => lambda {|record| format_activity_severity(record['severity']) } },
|
273
|
+
{"TYPE" => lambda {|record| record['activityType'] } },
|
274
|
+
{"NAME" => lambda {|record| record['name'] } },
|
275
|
+
{"RESOURCE" => lambda {|record| "#{record['objectType']} #{record['objectId']}" } },
|
276
|
+
{"MESSAGE" => lambda {|record| record['message'] || '' } },
|
277
|
+
{"USER" => lambda {|record| record['user'] ? record['user']['username'] : record['userName'] } },
|
278
|
+
#{"DATE" => lambda {|record| "#{format_duration_ago(record['ts'] || record['timestamp'])}" } },
|
279
|
+
{"DATE" => lambda {|record|
|
280
|
+
# show full time if searching for custom timerange, otherwise the default is to show relative time
|
281
|
+
if params['start'] || params['end'] || params['timeframe']
|
282
|
+
"#{format_local_dt(record['ts'] || record['timestamp'])}"
|
283
|
+
else
|
284
|
+
"#{format_duration_ago(record['ts'] || record['timestamp'])}"
|
285
|
+
end
|
286
|
+
|
287
|
+
} },
|
288
|
+
]
|
289
|
+
print as_pretty_table(activity, columns, options)
|
290
|
+
# print_results_pagination(json_response)
|
291
|
+
# print reset,"\n"
|
53
292
|
|
54
293
|
end
|
55
|
-
|
56
|
-
print_rest_exception(e, options)
|
57
|
-
exit 1
|
294
|
+
|
58
295
|
end
|
296
|
+
print reset,"\n"
|
297
|
+
return 0, nil
|
59
298
|
end
|
60
299
|
|
300
|
+
|
61
301
|
end
|
@@ -8,7 +8,7 @@ class Morpheus::Cli::InvoicesCommand
|
|
8
8
|
|
9
9
|
set_command_name :'invoices'
|
10
10
|
|
11
|
-
register_subcommands :list, :get, :refresh,
|
11
|
+
register_subcommands :list, :get, :update, :refresh,
|
12
12
|
:list_line_items, :get_line_item
|
13
13
|
|
14
14
|
def connect(opts)
|
@@ -591,6 +591,68 @@ EOT
|
|
591
591
|
end
|
592
592
|
end
|
593
593
|
|
594
|
+
def update(args)
|
595
|
+
options = {}
|
596
|
+
params = {}
|
597
|
+
payload = {}
|
598
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
599
|
+
opts.banner = subcommand_usage("[invoice] [options]")
|
600
|
+
opts.on('--tags LIST', String, "Tags in the format 'name:value, name:value'. This will add and remove tags.") do |val|
|
601
|
+
options[:tags] = val
|
602
|
+
end
|
603
|
+
opts.on('--add-tags TAGS', String, "Add Tags in the format 'name:value, name:value'. This will only add/update tags.") do |val|
|
604
|
+
options[:add_tags] = val
|
605
|
+
end
|
606
|
+
opts.on('--remove-tags TAGS', String, "Remove Tags in the format 'name, name:value'. This removes tags, the :value component is optional and must match if passed.") do |val|
|
607
|
+
options[:remove_tags] = val
|
608
|
+
end
|
609
|
+
build_standard_update_options(opts, options)
|
610
|
+
opts.footer = <<-EOT
|
611
|
+
Update an invoice.
|
612
|
+
[invoice] is required. This is the id of an invoice.
|
613
|
+
EOT
|
614
|
+
end
|
615
|
+
optparse.parse!(args)
|
616
|
+
verify_args!(args:args, optparse:optparse, count:1)
|
617
|
+
connect(options)
|
618
|
+
json_response = @invoices_interface.get(args[0])
|
619
|
+
invoice = json_response['invoice']
|
620
|
+
|
621
|
+
invoice_payload = parse_passed_options(options)
|
622
|
+
if options[:tags]
|
623
|
+
invoice_payload['tags'] = parse_metadata(options[:tags])
|
624
|
+
end
|
625
|
+
if options[:add_tags]
|
626
|
+
invoice_payload['addTags'] = parse_metadata(options[:add_tags])
|
627
|
+
end
|
628
|
+
if options[:remove_tags]
|
629
|
+
invoice_payload['removeTags'] = parse_metadata(options[:remove_tags])
|
630
|
+
end
|
631
|
+
|
632
|
+
payload = {}
|
633
|
+
if options[:payload]
|
634
|
+
payload = options[:payload]
|
635
|
+
payload.deep_merge!({'invoice' => invoice_payload})
|
636
|
+
else
|
637
|
+
payload.deep_merge!({'invoice' => invoice_payload})
|
638
|
+
if invoice_payload.empty?
|
639
|
+
raise_command_error "Specify at least one option to update.\n#{optparse}"
|
640
|
+
end
|
641
|
+
end
|
642
|
+
@invoices_interface.setopts(options)
|
643
|
+
if options[:dry_run]
|
644
|
+
print_dry_run @invoices_interface.dry.update(invoice['id'], payload)
|
645
|
+
return
|
646
|
+
end
|
647
|
+
json_response = @invoices_interface.update(invoice['id'], payload)
|
648
|
+
invoice = json_response['invoice']
|
649
|
+
render_response(json_response, options, 'invoice') do
|
650
|
+
print_green_success "Updated invoice #{invoice['id']}"
|
651
|
+
return _get(invoice["id"], options)
|
652
|
+
end
|
653
|
+
return 0, nil
|
654
|
+
end
|
655
|
+
|
594
656
|
def refresh(args)
|
595
657
|
options = {}
|
596
658
|
params = {}
|