blower 0.2.3 → 2.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/bin/blow +13 -17
- data/lib/blower/context.rb +107 -0
- data/lib/blower/host.rb +112 -0
- data/lib/blower/host_group.rb +36 -0
- data/lib/blower/logger.rb +42 -34
- data/lib/blower/mock_host.rb +42 -0
- data/lib/blower/version.rb +1 -1
- data/lib/blower.rb +4 -2
- metadata +20 -4
- data/lib/blower/huff.rb +0 -56
- data/lib/blower/puff.rb +0 -57
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 43a7308c9eaa1ad35d5a1c3942fad1850b32f3dc
|
4
|
+
data.tar.gz: 0d43517ed6d42111fd7aa0eabe3f30275ee1d227
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dd7de8fa74c568387e02af7afde83fa52d719a9ccb8658eae7a26ef9fa483b924654bb7bf9fe5f0b97d08b595a7652f72c4994810424b833058d14feb3dbed89
|
7
|
+
data.tar.gz: 200326faec848b91ea49c5422f8fb74843c17de9ac622559fd927e19099628a1a5de6d10573655ae5b4bd67056c9cca459b97758a4d3c160867bec3ba486a422
|
data/bin/blow
CHANGED
@@ -1,24 +1,20 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
|
3
2
|
require 'blower'
|
3
|
+
require 'pathname'
|
4
|
+
require 'optparse'
|
4
5
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
huff.log.fail "Usage: blow TASK..."
|
12
|
-
exit 1
|
13
|
-
end
|
14
|
-
|
15
|
-
args = ["hosts.ini", *args] if File.exist?("hosts.ini")
|
6
|
+
OptionParser.new do |opts|
|
7
|
+
opts.banner = "Usage: blow [options] task..."
|
8
|
+
opts.on "-d DIR", "Change directory" do |v|
|
9
|
+
Dir.chdir v
|
10
|
+
end
|
11
|
+
end.order!
|
16
12
|
|
17
13
|
begin
|
18
|
-
|
19
|
-
|
14
|
+
context = Blower::Context.new([".", Dir.pwd])
|
15
|
+
context.target = nil
|
16
|
+
context.run "Blowfile"
|
17
|
+
ARGV.each do |arg|
|
18
|
+
context.run arg
|
20
19
|
end
|
21
|
-
rescue RuntimeError => e
|
22
|
-
huff.log.fail e.message
|
23
|
-
exit 1
|
24
20
|
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Blower
|
4
|
+
|
5
|
+
class Context
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
attr_accessor :path
|
9
|
+
attr_accessor :location
|
10
|
+
attr_accessor :target
|
11
|
+
|
12
|
+
def initialize (path)
|
13
|
+
@path = path
|
14
|
+
@have_seen = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def log (message, level, &block)
|
18
|
+
message = "(on #{target.name}) " + message if target.respond_to?(:name)
|
19
|
+
Logger.instance.log(message, level, &block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def stage (message, &block)
|
23
|
+
log message, :info, &block
|
24
|
+
end
|
25
|
+
|
26
|
+
def one_host (name = nil, &block)
|
27
|
+
each_host [name || target.hosts.sample], &block
|
28
|
+
end
|
29
|
+
|
30
|
+
def each_host (hosts = target, parallel: true, &block)
|
31
|
+
hosts.each do |host|
|
32
|
+
ctx = dup
|
33
|
+
ctx.target = host
|
34
|
+
ctx.instance_exec(&block)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def reboot
|
39
|
+
begin
|
40
|
+
sh "shutdown -r now"
|
41
|
+
rescue IOError
|
42
|
+
sleep 0.1 while ping
|
43
|
+
sleep 1.0 until ping
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def sh (command)
|
48
|
+
log "execute #{command}", :debug
|
49
|
+
target.sh(command)
|
50
|
+
end
|
51
|
+
|
52
|
+
def cp (from, to)
|
53
|
+
log "upload #{Array(from).join(", ")} -> #{to}", :debug
|
54
|
+
target.cp(from, to)
|
55
|
+
end
|
56
|
+
|
57
|
+
def write (string, to)
|
58
|
+
log "upload data to #{to}", :debug
|
59
|
+
target.cp(StringIO.new(string), to)
|
60
|
+
end
|
61
|
+
|
62
|
+
def sh? (command)
|
63
|
+
log "execute #{command}", :debug
|
64
|
+
target.sh(command)
|
65
|
+
rescue Blower::Host::ExecuteError
|
66
|
+
false
|
67
|
+
end
|
68
|
+
|
69
|
+
def capture (command)
|
70
|
+
stdout = ""
|
71
|
+
target.sh(command, stdout)
|
72
|
+
stdout
|
73
|
+
end
|
74
|
+
|
75
|
+
def run (task)
|
76
|
+
files = []
|
77
|
+
@path.each do |dir|
|
78
|
+
name = File.join(dir, task)
|
79
|
+
name += ".rb" unless File.exist?(name)
|
80
|
+
if File.directory?(name)
|
81
|
+
dirtask = File.join(name, File.basename(@task))
|
82
|
+
dirtask += ".rb" unless File.exist?(dirtask)
|
83
|
+
name = dirtask
|
84
|
+
blowfile = File.join(name, "Blowfile")
|
85
|
+
files << blowfile if File.exist?(blowfile) && !@have_seen[blowfile]
|
86
|
+
end
|
87
|
+
files << name if File.exist?(name)
|
88
|
+
break unless files.empty?
|
89
|
+
end
|
90
|
+
if files.empty?
|
91
|
+
fail "can't find #{task}"
|
92
|
+
else
|
93
|
+
begin
|
94
|
+
old_task, @task = @task, task
|
95
|
+
files.each do |file|
|
96
|
+
@have_seen[file] = true
|
97
|
+
instance_eval(File.read(file), file)
|
98
|
+
end
|
99
|
+
ensure
|
100
|
+
@task = old_task
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
data/lib/blower/host.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'net/ssh'
|
2
|
+
require 'net/scp'
|
3
|
+
require 'monitor'
|
4
|
+
require 'colorize'
|
5
|
+
|
6
|
+
module Blower
|
7
|
+
|
8
|
+
class Host
|
9
|
+
include MonitorMixin
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
attr_accessor :name
|
13
|
+
attr_accessor :user
|
14
|
+
attr_accessor :data
|
15
|
+
|
16
|
+
def_delegators :data, :[], :[]=
|
17
|
+
|
18
|
+
class ExecuteError < Exception
|
19
|
+
attr_accessor :status
|
20
|
+
def initialize (status)
|
21
|
+
@status = status
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize (name, user = "root")
|
26
|
+
@name = name
|
27
|
+
@user = user
|
28
|
+
@data = {}
|
29
|
+
super()
|
30
|
+
end
|
31
|
+
|
32
|
+
def log
|
33
|
+
Logger.instance
|
34
|
+
end
|
35
|
+
|
36
|
+
def ping
|
37
|
+
Timeout.timeout(1) do
|
38
|
+
TCPSocket.new(name, 22).close
|
39
|
+
end
|
40
|
+
true
|
41
|
+
rescue Timeout::ExitException
|
42
|
+
false
|
43
|
+
rescue Errno::ECONNREFUSED
|
44
|
+
false
|
45
|
+
end
|
46
|
+
|
47
|
+
def cp (from, to, output = "")
|
48
|
+
synchronize do
|
49
|
+
if from.is_a?(String) || from.is_a?(Array)
|
50
|
+
to += "/" if to[-1] != "/" && from.is_a?(Array)
|
51
|
+
IO.popen(["rsync", "-z", "-r", "--progress", *from, "#{@user}@#{@name}:#{to}"],
|
52
|
+
in: :close, err: %i(child out)) do |io|
|
53
|
+
until io.eof?
|
54
|
+
begin
|
55
|
+
output << io.read_nonblock(100)
|
56
|
+
rescue IO::WaitReadable
|
57
|
+
IO.select([io])
|
58
|
+
retry
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
elsif from.is_a?(StringIO) or from.is_a?(IO)
|
63
|
+
log.info "string -> #{to}" unless quiet
|
64
|
+
ssh.scp.upload!(from, to)
|
65
|
+
else
|
66
|
+
fail "Don't know how to copy a #{from.class}: #{from}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
true
|
70
|
+
rescue => e
|
71
|
+
false
|
72
|
+
end
|
73
|
+
|
74
|
+
def sh (command, output = "")
|
75
|
+
synchronize do
|
76
|
+
result = nil
|
77
|
+
ch = ssh.open_channel do |ch|
|
78
|
+
ch.exec(command) do |_, success|
|
79
|
+
fail "failed to execute command" unless success
|
80
|
+
ch.on_data do |_, data|
|
81
|
+
output << data
|
82
|
+
end
|
83
|
+
ch.on_extended_data do |_, _, data|
|
84
|
+
output << data.colorize(:red)
|
85
|
+
end
|
86
|
+
ch.on_request("exit-status") { |_, data| result = data.read_long }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
ch.wait
|
90
|
+
if result != 0
|
91
|
+
log.fatal "failed on #{name}"
|
92
|
+
log.raw output
|
93
|
+
exit 1
|
94
|
+
end
|
95
|
+
result
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def each (&block)
|
100
|
+
block.(self)
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def ssh
|
106
|
+
@ssh = nil if @ssh && @ssh.closed?
|
107
|
+
@ssh ||= Net::SSH.start(name, user)
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Blower
|
2
|
+
|
3
|
+
class HostGroup
|
4
|
+
|
5
|
+
attr_accessor :hosts
|
6
|
+
attr_accessor :root
|
7
|
+
attr_accessor :location
|
8
|
+
|
9
|
+
def initialize (hosts)
|
10
|
+
@hosts = hosts
|
11
|
+
end
|
12
|
+
|
13
|
+
def sh (command = nil, *args, &block)
|
14
|
+
each do |host|
|
15
|
+
command = block.() if block
|
16
|
+
host.sh(command)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def cp (from, to)
|
21
|
+
each do |host|
|
22
|
+
host.cp(from, to)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def each (&block)
|
27
|
+
hosts.map do |host|
|
28
|
+
Thread.new do
|
29
|
+
block.(host)
|
30
|
+
end
|
31
|
+
end.map(&:join)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
data/lib/blower/logger.rb
CHANGED
@@ -1,44 +1,52 @@
|
|
1
|
-
require "
|
1
|
+
require "singleton"
|
2
2
|
|
3
3
|
module Blower
|
4
|
-
class Logger
|
5
4
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
5
|
+
class Logger
|
6
|
+
include MonitorMixin
|
7
|
+
include Singleton
|
8
|
+
|
9
|
+
COLORS = {
|
10
|
+
trace: :light_black,
|
11
|
+
debug: :light_black,
|
12
|
+
info: :blue,
|
13
|
+
warn: :yellow,
|
14
|
+
error: :red,
|
15
|
+
fatal: :magenta,
|
16
|
+
}
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@indent = 0
|
20
|
+
super()
|
21
|
+
end
|
17
22
|
|
18
|
-
|
19
|
-
log(
|
20
|
-
|
23
|
+
def trace (a=nil, *b, &c); log(a, :trace, *b, &c); end
|
24
|
+
def debug (a=nil, *b, &c); log(a, :debug, *b, &c); end
|
25
|
+
def info (a=nil, *b, &c); log(a, :info, *b, &c); end
|
26
|
+
def warn (a=nil, *b, &c); log(a, :warn, *b, &c); end
|
27
|
+
def error (a=nil, *b, &c); log(a, :error, *b, &c); end
|
28
|
+
def fatal (a=nil, *b, &c); log(a, :fatal, *b, &c); end
|
29
|
+
def raw (a=nil, *b, &c); log(a, nil, *b, &c); end
|
30
|
+
|
31
|
+
def log (message = nil, level = :info, &block)
|
32
|
+
if message
|
33
|
+
synchronize do
|
34
|
+
message = message.colorize(COLORS[level]) if level
|
35
|
+
puts " " * @indent + message
|
36
|
+
end
|
37
|
+
end
|
38
|
+
begin
|
39
|
+
@indent += 1
|
40
|
+
block.()
|
41
|
+
ensure
|
42
|
+
@indent -= 1
|
43
|
+
end if block
|
44
|
+
end
|
21
45
|
|
22
|
-
def win (message, **keys, &block)
|
23
|
-
log(message, :green, **keys, &block)
|
24
46
|
end
|
25
47
|
|
26
|
-
def log (
|
27
|
-
|
28
|
-
case line[-1]
|
29
|
-
when "\n", "\r"
|
30
|
-
else
|
31
|
-
line = line + "\n"
|
32
|
-
end
|
33
|
-
STDOUT.write " " * @logdent + prefix + (color ? line.colorize(color) : line)
|
34
|
-
end
|
35
|
-
begin
|
36
|
-
@logdent +=1
|
37
|
-
block.()
|
38
|
-
ensure
|
39
|
-
@logdent -= 1
|
40
|
-
end if block
|
48
|
+
def self.log (*args, &block)
|
49
|
+
Logger.instance.log(*args, &block)
|
41
50
|
end
|
42
51
|
|
43
52
|
end
|
44
|
-
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Blower
|
2
|
+
|
3
|
+
class MockHost
|
4
|
+
extend Forwardable
|
5
|
+
|
6
|
+
attr_accessor :log
|
7
|
+
attr_accessor :data
|
8
|
+
|
9
|
+
def_delegators :data, :[], :[]=
|
10
|
+
|
11
|
+
def initialize (name)
|
12
|
+
@log = Logger.new("mock #{name.ljust(15)} | ")
|
13
|
+
@data = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def sh (command, stdout: nil, stdin: nil)
|
17
|
+
log.info command
|
18
|
+
sleep rand * 0.1
|
19
|
+
end
|
20
|
+
|
21
|
+
def cp (from, to, quiet: false)
|
22
|
+
if from.is_a?(String)
|
23
|
+
to += File.basename(from) if to[-1] == "/"
|
24
|
+
log.info "#{from} -> #{to}" unless quiet
|
25
|
+
elsif from.is_a?(Array)
|
26
|
+
to += "/" unless to[-1] == "/"
|
27
|
+
log.info "#{from.join(", ")} -> #{to}" unless quiet
|
28
|
+
elsif from.is_a?(StringIO) or from.is_a?(IO)
|
29
|
+
log.info "string -> #{to}" unless quiet
|
30
|
+
else
|
31
|
+
fail "Don't know how to copy a #{from.class}: #{from}"
|
32
|
+
end
|
33
|
+
sleep rand * 0.1
|
34
|
+
end
|
35
|
+
|
36
|
+
def each (&block)
|
37
|
+
block.(self)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
data/lib/blower/version.rb
CHANGED
data/lib/blower.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: blower
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nathan Baum
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-11-
|
11
|
+
date: 2015-11-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: net-ssh
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '3.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: net-scp
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.2'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: colorize
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -47,9 +61,11 @@ extra_rdoc_files: []
|
|
47
61
|
files:
|
48
62
|
- bin/blow
|
49
63
|
- lib/blower.rb
|
50
|
-
- lib/blower/
|
64
|
+
- lib/blower/context.rb
|
65
|
+
- lib/blower/host.rb
|
66
|
+
- lib/blower/host_group.rb
|
51
67
|
- lib/blower/logger.rb
|
52
|
-
- lib/blower/
|
68
|
+
- lib/blower/mock_host.rb
|
53
69
|
- lib/blower/version.rb
|
54
70
|
homepage: http://www.github.org/nbaum/blower
|
55
71
|
licenses:
|
data/lib/blower/huff.rb
DELETED
@@ -1,56 +0,0 @@
|
|
1
|
-
module Blower
|
2
|
-
class Huff
|
3
|
-
|
4
|
-
attr_accessor :hosts, :log, :puffs, :env
|
5
|
-
|
6
|
-
def initialize ()
|
7
|
-
@hosts = ["127.0.0.1"]
|
8
|
-
@log = Logger.new
|
9
|
-
@logdent = 0
|
10
|
-
@puffs = Hash.new { |h, k| h[k] = Puff.new(self, k) }
|
11
|
-
@env = {}
|
12
|
-
end
|
13
|
-
|
14
|
-
def hosts (hosts)
|
15
|
-
log.info "new hosts: #{hosts.join(", ")}"
|
16
|
-
@hosts = hosts
|
17
|
-
end
|
18
|
-
|
19
|
-
def ruby (task)
|
20
|
-
instance_eval(File.read(task), task)
|
21
|
-
end
|
22
|
-
|
23
|
-
def shell (task)
|
24
|
-
@hosts.each do |host|
|
25
|
-
log.info "running #{task} on #{host}" do
|
26
|
-
puffs[host].shell(task)
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
def inventory (task)
|
32
|
-
hosts File.read(task).split
|
33
|
-
end
|
34
|
-
|
35
|
-
def run (name)
|
36
|
-
name = name.to_s
|
37
|
-
task = File.exist?(name) ? name : Dir[name + ".*"].first
|
38
|
-
if !task
|
39
|
-
fail "Can't find #{name}"
|
40
|
-
elsif File.directory?(task)
|
41
|
-
Dir.chdir(task) do
|
42
|
-
run("main")
|
43
|
-
end
|
44
|
-
elsif task =~ /\.rb$/
|
45
|
-
ruby(task)
|
46
|
-
elsif task =~ /\.sh$/
|
47
|
-
shell(task)
|
48
|
-
elsif task =~ /\.ini$/
|
49
|
-
inventory(task)
|
50
|
-
else
|
51
|
-
fail "Don't know what to do with #{task}"
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
end
|
56
|
-
end
|
data/lib/blower/puff.rb
DELETED
@@ -1,57 +0,0 @@
|
|
1
|
-
require "net/ssh"
|
2
|
-
require "shellwords"
|
3
|
-
|
4
|
-
module Blower
|
5
|
-
class Puff
|
6
|
-
|
7
|
-
attr_accessor :huff, :host, :log, :env
|
8
|
-
|
9
|
-
def initialize (huff, host)
|
10
|
-
@huff = huff
|
11
|
-
@log = huff.log
|
12
|
-
@host = host
|
13
|
-
@env = {}
|
14
|
-
end
|
15
|
-
|
16
|
-
def env2shell ()
|
17
|
-
huff.env.merge(@env).map do |name, value|
|
18
|
-
next unless name =~ /\A[A-Za-z\d_]+\z/
|
19
|
-
"#{name}=#{value.shellescape}"
|
20
|
-
end.join("\n") + "\n"
|
21
|
-
end
|
22
|
-
|
23
|
-
def shell (task)
|
24
|
-
Net::SSH.start(host, "root") do |ssh|
|
25
|
-
command = File.read(task)
|
26
|
-
status, signal = nil, nil
|
27
|
-
ssh.open_channel do |ch|
|
28
|
-
stdout, stderr = "", ""
|
29
|
-
ch.exec(env2shell + command) do |_, success|
|
30
|
-
fail "failed to execute command" unless success
|
31
|
-
ch.on_data do |_, data|
|
32
|
-
stdout << data
|
33
|
-
if i = stdout.rindex(/[\n\r]/)
|
34
|
-
data, stdout = stdout[0..i], (stdout[(i + 1)..-1] || "")
|
35
|
-
log.log data
|
36
|
-
end
|
37
|
-
end
|
38
|
-
ch.on_extended_data do |_, _, data|
|
39
|
-
stderr << data
|
40
|
-
if i = stderr.rindex(/[\n\r]/)
|
41
|
-
data, stderr = stderr[0..i], (stderr[(i + 1)..-1] || "")
|
42
|
-
log.fail data
|
43
|
-
end
|
44
|
-
end
|
45
|
-
ch.on_request("exit-status") { |_, data| status = data.read_long }
|
46
|
-
ch.on_request("exit-signal") { |_, data| signal = data.read_long }
|
47
|
-
end
|
48
|
-
end
|
49
|
-
ssh.loop
|
50
|
-
if status != 0
|
51
|
-
fail "exit status #{status}"
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
end
|
57
|
-
end
|