big_brother 0.1.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.
- data/.gitignore +21 -0
- data/.rake_commit +1 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/Rakefile +6 -0
- data/big_brother.gemspec +33 -0
- data/bin/bigbro +6 -0
- data/bin/ocf_big_brother +174 -0
- data/config.ru +5 -0
- data/lib/big_brother.rb +49 -0
- data/lib/big_brother/app.rb +30 -0
- data/lib/big_brother/cli.rb +82 -0
- data/lib/big_brother/cluster.rb +70 -0
- data/lib/big_brother/configuration.rb +48 -0
- data/lib/big_brother/ipvs.rb +51 -0
- data/lib/big_brother/logger.rb +11 -0
- data/lib/big_brother/node.rb +30 -0
- data/lib/big_brother/shell_executor.rb +18 -0
- data/lib/big_brother/status_file.rb +26 -0
- data/lib/big_brother/ticker.rb +28 -0
- data/lib/big_brother/version.rb +3 -0
- data/lib/sinatra/synchrony.rb +40 -0
- data/lib/thin/backends/tcp_server_with_callbacks.rb +20 -0
- data/lib/thin/callback_rack_handler.rb +14 -0
- data/lib/thin/callbacks.rb +19 -0
- data/spec/big_brother/app_spec.rb +127 -0
- data/spec/big_brother/cluster_spec.rb +102 -0
- data/spec/big_brother/configuration_spec.rb +81 -0
- data/spec/big_brother/ipvs_spec.rb +26 -0
- data/spec/big_brother/node_spec.rb +44 -0
- data/spec/big_brother/shell_executor_spec.rb +21 -0
- data/spec/big_brother/status_file_spec.rb +39 -0
- data/spec/big_brother/ticker_spec.rb +60 -0
- data/spec/big_brother/version_spec.rb +7 -0
- data/spec/big_brother_spec.rb +119 -0
- data/spec/spec_helper.rb +57 -0
- data/spec/support/example_config.yml +34 -0
- data/spec/support/factories/cluster_factory.rb +13 -0
- data/spec/support/factories/node_factory.rb +9 -0
- data/spec/support/ipvsadm +3 -0
- data/spec/support/mock_session.rb +14 -0
- data/spec/support/null_logger.rb +7 -0
- data/spec/support/playback_executor.rb +13 -0
- data/spec/support/recording_executor.rb +11 -0
- data/spec/support/stub_server.rb +22 -0
- metadata +271 -0
data/.gitignore
ADDED
data/.rake_commit
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--without-prompt=feature
|
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm ruby-1.9.3-p0@big_brother --create
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Braintree Payment Solutions LLC
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# BigBrother
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'big_brother'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install big_brother
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/big_brother.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/big_brother/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Braintree"]
|
6
|
+
gem.email = ["code@getbraintree.com"]
|
7
|
+
gem.description = %q{IPVS backend supervisor}
|
8
|
+
gem.summary = %q{Process to monitor and update weights for servers in an IPVS pool}
|
9
|
+
gem.homepage = "https://github.com/braintree/big_brother"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "big_brother"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = BigBrother::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency "thin", "~> 1.3.1"
|
19
|
+
gem.add_dependency "async-rack", "~> 0.5.1"
|
20
|
+
gem.add_dependency "sinatra", "~> 1.0"
|
21
|
+
gem.add_dependency "rack-fiber_pool", "~> 0.9"
|
22
|
+
gem.add_dependency "eventmachine", "> 1.0.0.beta.1", "< 1.0.0.beta.100"
|
23
|
+
gem.add_dependency "em-http-request", "~> 1.0"
|
24
|
+
gem.add_dependency "em-synchrony", "~> 1.0"
|
25
|
+
gem.add_dependency "em-resolv-replace", "~> 1.1"
|
26
|
+
gem.add_dependency "em-syslog", "~> 0.0.2"
|
27
|
+
|
28
|
+
gem.add_development_dependency "rspec", "~> 2.9.0"
|
29
|
+
gem.add_development_dependency "rack-test", "~> 0.6.1"
|
30
|
+
gem.add_development_dependency "rake"
|
31
|
+
gem.add_development_dependency "rake_commit", "~> 0.13"
|
32
|
+
gem.add_development_dependency "vagrant"
|
33
|
+
end
|
data/bin/bigbro
ADDED
data/bin/ocf_big_brother
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
#
|
3
|
+
# Michael Vallaly (Aug '11) Ver 1.0
|
4
|
+
#
|
5
|
+
# IPVS Supervisor daemon OCF resource handler script
|
6
|
+
#
|
7
|
+
|
8
|
+
AWK_BIN="/usr/bin/awk"
|
9
|
+
CURL_BIN="/usr/bin/curl"
|
10
|
+
EGREP_BIN="/bin/egrep"
|
11
|
+
|
12
|
+
CURL_TIMEOUT_SEC=5
|
13
|
+
SUPERVISOR_URL="http://127.0.0.1:9292"
|
14
|
+
|
15
|
+
#######################################################################
|
16
|
+
|
17
|
+
# Pull in OCF functions
|
18
|
+
. /usr/lib/ocf/resource.d/heartbeat/.ocf-shellfuncs
|
19
|
+
|
20
|
+
#######################################################################
|
21
|
+
|
22
|
+
meta_data() {
|
23
|
+
cat <<END
|
24
|
+
<?xml version="1.0"?>
|
25
|
+
<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">
|
26
|
+
<resource-agent name="Big Brother" version="0.9">
|
27
|
+
<version>1.0</version>
|
28
|
+
|
29
|
+
<longdesc lang="en">
|
30
|
+
This is the Big Brother Resource Agent. It enables the management
|
31
|
+
and monitoring of IPVS via the OCF resource API.
|
32
|
+
</longdesc>
|
33
|
+
<shortdesc lang="en">Big Brother resource agent</shortdesc>
|
34
|
+
|
35
|
+
<parameters>
|
36
|
+
<parameter name="cluster" unique="1" required="1">
|
37
|
+
<longdesc lang="en">
|
38
|
+
Cluster Name as defined in the Big Brother Configuration
|
39
|
+
</longdesc>
|
40
|
+
<shortdesc lang="en">Cluster Name</shortdesc>
|
41
|
+
<content type="string" default="" />
|
42
|
+
</parameter>
|
43
|
+
</parameters>
|
44
|
+
|
45
|
+
<actions>
|
46
|
+
<action name="start" timeout="20" />
|
47
|
+
<action name="stop" timeout="40" />
|
48
|
+
<action name="monitor" timeout="20" interval="10" depth="0" start-delay="0" />
|
49
|
+
<action name="reload" timeout="60" />
|
50
|
+
<action name="migrate_to" timeout="100" />
|
51
|
+
<action name="migrate_from" timeout="90" />
|
52
|
+
<action name="meta-data" timeout="5" />
|
53
|
+
<action name="validate-all" timeout="30" />
|
54
|
+
</actions>
|
55
|
+
</resource-agent>
|
56
|
+
END
|
57
|
+
}
|
58
|
+
|
59
|
+
#######################################################################
|
60
|
+
|
61
|
+
# don't exit on TERM, to test that lrmd makes sure that we do exit
|
62
|
+
trap sigterm_handler TERM
|
63
|
+
sigterm_handler() {
|
64
|
+
ocf_log info "Attempted to use TERM to bring us down. No such luck."
|
65
|
+
return
|
66
|
+
}
|
67
|
+
|
68
|
+
ipvs_cluster_usage() {
|
69
|
+
cat <<END
|
70
|
+
usage: $0 {start|stop|monitor|meta-data|validate-all}
|
71
|
+
|
72
|
+
Expects to have a fully populated OCF RA-compliant environment set.
|
73
|
+
END
|
74
|
+
}
|
75
|
+
|
76
|
+
ipvs_cluster_start() {
|
77
|
+
|
78
|
+
# Check if the protocol is already running
|
79
|
+
ipvs_cluster_monitor
|
80
|
+
if [ $? -eq ${OCF_SUCCESS} ]; then
|
81
|
+
return ${OCF_SUCCESS}
|
82
|
+
else
|
83
|
+
# Start the requested cluster
|
84
|
+
if [ `$CURL_BIN -m ${CURL_TIMEOUT_SEC} -X PUT -w "%{http_code}" -o /dev/null -s "${SUPERVISOR_URL}/cluster/${OCF_RESKEY_cluster}"` == 200 ]; then
|
85
|
+
return ${OCF_SUCCESS}
|
86
|
+
else
|
87
|
+
return ${OCF_ERR_GENERIC}
|
88
|
+
fi
|
89
|
+
fi
|
90
|
+
|
91
|
+
}
|
92
|
+
|
93
|
+
ipvs_cluster_stop() {
|
94
|
+
|
95
|
+
# Check if the protocol is already running
|
96
|
+
ipvs_cluster_monitor
|
97
|
+
if [ $? -eq ${OCF_SUCCESS} ]; then
|
98
|
+
# Stop the requested cluster
|
99
|
+
if [ `$CURL_BIN -m ${CURL_TIMEOUT_SEC} -X DELETE -w "%{http_code}" -o /dev/null -s "${SUPERVISOR_URL}/cluster/${OCF_RESKEY_cluster}"` == 200 ]; then
|
100
|
+
return ${OCF_SUCCESS}
|
101
|
+
else
|
102
|
+
return ${OCF_ERR_GENERIC}
|
103
|
+
fi
|
104
|
+
else
|
105
|
+
return ${OCF_SUCCESS}
|
106
|
+
fi
|
107
|
+
|
108
|
+
}
|
109
|
+
|
110
|
+
ipvs_cluster_monitor() {
|
111
|
+
|
112
|
+
local cluster_status
|
113
|
+
local http_status
|
114
|
+
|
115
|
+
# Check if the IPVS supervisor is running for the cluster
|
116
|
+
cluster_status=`$CURL_BIN -m ${CURL_TIMEOUT_SEC} -w '\nHTTP_Status: %{http_code}\n' -s "${SUPERVISOR_URL}/cluster/${OCF_RESKEY_cluster}"`
|
117
|
+
# If curl can't connect then we got bigger issues
|
118
|
+
if [ $? -ne 0 ]; then
|
119
|
+
return ${OCF_ERR_PERM};
|
120
|
+
fi
|
121
|
+
|
122
|
+
# Check for remote HTTP response code
|
123
|
+
http_status=`echo "${cluster_status}" |${EGREP_BIN} -e "^HTTP_Status: "|$AWK_BIN '{print $2}' |tr -d '[:alpha:][:punct:][:space:]' |head -1`
|
124
|
+
|
125
|
+
# We aren't running if we never get a status code back
|
126
|
+
if [ "${http_status}x" != "x" ]; then
|
127
|
+
if [ ${http_status} -eq 200 ]; then
|
128
|
+
if [ `echo ${cluster_status} |${EGREP_BIN} -e "^Running: "|$AWK_BIN '{print $2}'` == "true" ]; then
|
129
|
+
return ${OCF_SUCCESS}
|
130
|
+
fi
|
131
|
+
fi
|
132
|
+
fi
|
133
|
+
return ${OCF_NOT_RUNNING}
|
134
|
+
|
135
|
+
}
|
136
|
+
|
137
|
+
ipvs_cluster_validate_all() {
|
138
|
+
|
139
|
+
# Validate binary dependencies are executable
|
140
|
+
for req_bin in $AWK_BIN $CURL_BIN $EGREP_BIN; do
|
141
|
+
if [ ! -x "$req_bin" ]; then
|
142
|
+
ocf_log debug "Unable to execute (${req_bin})! Aborting.."
|
143
|
+
return ${OCF_ERR_INSTALLED}
|
144
|
+
fi
|
145
|
+
done
|
146
|
+
|
147
|
+
# Check if the IPVS supervisor knows about the cluster
|
148
|
+
if [ `$CURL_BIN -m ${CURL_TIMEOUT_SEC} -w '\nHTTP_Status: %{http_code}\n' -s "${SUPERVISOR_URL}/cluster/${OCF_RESKEY_cluster}" |${EGREP_BIN} -ce '^HTTP_Status: 200'` -eq 0 ]; then
|
149
|
+
return ${OCF_ERR_ARGS}
|
150
|
+
fi
|
151
|
+
|
152
|
+
return ${OCF_SUCCESS}
|
153
|
+
|
154
|
+
}
|
155
|
+
|
156
|
+
case $__OCF_ACTION in
|
157
|
+
meta-data) meta_data
|
158
|
+
exit ${OCF_SUCCESS}
|
159
|
+
;;
|
160
|
+
start) ipvs_cluster_start;;
|
161
|
+
stop) ipvs_cluster_stop;;
|
162
|
+
monitor) ipvs_cluster_monitor;;
|
163
|
+
validate-all) ipvs_cluster_validate_all;;
|
164
|
+
usage|help) ipvs_cluster_usage
|
165
|
+
exit ${OCF_SUCCESS}
|
166
|
+
;;
|
167
|
+
*) ipvs_cluster_usage
|
168
|
+
exit ${OCF_ERR_UNIMPLEMENTED}
|
169
|
+
;;
|
170
|
+
esac
|
171
|
+
|
172
|
+
rc=$?
|
173
|
+
ocf_log debug "${OCF_RESOURCE_INSTANCE} $__OCF_ACTION : $rc"
|
174
|
+
exit $rc
|
data/config.ru
ADDED
data/lib/big_brother.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'async-rack'
|
2
|
+
require 'sinatra/base'
|
3
|
+
require 'em-synchrony/em-http'
|
4
|
+
require 'em/syslog'
|
5
|
+
require 'thin'
|
6
|
+
require 'yaml'
|
7
|
+
|
8
|
+
require 'sinatra/synchrony'
|
9
|
+
|
10
|
+
require 'big_brother/app'
|
11
|
+
require 'big_brother/cluster'
|
12
|
+
require 'big_brother/configuration'
|
13
|
+
require 'big_brother/ipvs'
|
14
|
+
require 'big_brother/logger'
|
15
|
+
require 'big_brother/node'
|
16
|
+
require 'big_brother/shell_executor'
|
17
|
+
require 'big_brother/status_file'
|
18
|
+
require 'big_brother/ticker'
|
19
|
+
require 'big_brother/version'
|
20
|
+
|
21
|
+
require 'thin/callbacks'
|
22
|
+
require 'thin/backends/tcp_server_with_callbacks'
|
23
|
+
require 'thin/callback_rack_handler'
|
24
|
+
|
25
|
+
module BigBrother
|
26
|
+
class << self
|
27
|
+
attr_accessor :ipvs, :clusters, :config_dir, :logger
|
28
|
+
end
|
29
|
+
|
30
|
+
self.ipvs = IPVS.new
|
31
|
+
self.clusters = {}
|
32
|
+
self.logger = BigBrother::Logger.new
|
33
|
+
|
34
|
+
def self.configure(filename)
|
35
|
+
@config_file = filename
|
36
|
+
@clusters = BigBrother::Configuration.evaluate(filename)
|
37
|
+
BigBrother::Configuration.synchronize_with_ipvs(@clusters, BigBrother.ipvs.running_configuration)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.start_ticker!
|
41
|
+
Ticker.schedule!
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.reconfigure
|
45
|
+
Ticker.pause do
|
46
|
+
configure(@config_file)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module BigBrother
|
2
|
+
class App < Sinatra::Base
|
3
|
+
register Sinatra::Synchrony
|
4
|
+
|
5
|
+
get "/" do
|
6
|
+
BigBrother.clusters.map do |name, cluster|
|
7
|
+
"#{cluster}: #{cluster.monitored? ? "running" : "not running"}"
|
8
|
+
end.join("\n") + "\n"
|
9
|
+
end
|
10
|
+
|
11
|
+
before "/cluster/:name" do |name|
|
12
|
+
@cluster = BigBrother.clusters[name]
|
13
|
+
halt 404, "Cluster #{name} not found" if @cluster.nil?
|
14
|
+
end
|
15
|
+
|
16
|
+
get "/cluster/:name" do |name|
|
17
|
+
[200, "Running: #{@cluster.monitored?}"]
|
18
|
+
end
|
19
|
+
|
20
|
+
put "/cluster/:name" do |name|
|
21
|
+
halt 304 if @cluster.monitored?
|
22
|
+
@cluster.start_monitoring!
|
23
|
+
end
|
24
|
+
|
25
|
+
delete "/cluster/:name" do |name|
|
26
|
+
halt 304 unless @cluster.monitored?
|
27
|
+
@cluster.stop_monitoring!
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module BigBrother
|
2
|
+
class CLI < Rack::Server
|
3
|
+
class Options
|
4
|
+
def parse!(args)
|
5
|
+
args, options = args.dup, {}
|
6
|
+
|
7
|
+
opt_parser = OptionParser.new do |opts|
|
8
|
+
opts.banner = "Usage: bigbro [options]"
|
9
|
+
opts.on("-c", "--config=file", String,
|
10
|
+
"BigBrother configuration file", "Default: /etc/big_brother.conf") { |v| options[:big_brother_config] = v }
|
11
|
+
opts.on("-D", "--data-dir=path", String,
|
12
|
+
"BigBrother data directory", "Default: /etc/big_brother") { |v| options[:config_dir] = v }
|
13
|
+
|
14
|
+
opts.separator ""
|
15
|
+
|
16
|
+
opts.on("-p", "--port=port", Integer,
|
17
|
+
"Runs BigBrother on the specified port.", "Default: 9292") { |v| options[:Port] = v }
|
18
|
+
opts.on("-b", "--binding=ip", String,
|
19
|
+
"Binds BigBrother to the specified ip.", "Default: 0.0.0.0") { |v| options[:Host] = v }
|
20
|
+
opts.on("-d", "--daemon", "Make server run as a Daemon.") { options[:daemonize] = true }
|
21
|
+
opts.on("-P","--pid=pid",String,
|
22
|
+
"Specifies the PID file.",
|
23
|
+
"Default: rack.pid") { |v| options[:pid] = v }
|
24
|
+
|
25
|
+
opts.separator ""
|
26
|
+
|
27
|
+
opts.on("-h", "--help", "Show this help message.") { puts opts; exit }
|
28
|
+
end
|
29
|
+
|
30
|
+
opt_parser.parse! args
|
31
|
+
|
32
|
+
options[:config] = File.expand_path("../../config.ru", File.dirname(__FILE__))
|
33
|
+
options[:server] = 'thin-with-callbacks'
|
34
|
+
options[:backend] = Thin::Backends::TcpServerWithCallbacks
|
35
|
+
options
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize(options = nil)
|
40
|
+
super
|
41
|
+
end
|
42
|
+
|
43
|
+
def opt_parser
|
44
|
+
Options.new
|
45
|
+
end
|
46
|
+
|
47
|
+
def start
|
48
|
+
if !File.exists?(options[:big_brother_config])
|
49
|
+
puts "Could not find #{options[:big_brother_config]}. Specify correct location with -c file"
|
50
|
+
exit 1
|
51
|
+
end
|
52
|
+
|
53
|
+
BigBrother.config_dir = options[:config_dir]
|
54
|
+
|
55
|
+
Thin::Callbacks.after_connect do
|
56
|
+
EM.syslog_setup('0.0.0.0', 514)
|
57
|
+
BigBrother.logger.info "Starting big brother on port #{options[:Port]}"
|
58
|
+
|
59
|
+
EM.synchrony do
|
60
|
+
BigBrother.configure(options[:big_brother_config])
|
61
|
+
BigBrother.start_ticker!
|
62
|
+
end
|
63
|
+
|
64
|
+
Signal.trap("HUP") do
|
65
|
+
EM.synchrony do
|
66
|
+
BigBrother.logger.info "HUP trapped. Reconfiguring big brother"
|
67
|
+
BigBrother.reconfigure
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
super
|
73
|
+
end
|
74
|
+
|
75
|
+
def default_options
|
76
|
+
super.merge(
|
77
|
+
:big_brother_config => '/etc/big_brother.conf',
|
78
|
+
:config_dir => '/etc/big_brother'
|
79
|
+
)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|