rea-netscaler-cli 0.5.9
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +2 -0
- data/Gemfile.lock +57 -0
- data/README.markdown +37 -0
- data/Rakefile +16 -0
- data/bin/netscaler +5 -0
- data/lib/VERSION.yml +6 -0
- data/lib/netscaler/base_request.rb +52 -0
- data/lib/netscaler/cli.rb +246 -0
- data/lib/netscaler/config.rb +89 -0
- data/lib/netscaler/errors.rb +7 -0
- data/lib/netscaler/executor.rb +28 -0
- data/lib/netscaler/logging.rb +47 -0
- data/lib/netscaler/server/request.rb +35 -0
- data/lib/netscaler/server/response.rb +114 -0
- data/lib/netscaler/service/request.rb +42 -0
- data/lib/netscaler/service/response.rb +36 -0
- data/lib/netscaler/servicegroup/request.rb +46 -0
- data/lib/netscaler/servicegroup/response.rb +78 -0
- data/lib/netscaler/transaction.rb +68 -0
- data/lib/netscaler/vserver/request.rb +67 -0
- data/lib/netscaler/vserver/response.rb +115 -0
- data/spec/netscaler/cli_spec.rb +204 -0
- data/spec/netscaler/config_spec.rb +63 -0
- data/spec/netscaler/configs/bad-yaml.yml +2 -0
- data/spec/netscaler/configs/missing-username.yml +2 -0
- data/spec/netscaler/configs/simple-config.yml +8 -0
- data/spec/spec_helpers.rb +9 -0
- metadata +228 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
netscaler-cli (0)
|
5
|
+
choosy (>= 0.4.8)
|
6
|
+
highline (>= 1.6)
|
7
|
+
json_pure (>= 1.5.1)
|
8
|
+
log4r (>= 1.1.9)
|
9
|
+
savon (>= 0.7.9)
|
10
|
+
|
11
|
+
GEM
|
12
|
+
remote: http://rubygems.org/
|
13
|
+
specs:
|
14
|
+
ZenTest (4.5.0)
|
15
|
+
autotest (4.4.6)
|
16
|
+
ZenTest (>= 4.4.1)
|
17
|
+
autotest-notification (2.3.1)
|
18
|
+
autotest (~> 4.3)
|
19
|
+
builder (3.0.0)
|
20
|
+
choosy (0.4.9)
|
21
|
+
diff-lcs (1.1.2)
|
22
|
+
gyoku (0.4.4)
|
23
|
+
builder (>= 2.1.2)
|
24
|
+
highline (1.6.2)
|
25
|
+
httpi (0.9.4)
|
26
|
+
pyu-ntlm-http (>= 0.1.3.1)
|
27
|
+
rack
|
28
|
+
json_pure (1.5.1)
|
29
|
+
log4r (1.1.9)
|
30
|
+
nokogiri (1.4.4)
|
31
|
+
nori (0.2.3)
|
32
|
+
pyu-ntlm-http (0.1.3.1)
|
33
|
+
rack (1.3.0)
|
34
|
+
rspec (2.5.0)
|
35
|
+
rspec-core (~> 2.5.0)
|
36
|
+
rspec-expectations (~> 2.5.0)
|
37
|
+
rspec-mocks (~> 2.5.0)
|
38
|
+
rspec-core (2.5.1)
|
39
|
+
rspec-expectations (2.5.0)
|
40
|
+
diff-lcs (~> 1.1.2)
|
41
|
+
rspec-mocks (2.5.0)
|
42
|
+
savon (0.9.2)
|
43
|
+
builder (>= 2.1.2)
|
44
|
+
gyoku (>= 0.4.0)
|
45
|
+
httpi (>= 0.7.8)
|
46
|
+
nokogiri (>= 1.4.0)
|
47
|
+
nori (>= 0.2.0)
|
48
|
+
|
49
|
+
PLATFORMS
|
50
|
+
ruby
|
51
|
+
|
52
|
+
DEPENDENCIES
|
53
|
+
ZenTest
|
54
|
+
autotest
|
55
|
+
autotest-notification
|
56
|
+
netscaler-cli!
|
57
|
+
rspec
|
data/README.markdown
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# Netscaler CLI
|
2
|
+
|
3
|
+
This is a simple command line interface for accessing a Netscaler load balancer. It is currently alpha software, so use with caution.
|
4
|
+
|
5
|
+
# Installing
|
6
|
+
|
7
|
+
The command line tools can be installed with:
|
8
|
+
|
9
|
+
gem install netscaler-cli
|
10
|
+
|
11
|
+
# Using
|
12
|
+
|
13
|
+
The following commands are currently a part of the system:
|
14
|
+
|
15
|
+
* *netscaler vserver* -- An interface for enabling, disabling, querying, and binding responder policies to a specific virtual server.
|
16
|
+
* *netscaler service* -- An interface for enabling, disabling, querying, and binding servers to specific services.
|
17
|
+
* *netscaler server* -- An interface for enabling, disabling, querying, and binding servers to virtual servers.
|
18
|
+
|
19
|
+
Each command requires at least the --netscaler flag (which can be the full netscaler host name in the configuration file, or its alias, see below).
|
20
|
+
|
21
|
+
# Configuration
|
22
|
+
|
23
|
+
All of the commands rely upon a configuration file in the YAML format. By default, it looks for a file in your home directory
|
24
|
+
|
25
|
+
~/.netscaler-cli.yml
|
26
|
+
|
27
|
+
Each load balancer requires an entry in the file in the form:
|
28
|
+
|
29
|
+
netscaler.loadbalancer.somecompany.com:
|
30
|
+
username: 'some.username'
|
31
|
+
password: 'super!duper!secret!'
|
32
|
+
alias: prod
|
33
|
+
version: '9.2'
|
34
|
+
|
35
|
+
Multiple entries can be in the file; the password and the alias settings are not required. An alias can be used as a shortcut name on the command line for a particular netscaler server. However, if no password is given in the file for a given configuration, the tool will ask you for it.
|
36
|
+
|
37
|
+
The version information is optional, but breaking API changes were introduced in version 9.2 of Netscaler software, so some commands may fail if this isn't set for this Netscaler software version.
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
|
2
|
+
$LOAD_PATH.unshift File.expand_path("../spec", __FILE__)
|
3
|
+
|
4
|
+
require 'fileutils'
|
5
|
+
require 'rake'
|
6
|
+
require 'rubygems'
|
7
|
+
require 'rspec/core/rake_task'
|
8
|
+
require 'choosy/rake'
|
9
|
+
|
10
|
+
task :default => :spec
|
11
|
+
|
12
|
+
desc "Run the RSpec tests"
|
13
|
+
RSpec::Core::RakeTask.new :spec
|
14
|
+
|
15
|
+
desc "Cleans the gem files up."
|
16
|
+
task :clean => ['gem:clean']
|
data/bin/netscaler
ADDED
data/lib/VERSION.yml
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'netscaler/logging'
|
2
|
+
|
3
|
+
module Netscaler
|
4
|
+
class BaseRequest
|
5
|
+
include Netscaler::Logging
|
6
|
+
|
7
|
+
attr_reader :client
|
8
|
+
|
9
|
+
def initialize(client)
|
10
|
+
@client = client
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
def send_request(name, params, &block)
|
15
|
+
if params.nil? || params.empty?
|
16
|
+
raise Netscaler::TransactionError.new("The parameters were empty.")
|
17
|
+
end
|
18
|
+
|
19
|
+
params.delete(:empty)
|
20
|
+
|
21
|
+
log.debug("Calling: #{name}")
|
22
|
+
|
23
|
+
result = client.request name do
|
24
|
+
soap.namespace = Netscaler::NSCONFIG_NAMESPACE
|
25
|
+
soap.input = name
|
26
|
+
body = Hash.new
|
27
|
+
params.each do |k,v|
|
28
|
+
body[k.to_s] = v
|
29
|
+
end
|
30
|
+
soap.body = body
|
31
|
+
end
|
32
|
+
|
33
|
+
if log.debug?
|
34
|
+
require 'pp'
|
35
|
+
PP::pp(result.to_hash, $stderr, 80)
|
36
|
+
end
|
37
|
+
|
38
|
+
response = result.to_hash["#{name.to_s}_response".to_sym]
|
39
|
+
msg = response[:return][:message]
|
40
|
+
if msg !~ /^Done$/
|
41
|
+
log.error(response[:return][:message])
|
42
|
+
exit(1)
|
43
|
+
elsif block_given?
|
44
|
+
yield response
|
45
|
+
else
|
46
|
+
log.debug(msg)
|
47
|
+
end
|
48
|
+
|
49
|
+
result
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,246 @@
|
|
1
|
+
require 'netscaler/errors'
|
2
|
+
require 'netscaler/config'
|
3
|
+
require 'netscaler/executor'
|
4
|
+
require 'netscaler/server/request'
|
5
|
+
require 'netscaler/vserver/request'
|
6
|
+
require 'netscaler/service/request'
|
7
|
+
require 'netscaler/servicegroup/request'
|
8
|
+
require 'choosy'
|
9
|
+
|
10
|
+
module Netscaler
|
11
|
+
class CLI
|
12
|
+
|
13
|
+
def initialize(args)
|
14
|
+
@args = args.dup
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse!(propagate=nil)
|
18
|
+
command.parse!(@args, propagate)
|
19
|
+
end
|
20
|
+
|
21
|
+
def execute!
|
22
|
+
begin
|
23
|
+
command.execute!(@args)
|
24
|
+
rescue SystemExit
|
25
|
+
raise
|
26
|
+
rescue Netscaler::ConfigurationError => e
|
27
|
+
print_error(e.message)
|
28
|
+
exit 1
|
29
|
+
rescue Exception => e
|
30
|
+
STDERR.puts e.backtrace
|
31
|
+
print_error(e.message)
|
32
|
+
exit 1
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
def command
|
38
|
+
cmds = [vservers, servers, services, servicegroups]
|
39
|
+
@command ||= Choosy::SuperCommand.new :netscaler do
|
40
|
+
printer :standard, :color => true, :headers => [:bold, :blue], :max_width => 80
|
41
|
+
|
42
|
+
summary "This is a command line tool for interacting with Netscaler load balancer"
|
43
|
+
heading 'Description:'
|
44
|
+
para "There are several subcommands to do various things with the load balancer. Try 'netscaler help SUBCOMMAND' for more information about the particular command you want to use."
|
45
|
+
para "Note that you can supply a configuration file, which would normally be found under ~/.netscaler-cli.yml. That file describes the relationship between your Netscaler load balancers and the aliases, usernames, and passwords that you supply for them. The file is in the general format:"
|
46
|
+
para " netscaler.host.name.com:
|
47
|
+
alias: is_optional
|
48
|
+
usernamd: is_required
|
49
|
+
password: is_optional_but_querried_if_not_found"
|
50
|
+
|
51
|
+
# COMMANDS
|
52
|
+
heading 'Commands:'
|
53
|
+
cmds.each do |cmd|
|
54
|
+
command cmd
|
55
|
+
end
|
56
|
+
para
|
57
|
+
command :help
|
58
|
+
|
59
|
+
# OPTIONS
|
60
|
+
heading 'Global Options:'
|
61
|
+
string :netscaler, "The IP Address, hostname, or alias in the config file of the Netscaler load balancer. This is required." do
|
62
|
+
depends_on :config
|
63
|
+
required
|
64
|
+
|
65
|
+
validate do |arg, options|
|
66
|
+
reader = Netscaler::ConfigurationReader.new(options[:config])
|
67
|
+
config = reader[arg]
|
68
|
+
if config.nil?
|
69
|
+
die "the Netscaler address '#{arg}' is not defined in the configuration file"
|
70
|
+
else
|
71
|
+
options[:netscaler] = config
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
yaml :config, "The path to the netscaler configuration file. By default, it is ~/.netscaler-cli.yml" do
|
76
|
+
default File.join(ENV['HOME'], '.netscaler-cli.yml')
|
77
|
+
end
|
78
|
+
|
79
|
+
heading 'Informative:'
|
80
|
+
boolean_ :debug, "Print extra debug information"
|
81
|
+
boolean_ :json, "Prints out JSON instead of textual data"
|
82
|
+
help
|
83
|
+
version Choosy::Version.load_from_parent.to_s
|
84
|
+
end
|
85
|
+
end#command
|
86
|
+
|
87
|
+
def servers
|
88
|
+
Choosy::Command.new :server do |s|
|
89
|
+
executor Netscaler::Executor.new(Netscaler::Server::Request)
|
90
|
+
summary "Enables, disbles, or lists servers in the load balancer"
|
91
|
+
heading 'Description:'
|
92
|
+
para "This is a tool for enabling and disabling a server in a Netscaler load balancer. The name of the server is required, as is the address of the Netscaler load balancer."
|
93
|
+
para "By default, this command will tell you what the current status of the server is."
|
94
|
+
para "If you want to list all of the services, use the --list flag."
|
95
|
+
|
96
|
+
heading 'Options:'
|
97
|
+
enum :action, [:enable, :disable, :list, :status], "Either [enable, disable, list]. 'list' will ignore additional arguments. Default action is 'status'" do
|
98
|
+
default :status
|
99
|
+
end
|
100
|
+
arguments do
|
101
|
+
count 0..1 #:at_least => 0, :at_most => 1
|
102
|
+
metaname '[SERVER]'
|
103
|
+
validate do |args, options|
|
104
|
+
if args.length == 0
|
105
|
+
die "no server given to act upon" unless options[:action] == :list
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def vservers
|
113
|
+
Choosy::Command.new :vserver do
|
114
|
+
executor Netscaler::Executor.new(Netscaler::VServer::Request)
|
115
|
+
summary "Enables, disables, binds or unbinds policies, or lists virtual servers."
|
116
|
+
heading 'Description:'
|
117
|
+
para "This is a tool for acting upon virtual servers (VIPs) in a Netscaler load balancer. The name of the virtual server is required."
|
118
|
+
para "By default, this command will tell you what the current status of the server is."
|
119
|
+
para "If you want to list all of the virtual servers, use the --list flag."
|
120
|
+
|
121
|
+
heading 'Options:'
|
122
|
+
enum :action, [:enable, :disable, :list, :bind, :unbind, :status], "Either [enable, disable, list, bind, unbind, status]. 'bind' and 'unbind' require the additional '--policy' flag. 'list' will ignore additional arguments. Default action is 'status'." do
|
123
|
+
default :status
|
124
|
+
end
|
125
|
+
string :policy, "The name of the policy to bind/unbind." do
|
126
|
+
depends_on :action
|
127
|
+
default :unset
|
128
|
+
validate do |arg, options|
|
129
|
+
if [:bind, :unbind].include?(options[:action])
|
130
|
+
die "required by the 'bind/unbind' actions" if arg == :unset
|
131
|
+
else
|
132
|
+
die "only used with bind/unbind" unless arg == :unset
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
integer :Priority, "The integer priority of the policy to bind with. Default is 100." do
|
137
|
+
depends_on :action, :policy
|
138
|
+
default -1
|
139
|
+
validate do |arg, options|
|
140
|
+
if options[:action] == :bind
|
141
|
+
if arg == -1
|
142
|
+
options[:Priority] = 100
|
143
|
+
end
|
144
|
+
elsif arg != -1
|
145
|
+
die "only used with the bind action"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
arguments do
|
150
|
+
count 0..1 #:at_least => 0, :at_most => 1
|
151
|
+
metaname '[SERVER]'
|
152
|
+
validate do |args, options|
|
153
|
+
if args.length == 0
|
154
|
+
die "no virtual server given to act upon" unless options[:action] == :list
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def services
|
162
|
+
Choosy::Command.new :service do
|
163
|
+
executor Netscaler::Executor.new(Netscaler::Service::Request)
|
164
|
+
summary "Enables, disables, binds or unbinds from a virtual server, a given service."
|
165
|
+
heading 'Description:'
|
166
|
+
para "This is a tool for enabling and disabling services in a Netscaler load balancer. The name of the service is required, as is the address of the Netscaler load balancer."
|
167
|
+
|
168
|
+
heading 'Options:'
|
169
|
+
enum :action, [:enable, :disable, :bind, :unbind, :status], "Either [enable, disable, bind, unbind, status] of a service. 'bind' and 'unbind' require the '--vserver' flag. Default is 'status'." do
|
170
|
+
default :status
|
171
|
+
end
|
172
|
+
string :vserver, "The virtual server to bind/unbind this service to/from." do
|
173
|
+
depends_on :action
|
174
|
+
default :unset
|
175
|
+
validate do |arg, options|
|
176
|
+
if [:bind, :unbind].include?(options[:action])
|
177
|
+
die "requires the -v/--vserver flag" if arg == :unset
|
178
|
+
else
|
179
|
+
die "only used with bind/unbind" unless arg == :unset
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
arguments do
|
184
|
+
count 0..1 #:at_least => 0, :at_most => 1
|
185
|
+
metaname '[SERVICE]'
|
186
|
+
validate do |args, options|
|
187
|
+
if args.length == 0
|
188
|
+
die "no services given to act on" unless options[:action] == :list
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def servicegroups
|
196
|
+
Choosy::Command.new :servicegroup do
|
197
|
+
executor Netscaler::Executor.new(Netscaler::ServiceGroup::Request)
|
198
|
+
summary "Enables, disables, binds or unbinds from a virtual server, a given service group."
|
199
|
+
heading 'Description:'
|
200
|
+
para "This is a tool for enabling and disabling service groups in a Netscaler load balancer. The name of the service group is required, as is the address of the Netscaler load balancer."
|
201
|
+
|
202
|
+
heading 'Options:'
|
203
|
+
enum :action, [:enable, :disable, :bind, :unbind, :status], "Either [enable, disable, bind, unbind, status] of a service group. 'bind' and 'unbind' require the '--vserver' flag. Default is 'status'." do
|
204
|
+
default :status
|
205
|
+
end
|
206
|
+
string :vserver, "The virtual server to bind/unbind this service to/from." do
|
207
|
+
depends_on :action
|
208
|
+
default :unset
|
209
|
+
validate do |arg, options|
|
210
|
+
if [:bind, :unbind].include?(options[:action])
|
211
|
+
die "requires the -v/--vserver flag" if arg == :unset
|
212
|
+
else
|
213
|
+
die "only used with bind/unbind" unless arg == :unset
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
string :servername, "The name of the server that an individual service runs on (used when scoping the action to an individual service in a service group)." do
|
218
|
+
depends_on :action
|
219
|
+
end
|
220
|
+
string :port, "The port number that an individual service in bound to (used when scoping the action to an individual service in a service group)." do
|
221
|
+
depends_on :action
|
222
|
+
end
|
223
|
+
string :delay, "The delay (in seconds) to wait before disabled services transition to Out of Service. Default is 0 seconds (immediately)." do
|
224
|
+
depends_on :action
|
225
|
+
default "0"
|
226
|
+
end
|
227
|
+
arguments do
|
228
|
+
count 0..1 #:at_least => 0, :at_most => 1
|
229
|
+
metaname '[SERVICEGROUP]'
|
230
|
+
validate do |args, options|
|
231
|
+
if args.length == 0
|
232
|
+
die "no service group given to act on" unless options[:action] == :list
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
private
|
240
|
+
def print_error(e)
|
241
|
+
STDERR.puts "#{File.basename($0)}: #{e}"
|
242
|
+
STDERR.puts "Try '#{File.basename($0)} help' for more information"
|
243
|
+
exit 1
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'yaml'
|
3
|
+
require 'etc'
|
4
|
+
require 'highline/import'
|
5
|
+
|
6
|
+
module Netscaler
|
7
|
+
|
8
|
+
class ConfigurationReader
|
9
|
+
def initialize(yaml)
|
10
|
+
@servers = yaml
|
11
|
+
end
|
12
|
+
|
13
|
+
def [](name)
|
14
|
+
# First, try the aliases
|
15
|
+
@servers.each_key do |lbname|
|
16
|
+
found = @servers[lbname]
|
17
|
+
if found['alias'] == name
|
18
|
+
return create_config(lbname, found)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Next, check the actual server names
|
23
|
+
found = @servers[name]
|
24
|
+
if found.nil?
|
25
|
+
raise Netscaler::ConfigurationError.new("The specified Netscaler host was not found")
|
26
|
+
end
|
27
|
+
|
28
|
+
return create_config(name, found)
|
29
|
+
end
|
30
|
+
|
31
|
+
def load_balancers
|
32
|
+
@servers.keys
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.read_config_file(file)
|
36
|
+
if file.nil?
|
37
|
+
file = File.expand_path(".netscaler-cli.yml", Etc.getpwuid.dir)
|
38
|
+
end
|
39
|
+
|
40
|
+
if !File.exists?(file)
|
41
|
+
raise Netscaler::ConfigurationError.new("Unable to locate the netscaler-cli configuration file")
|
42
|
+
end
|
43
|
+
|
44
|
+
begin
|
45
|
+
yaml = File.read(file)
|
46
|
+
ConfigurationReader.new(YAML::load(yaml))
|
47
|
+
rescue Exception => e
|
48
|
+
raise Netscaler::ConfigurationError.new("Unable to load the netscaler-cli configuration file")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
def create_config(lbname, yaml)
|
54
|
+
if yaml['username'].nil?
|
55
|
+
raise Netscaler::ConfigurationError.new("No username was specified for the given Netscaler host")
|
56
|
+
end
|
57
|
+
|
58
|
+
Configuration.new(lbname, yaml['username'], yaml['password'], yaml['alias'], yaml['version'])
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class Configuration
|
63
|
+
attr_reader :host, :username, :password, :alias, :version
|
64
|
+
|
65
|
+
def initialize(host, username, password=nil, nalias=nil, version=nil)
|
66
|
+
@host = host
|
67
|
+
@username = username
|
68
|
+
@password = password
|
69
|
+
@alias = nalias
|
70
|
+
@version = if version
|
71
|
+
version.to_s
|
72
|
+
else
|
73
|
+
"9.2"
|
74
|
+
end
|
75
|
+
|
76
|
+
query_password
|
77
|
+
end
|
78
|
+
|
79
|
+
def query_password
|
80
|
+
if password.nil?
|
81
|
+
@password = ask("Netscaler password for host #{host}: ") {|q| q.echo = false }
|
82
|
+
end
|
83
|
+
|
84
|
+
if password.nil? || password.empty?
|
85
|
+
raise Netscaler::ConfigurationError.new("Unable to read the Netscaler password.")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|