3scale_toolbox 0.5.1 → 0.6.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/README.md +143 -23
- data/exe/3scale +10 -3
- data/lib/3scale_toolbox.rb +18 -0
- data/lib/3scale_toolbox/3scale_client_factory.rb +33 -0
- data/lib/3scale_toolbox/base_command.rb +52 -14
- data/lib/3scale_toolbox/cli.rb +26 -5
- data/lib/3scale_toolbox/cli/error_handler.rb +120 -0
- data/lib/3scale_toolbox/commands.rb +3 -9
- data/lib/3scale_toolbox/commands/3scale_command.rb +8 -6
- data/lib/3scale_toolbox/commands/copy_command.rb +4 -4
- data/lib/3scale_toolbox/commands/copy_command/copy_service.rb +40 -193
- data/lib/3scale_toolbox/commands/help_command.rb +1 -1
- data/lib/3scale_toolbox/commands/import_command.rb +6 -4
- data/lib/3scale_toolbox/commands/import_command/import_csv.rb +15 -41
- data/lib/3scale_toolbox/commands/import_command/openapi.rb +70 -0
- data/lib/3scale_toolbox/commands/import_command/openapi/create_mapping_rule_step.rb +18 -0
- data/lib/3scale_toolbox/commands/import_command/openapi/create_method_step.rb +39 -0
- data/lib/3scale_toolbox/commands/import_command/openapi/create_service_step.rb +69 -0
- data/lib/3scale_toolbox/commands/import_command/openapi/mapping_rule.rb +35 -0
- data/lib/3scale_toolbox/commands/import_command/openapi/method.rb +25 -0
- data/lib/3scale_toolbox/commands/import_command/openapi/operation.rb +22 -0
- data/lib/3scale_toolbox/commands/import_command/openapi/resource_reader.rb +49 -0
- data/lib/3scale_toolbox/commands/import_command/openapi/step.rb +45 -0
- data/lib/3scale_toolbox/commands/import_command/openapi/threescale_api_spec.rb +33 -0
- data/lib/3scale_toolbox/commands/remote_command.rb +36 -0
- data/lib/3scale_toolbox/commands/remote_command/remote_add.rb +47 -0
- data/lib/3scale_toolbox/commands/remote_command/remote_list.rb +29 -0
- data/lib/3scale_toolbox/commands/remote_command/remote_remove.rb +26 -0
- data/lib/3scale_toolbox/commands/remote_command/remote_rename.rb +42 -0
- data/lib/3scale_toolbox/commands/update_command.rb +4 -4
- data/lib/3scale_toolbox/commands/update_command/update_service.rb +45 -235
- data/lib/3scale_toolbox/configuration.rb +35 -0
- data/lib/3scale_toolbox/entities.rb +1 -0
- data/lib/3scale_toolbox/entities/service.rb +113 -0
- data/lib/3scale_toolbox/error.rb +8 -0
- data/lib/3scale_toolbox/helper.rb +37 -0
- data/lib/3scale_toolbox/remotes.rb +93 -0
- data/lib/3scale_toolbox/tasks.rb +10 -0
- data/lib/3scale_toolbox/tasks/copy_app_plans_task.rb +31 -0
- data/lib/3scale_toolbox/tasks/copy_limits_task.rb +36 -0
- data/lib/3scale_toolbox/tasks/copy_mapping_rules_task.rb +29 -0
- data/lib/3scale_toolbox/tasks/copy_methods_task.rb +29 -0
- data/lib/3scale_toolbox/tasks/copy_metrics_task.rb +33 -0
- data/lib/3scale_toolbox/tasks/copy_service_proxy_task.rb +12 -0
- data/lib/3scale_toolbox/tasks/copy_task.rb +26 -0
- data/lib/3scale_toolbox/tasks/destroy_mapping_rules_task.rb +22 -0
- data/lib/3scale_toolbox/tasks/helper_task.rb +25 -0
- data/lib/3scale_toolbox/tasks/update_service_settings_task.rb +32 -0
- data/lib/3scale_toolbox/version.rb +1 -1
- metadata +87 -11
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module ThreeScaleToolbox
|
4
|
+
module CLI
|
5
|
+
class ErrorHandler
|
6
|
+
def self.error_watchdog
|
7
|
+
new.error_watchdog { yield }
|
8
|
+
end
|
9
|
+
|
10
|
+
# Catches errors and prints nice diagnostic messages
|
11
|
+
def error_watchdog
|
12
|
+
# Run
|
13
|
+
yield
|
14
|
+
rescue StandardError, ScriptError => e
|
15
|
+
handle_error e
|
16
|
+
e
|
17
|
+
else
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def handle_error(error)
|
24
|
+
if expected_error?(error)
|
25
|
+
warn
|
26
|
+
warn "\e[1m\e[31mError: #{error.message}\e[0m"
|
27
|
+
else
|
28
|
+
print_error(error)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def expected_error?(error)
|
33
|
+
case error
|
34
|
+
when ThreeScaleToolbox::Error
|
35
|
+
true
|
36
|
+
else
|
37
|
+
false
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def print_error(error)
|
42
|
+
write_error(error, $stderr)
|
43
|
+
|
44
|
+
File.open('crash.log', 'w') do |io|
|
45
|
+
write_verbose_error(error, io)
|
46
|
+
end
|
47
|
+
|
48
|
+
write_section_header($stderr, 'Detailed information')
|
49
|
+
warn
|
50
|
+
warn 'A detailed crash log has been written to ./crash.log.'
|
51
|
+
end
|
52
|
+
|
53
|
+
def write_error(error, stream)
|
54
|
+
write_error_message(error, stream)
|
55
|
+
write_stack_trace(error, stream)
|
56
|
+
end
|
57
|
+
|
58
|
+
def write_error_message(error, stream)
|
59
|
+
write_section_header(stream, 'Message')
|
60
|
+
stream.puts "\e[1m\e[31m#{error.class}: #{error.message}\e[0m"
|
61
|
+
end
|
62
|
+
|
63
|
+
def write_stack_trace(error, stream)
|
64
|
+
write_section_header(stream, 'Backtrace')
|
65
|
+
stream.puts error.backtrace
|
66
|
+
end
|
67
|
+
|
68
|
+
def write_version_information(stream)
|
69
|
+
write_section_header(stream, 'Version Information')
|
70
|
+
stream.puts ThreeScaleToolbox::VERSION
|
71
|
+
end
|
72
|
+
|
73
|
+
def write_system_information(stream)
|
74
|
+
write_section_header(stream, 'System Information')
|
75
|
+
stream.puts Etc.uname.to_json
|
76
|
+
end
|
77
|
+
|
78
|
+
def write_installed_gems(stream)
|
79
|
+
write_section_header(stream, 'Installed gems')
|
80
|
+
gems_and_versions.each do |g|
|
81
|
+
stream.puts " #{g.first} #{g.last.join(', ')}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def write_load_paths(stream)
|
86
|
+
write_section_header(stream, 'Load paths')
|
87
|
+
$LOAD_PATH.each_with_index do |i, index|
|
88
|
+
stream.puts " #{index}. #{i}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def write_verbose_error(error, stream)
|
93
|
+
stream.puts "Crashlog created at #{Time.now}"
|
94
|
+
|
95
|
+
write_error_message(error, stream)
|
96
|
+
write_stack_trace(error, stream)
|
97
|
+
write_version_information(stream)
|
98
|
+
write_system_information(stream)
|
99
|
+
write_installed_gems(stream)
|
100
|
+
write_load_paths(stream)
|
101
|
+
end
|
102
|
+
|
103
|
+
def gems_and_versions
|
104
|
+
gems = {}
|
105
|
+
Gem::Specification.find_all.sort_by { |s| [s.name, s.version] }.each do |spec|
|
106
|
+
gems[spec.name] ||= []
|
107
|
+
gems[spec.name] << spec.version.to_s
|
108
|
+
end
|
109
|
+
gems
|
110
|
+
end
|
111
|
+
|
112
|
+
def write_section_header(stream, title)
|
113
|
+
stream.puts
|
114
|
+
|
115
|
+
stream.puts "===== #{title.upcase}:"
|
116
|
+
stream.puts
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -3,6 +3,7 @@ require '3scale_toolbox/commands/help_command'
|
|
3
3
|
require '3scale_toolbox/commands/copy_command'
|
4
4
|
require '3scale_toolbox/commands/import_command'
|
5
5
|
require '3scale_toolbox/commands/update_command'
|
6
|
+
require '3scale_toolbox/commands/remote_command'
|
6
7
|
|
7
8
|
module ThreeScaleToolbox
|
8
9
|
module Commands
|
@@ -10,15 +11,8 @@ module ThreeScaleToolbox
|
|
10
11
|
ThreeScaleToolbox::Commands::HelpCommand,
|
11
12
|
ThreeScaleToolbox::Commands::CopyCommand,
|
12
13
|
ThreeScaleToolbox::Commands::ImportCommand,
|
13
|
-
ThreeScaleToolbox::Commands::UpdateCommand
|
14
|
+
ThreeScaleToolbox::Commands::UpdateCommand,
|
15
|
+
ThreeScaleToolbox::Commands::RemoteCommand::RemoteCommand
|
14
16
|
].freeze
|
15
|
-
|
16
|
-
def self.service_valid_params
|
17
|
-
%w[
|
18
|
-
name backend_version deployment_option description
|
19
|
-
system_name end_user_registration_required
|
20
|
-
support_email tech_support_email admin_support_email
|
21
|
-
]
|
22
|
-
end
|
23
17
|
end
|
24
18
|
end
|
@@ -5,16 +5,18 @@ require '3scale_toolbox/base_command'
|
|
5
5
|
module ThreeScaleToolbox
|
6
6
|
module Commands
|
7
7
|
module ThreeScaleCommand
|
8
|
-
|
8
|
+
include ThreeScaleToolbox::Command
|
9
9
|
def self.command
|
10
10
|
Cri::Command.define do
|
11
11
|
name '3scale'
|
12
|
-
usage '3scale <command> [options]'
|
13
|
-
summary '3scale
|
14
|
-
description '3scale
|
15
|
-
|
12
|
+
usage '3scale <sub-command> [options]'
|
13
|
+
summary '3scale toolbox'
|
14
|
+
description '3scale toolbox to manage your API from the terminal.'
|
15
|
+
option :c, 'config-file', '3scale toolbox configuration file',
|
16
|
+
argument: :required, default: ThreeScaleToolbox.default_config_file
|
17
|
+
flag :v, :version, 'Prints the version of this command' do
|
16
18
|
puts ThreeScaleToolbox::VERSION
|
17
|
-
exit
|
19
|
+
exit 0
|
18
20
|
end
|
19
21
|
flag :k, :insecure, 'Proceed and operate even for server connections otherwise considered insecure'
|
20
22
|
flag :h, :help, 'show help for this command' do |_, cmd|
|
@@ -5,13 +5,13 @@ require '3scale_toolbox/commands/copy_command/copy_service'
|
|
5
5
|
module ThreeScaleToolbox
|
6
6
|
module Commands
|
7
7
|
module CopyCommand
|
8
|
-
|
8
|
+
include ThreeScaleToolbox::Command
|
9
9
|
def self.command
|
10
10
|
Cri::Command.define do
|
11
11
|
name 'copy'
|
12
|
-
usage 'copy <command> [options]'
|
13
|
-
summary '
|
14
|
-
description '3scale
|
12
|
+
usage 'copy <sub-command> [options]'
|
13
|
+
summary 'copy super command'
|
14
|
+
description 'Copy 3scale entities between tenants'
|
15
15
|
end
|
16
16
|
end
|
17
17
|
add_subcommand(CopyServiceSubcommand)
|
@@ -4,213 +4,60 @@ require '3scale_toolbox/base_command'
|
|
4
4
|
module ThreeScaleToolbox
|
5
5
|
module Commands
|
6
6
|
module CopyCommand
|
7
|
-
|
8
|
-
|
7
|
+
class CopyServiceSubcommand < Cri::CommandRunner
|
8
|
+
include ThreeScaleToolbox::Command
|
9
|
+
|
9
10
|
def self.command
|
10
11
|
Cri::Command.define do
|
11
12
|
name 'service'
|
12
13
|
usage 'service [opts] -s <src> -d <dst> <service_id>'
|
13
|
-
summary '
|
14
|
-
description '
|
14
|
+
summary 'copy service'
|
15
|
+
description 'will create a new services, copy existing proxy settings, metrics, methods, application plans and mapping rules.'
|
15
16
|
|
16
|
-
|
17
|
-
|
18
|
-
|
17
|
+
option :s, :source, '3scale source instance. Url or remote name', argument: :required
|
18
|
+
option :d, :destination, '3scale target instance. Url or remote name', argument: :required
|
19
|
+
option :t, 'target_system_name', 'Target system name', argument: :required
|
20
|
+
param :service_id
|
19
21
|
|
20
|
-
|
21
|
-
CopyServiceSubcommand.run opts, args
|
22
|
-
end
|
22
|
+
runner CopyServiceSubcommand
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
|
-
def
|
27
|
-
source = fetch_required_option(
|
28
|
-
destination = fetch_required_option(
|
29
|
-
system_name = fetch_required_option(
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
keys.map{ |key| first.fetch(key) } == keys.map{ |key| second.fetch(key) }
|
47
|
-
end
|
48
|
-
|
49
|
-
def self.provider_key_from_url(url)
|
50
|
-
url[/\w*@/][0..-2]
|
51
|
-
end
|
52
|
-
|
53
|
-
def self.endpoint_from_url(url)
|
54
|
-
url.sub /\w*@/, ''
|
26
|
+
def run
|
27
|
+
source = fetch_required_option(:source)
|
28
|
+
destination = fetch_required_option(:destination)
|
29
|
+
system_name = fetch_required_option(:target_system_name)
|
30
|
+
|
31
|
+
source_service = Entities::Service.new(id: arguments[:service_id],
|
32
|
+
remote: threescale_client(source))
|
33
|
+
target_service = create_new_service(source_service.show_service, destination, system_name)
|
34
|
+
puts "new service id #{target_service.id}"
|
35
|
+
context = create_context(source_service, target_service)
|
36
|
+
tasks = [
|
37
|
+
Tasks::CopyServiceProxyTask.new(context),
|
38
|
+
Tasks::CopyMethodsTask.new(context),
|
39
|
+
Tasks::CopyMetricsTask.new(context),
|
40
|
+
Tasks::CopyApplicationPlansTask.new(context),
|
41
|
+
Tasks::CopyLimitsTask.new(context),
|
42
|
+
Tasks::DestroyMappingRulesTask.new(context),
|
43
|
+
Tasks::CopyMappingRulesTask.new(context)
|
44
|
+
]
|
45
|
+
tasks.each(&:call)
|
55
46
|
end
|
56
47
|
|
48
|
+
private
|
57
49
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
target
|
62
|
-
|
63
|
-
end
|
64
|
-
|
65
|
-
def self.copy_service_params(original, system_name)
|
66
|
-
service_params = filter_params(Commands.service_valid_params, original)
|
67
|
-
service_params.tap do |hash|
|
68
|
-
hash['system_name'] = system_name if system_name
|
69
|
-
end
|
50
|
+
def create_context(source, target)
|
51
|
+
{
|
52
|
+
source: source,
|
53
|
+
target: target
|
54
|
+
}
|
70
55
|
end
|
71
56
|
|
72
|
-
def
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
endpoint: endpoint_from_url(source),
|
77
|
-
provider_key: provider_key_from_url(source),
|
78
|
-
verify_ssl: !insecure
|
79
|
-
)
|
80
|
-
client = ThreeScale::API.new(
|
81
|
-
endpoint: endpoint_from_url(destination),
|
82
|
-
provider_key: provider_key_from_url(destination),
|
83
|
-
verify_ssl: !insecure
|
84
|
-
)
|
85
|
-
|
86
|
-
service = source_client.show_service(service_id)
|
87
|
-
copy = client.create_service(copy_service_params(service, system_name))
|
88
|
-
|
89
|
-
raise "Service has not been saved. Errors: #{copy['errors']}" unless copy['errors'].nil?
|
90
|
-
|
91
|
-
service_copy_id = copy.fetch('id')
|
92
|
-
|
93
|
-
puts "new service id #{service_copy_id}"
|
94
|
-
|
95
|
-
proxy = source_client.show_proxy(service_id)
|
96
|
-
client.update_proxy(service_copy_id, proxy)
|
97
|
-
puts "updated proxy of #{service_copy_id} to match the original"
|
98
|
-
|
99
|
-
metrics = source_client.list_metrics(service_id)
|
100
|
-
metrics_copies = client.list_metrics(service_copy_id)
|
101
|
-
|
102
|
-
hits = metrics.find{ |metric| metric['system_name'] == 'hits' } or raise 'missing hits metric'
|
103
|
-
hits_copy = metrics_copies.find{ |metric| metric['system_name'] == 'hits' } or raise 'missing hits metric'
|
104
|
-
|
105
|
-
methods = source_client.list_methods(service_id, hits['id'])
|
106
|
-
methods_copies = client.list_methods(service_copy_id, hits_copy['id'])
|
107
|
-
|
108
|
-
puts "original service hits metric #{hits['id']} has #{methods.size} methods"
|
109
|
-
puts "copied service hits metric #{hits_copy['id']} has #{methods_copies.size} methods"
|
110
|
-
|
111
|
-
missing_methods = methods.reject { |method| methods_copies.find{|copy| compare_hashes(method, copy, ['system_name']) } }
|
112
|
-
|
113
|
-
puts "creating #{missing_methods.size} missing methods on copied service"
|
114
|
-
|
115
|
-
missing_methods.each do |method|
|
116
|
-
copy = { friendly_name: method['friendly_name'], system_name: method['system_name'] }
|
117
|
-
client.create_method(service_copy_id, hits_copy['id'], copy)
|
118
|
-
end
|
119
|
-
|
120
|
-
metrics_copies = client.list_metrics(service_copy_id)
|
121
|
-
|
122
|
-
puts "original service has #{metrics.size} metrics"
|
123
|
-
puts "copied service has #{metrics_copies.size} metrics"
|
124
|
-
|
125
|
-
missing_metrics = metrics.reject { |metric| metrics_copies.find{|copy| compare_hashes(metric, copy, ['system_name']) } }
|
126
|
-
|
127
|
-
missing_metrics.map do |metric|
|
128
|
-
metric.delete('links')
|
129
|
-
client.create_metric(service_copy_id, metric)
|
130
|
-
end
|
131
|
-
|
132
|
-
puts "created #{missing_metrics.size} metrics on the copied service"
|
133
|
-
|
134
|
-
plans = source_client.list_service_application_plans(service_id)
|
135
|
-
plan_copies = client.list_service_application_plans(service_copy_id)
|
136
|
-
|
137
|
-
puts "original service has #{plans.size} application plans "
|
138
|
-
puts "copied service has #{plan_copies.size} application plans"
|
139
|
-
|
140
|
-
missing_application_plans = plans.reject { |plan| plan_copies.find{|copy| plan.fetch('system_name') == copy.fetch('system_name') } }
|
141
|
-
|
142
|
-
puts "copied service missing #{missing_application_plans.size} application plans"
|
143
|
-
|
144
|
-
missing_application_plans.each do |plan|
|
145
|
-
plan.delete('links')
|
146
|
-
plan.delete('default') # TODO: handle default plan
|
147
|
-
|
148
|
-
if plan.delete('custom') # TODO: what to do with custom plans?
|
149
|
-
puts "skipping custom plan #{plan}"
|
150
|
-
else
|
151
|
-
client.create_application_plan(service_copy_id, plan)
|
152
|
-
end
|
153
|
-
end
|
154
|
-
|
155
|
-
application_plan_mapping = client.list_service_application_plans(service_copy_id).map do |plan_copy|
|
156
|
-
plan = plans.find{|plan| plan.fetch('system_name') == plan_copy.fetch('system_name') }
|
157
|
-
|
158
|
-
[plan['id'], plan_copy['id']]
|
159
|
-
end
|
160
|
-
|
161
|
-
metrics_mapping = client.list_metrics(service_copy_id).map do |copy|
|
162
|
-
metric = metrics.find{|metric| metric.fetch('system_name') == copy.fetch('system_name') }
|
163
|
-
metric ||= {}
|
164
|
-
|
165
|
-
[metric['id'], copy['id']]
|
166
|
-
end.to_h
|
167
|
-
|
168
|
-
puts "destroying all mapping rules of the copy which have been created by default"
|
169
|
-
client.list_mapping_rules(service_copy_id).each do |mapping_rule|
|
170
|
-
client.delete_mapping_rule(service_copy_id, mapping_rule['id'])
|
171
|
-
end
|
172
|
-
|
173
|
-
mapping_rules = source_client.list_mapping_rules(service_id)
|
174
|
-
mapping_rules_copy = client.list_mapping_rules(service_copy_id)
|
175
|
-
|
176
|
-
puts "the original service has #{mapping_rules.size} mapping rules"
|
177
|
-
puts "the copy has #{mapping_rules_copy.size} mapping rules"
|
178
|
-
|
179
|
-
unique_mapping_rules_copy = mapping_rules_copy.dup
|
180
|
-
|
181
|
-
missing_mapping_rules = mapping_rules.reject do |mapping_rule|
|
182
|
-
matching_metric = unique_mapping_rules_copy.find do |copy|
|
183
|
-
compare_hashes(mapping_rule, copy, %w(pattern http_method delta)) &&
|
184
|
-
metrics_mapping.fetch(mapping_rule.fetch('metric_id')) == copy.fetch('metric_id')
|
185
|
-
end
|
186
|
-
|
187
|
-
unique_mapping_rules_copy.delete(matching_metric)
|
188
|
-
end
|
189
|
-
|
190
|
-
puts "missing #{missing_mapping_rules.size} mapping rules"
|
191
|
-
|
192
|
-
missing_mapping_rules.each do |mapping_rule|
|
193
|
-
mapping_rule.delete('links')
|
194
|
-
mapping_rule['metric_id'] = metrics_mapping.fetch(mapping_rule.delete('metric_id'))
|
195
|
-
client.create_mapping_rule(service_copy_id, mapping_rule)
|
196
|
-
end
|
197
|
-
puts "created #{missing_mapping_rules.size} mapping rules"
|
198
|
-
|
199
|
-
puts "extra #{unique_mapping_rules_copy.size} mapping rules"
|
200
|
-
puts unique_mapping_rules_copy.each{|rule| rule.delete('links') }
|
201
|
-
|
202
|
-
application_plan_mapping.each do |original_id, copy_id|
|
203
|
-
limits = source_client.list_application_plan_limits(original_id)
|
204
|
-
limits_copy = client.list_application_plan_limits(copy_id)
|
205
|
-
|
206
|
-
missing_limits = limits.reject { |limit| limits_copy.find{|limit_copy| limit.fetch('period') == limit_copy.fetch('period') } }
|
207
|
-
|
208
|
-
missing_limits.each do |limit|
|
209
|
-
limit.delete('links')
|
210
|
-
client.create_application_plan_limit(copy_id, metrics_mapping.fetch(limit.fetch('metric_id')), limit)
|
211
|
-
end
|
212
|
-
puts "copied application plan #{copy_id} is missing #{missing_limits.size} from the original plan #{original_id}"
|
213
|
-
end
|
57
|
+
def create_new_service(service, destination, system_name)
|
58
|
+
Entities::Service.create(remote: threescale_client(destination),
|
59
|
+
service: service,
|
60
|
+
system_name: system_name)
|
214
61
|
end
|
215
62
|
end
|
216
63
|
end
|