dployr 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +25 -19
- data/lib/dployr/cli.rb +13 -16
- data/lib/dployr/commands/base.rb +43 -0
- data/lib/dployr/commands/config.rb +29 -12
- data/lib/dployr/commands/execute.rb +13 -16
- data/lib/dployr/commands/provision_test.rb +14 -18
- data/lib/dployr/commands/ssh.rb +40 -0
- data/lib/dployr/commands/start.rb +17 -18
- data/lib/dployr/commands/stop_destroy.rb +19 -21
- data/lib/dployr/commands/utils.rb +40 -0
- data/lib/dployr/compute/aws.rb +76 -77
- data/lib/dployr/compute/gce.rb +134 -0
- data/lib/dployr/config/file_utils.rb +7 -9
- data/lib/dployr/configuration.rb +3 -1
- data/lib/dployr/init.rb +1 -2
- data/lib/dployr/scripts/default_hooks.rb +2 -9
- data/lib/dployr/scripts/hook.rb +4 -9
- data/lib/dployr/scripts/local_shell.rb +29 -0
- data/lib/dployr/scripts/scp.rb +1 -9
- data/lib/dployr/scripts/shell.rb +11 -23
- data/lib/dployr/scripts/ssh.rb +23 -0
- data/lib/dployr/utils.rb +0 -21
- data/lib/dployr/version.rb +1 -1
- data/lib/dployr.rb +4 -0
- data/spec/commands_util_spec.rb +108 -0
- data/{config → spec/fixtures/config}/Dployrfile.yml +19 -8
- data/spec/utils_spec.rb +0 -68
- metadata +15 -5
- /data/{config → spec/fixtures/config}/hello/hello.sh +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9e18de4be7a56b2227ef830bd0159a0fe9e675a9
|
4
|
+
data.tar.gz: b2d9e30ad637839636e952069b43768d8476db1c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5151faf23cfdc2b09998a78a78d25aaaf00123b234275fc9a903fc5d72c0b11907142c9b046e49142f3fc9ccf3ba2c0e65abaee599adf53827e0535c2c4f4dc9
|
7
|
+
data.tar.gz: 5fb7a5c245f56633f6759f12d22a5530ce8795b22fdc85a7ae3089de2f0fd6f07e2c82a177688a31abc13d65e634cb9bc5895265e93b4c9a3aeabfec71b8f99c
|
data/README.md
CHANGED
@@ -54,10 +54,10 @@ or a YAML file (adding the `.yml` or `.yaml` extension)
|
|
54
54
|
|
55
55
|
Each configuration level supports the followings members:
|
56
56
|
|
57
|
-
- **attributes** `Object`
|
58
|
-
- **scripts** `Array`
|
57
|
+
- **attributes** `Object` Custom attrbutes to apply to the current template
|
58
|
+
- **scripts** `Array|Object`
|
59
59
|
- **providers** `Object`
|
60
|
-
- **authentication** `Object`
|
60
|
+
- **authentication** `Object` (optional)
|
61
61
|
- **extends** `String|Array` Allows to inherits the current config object from others
|
62
62
|
|
63
63
|
#### Templating
|
@@ -132,11 +132,13 @@ default:
|
|
132
132
|
instance_type: n1-standard-1
|
133
133
|
network: liberty-gce
|
134
134
|
scripts:
|
135
|
-
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
135
|
+
pre-start:
|
136
|
+
-
|
137
|
+
path: ./scripts/routes_allregions.sh
|
138
|
+
start:
|
139
|
+
-
|
140
|
+
args: "%{name}"
|
141
|
+
path: ./scripts/updatedns.sh
|
140
142
|
|
141
143
|
|
142
144
|
custom:
|
@@ -157,12 +159,13 @@ custom:
|
|
157
159
|
attributes:
|
158
160
|
instance_type: m1.large
|
159
161
|
scripts:
|
160
|
-
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
162
|
+
post-start:
|
163
|
+
-
|
164
|
+
args:
|
165
|
+
- "%{name}"
|
166
|
+
- "%{type}"
|
167
|
+
- "%{domain}"
|
168
|
+
path: ./scripts/.sh
|
166
169
|
-
|
167
170
|
args:
|
168
171
|
- "%{hydra}"
|
@@ -182,22 +185,25 @@ Usage: dployr <command> [options]
|
|
182
185
|
|
183
186
|
Commands
|
184
187
|
|
185
|
-
|
188
|
+
start start instances
|
186
189
|
halt stop instances
|
190
|
+
destroy destroy instances
|
187
191
|
status retrieve the instances status
|
188
192
|
test run remote test in instances
|
189
193
|
deploy start, provision and test running instances
|
190
194
|
provision instance provisioning
|
191
|
-
config generate configuration in YAML
|
195
|
+
config generate configuration in YAML from Dployrfile
|
196
|
+
execute run custom stages
|
192
197
|
init create a sample Dployrfile
|
193
198
|
|
194
199
|
Options
|
195
200
|
|
196
|
-
-e, --environment ENV environment to pass to the instances
|
197
201
|
-n, --name NAME template name identifier to load
|
202
|
+
-f, --file PATH custom config file path to load
|
198
203
|
-a, --attributes ATTRS aditional attributes to pass to the configuration in matrix query format
|
199
|
-
-p, --provider
|
200
|
-
-r, --region
|
204
|
+
-p, --provider VALUES provider to use (allow multiple values comma-separated)
|
205
|
+
-r, --region REGION region to use (allow multiple values comma-separated)
|
206
|
+
-v, -V, --version version
|
201
207
|
-h, --help help
|
202
208
|
|
203
209
|
```
|
data/lib/dployr/cli.rb
CHANGED
@@ -19,6 +19,7 @@ opt_parser = OptionParser.new do |opt|
|
|
19
19
|
opt.separator " provision instance provisioning"
|
20
20
|
opt.separator " config generate configuration in YAML from Dployrfile"
|
21
21
|
opt.separator " execute run custom stages"
|
22
|
+
opt.separator " ssh ssh into machine"
|
22
23
|
opt.separator " init create a sample Dployrfile"
|
23
24
|
opt.separator ""
|
24
25
|
opt.separator " Options"
|
@@ -58,35 +59,31 @@ end
|
|
58
59
|
|
59
60
|
opt_parser.parse!
|
60
61
|
|
61
|
-
dployr = Dployr::Init.new @attributes
|
62
|
-
dployr.load_config options[:file]
|
63
|
-
config = dployr.config.get_region(options[:name], options[:provider], options[:region])
|
64
|
-
|
65
62
|
case command
|
66
63
|
when "start"
|
67
|
-
Dployr::Commands::Start.new
|
64
|
+
Dployr::Commands::Start.new options
|
68
65
|
when "halt"
|
69
|
-
Dployr::Commands::
|
66
|
+
Dployr::Commands::StopDestroy.new options, "halt"
|
70
67
|
when "destroy"
|
71
|
-
Dployr::Commands::
|
68
|
+
Dployr::Commands::StopDestroy.new options, "destroy"
|
72
69
|
when "status"
|
73
70
|
puts "Command currently not available"
|
74
71
|
when "provision"
|
75
|
-
Dployr::Commands::
|
72
|
+
Dployr::Commands::ProvisionTest.new options, "provision"
|
76
73
|
when "test"
|
77
|
-
Dployr::Commands::
|
74
|
+
Dployr::Commands::ProvisionTest.new options, "test"
|
78
75
|
when "deploy"
|
79
|
-
Dployr::Commands::Start.new
|
80
|
-
Dployr::Commands::
|
81
|
-
Dployr::Commands::
|
76
|
+
Dployr::Commands::Start.new options
|
77
|
+
Dployr::Commands::ProvisionTest.new options, "provision"
|
78
|
+
Dployr::Commands::ProvisionTest.new options, "test"
|
82
79
|
when "execute"
|
83
|
-
Dployr::Commands::Execute.new
|
80
|
+
Dployr::Commands::Execute.new options, ARGV[1..-1]
|
81
|
+
when "ssh"
|
82
|
+
Dployr::Commands::Ssh.new options
|
84
83
|
when "config"
|
85
|
-
Dployr::Commands::Config.new
|
84
|
+
Dployr::Commands::Config.new options
|
86
85
|
when "init"
|
87
86
|
Dployr::Config::Create.write_file
|
88
|
-
when '-h', '--help', 'help'
|
89
|
-
# help already showed by option
|
90
87
|
else
|
91
88
|
puts opt_parser
|
92
89
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'dployr/init'
|
3
|
+
require 'dployr/commands/utils'
|
4
|
+
|
5
|
+
module Dployr
|
6
|
+
module Commands
|
7
|
+
class Base
|
8
|
+
|
9
|
+
include Dployr::Commands::Utils
|
10
|
+
|
11
|
+
attr_reader :options, :name, :log, :attrs, :dployr
|
12
|
+
|
13
|
+
def initialize(options)
|
14
|
+
@options = options
|
15
|
+
@name = options[:name]
|
16
|
+
@log = Logger.new STDOUT
|
17
|
+
@attrs = parse_attributes @options[:attributes]
|
18
|
+
end
|
19
|
+
|
20
|
+
def create
|
21
|
+
begin
|
22
|
+
@dployr = Dployr::Init.new @attrs
|
23
|
+
@dployr.load_config @options[:file]
|
24
|
+
rescue Exception => e
|
25
|
+
raise "Cannot load the config: #{e}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def create_compute_client
|
30
|
+
begin
|
31
|
+
Dployr::Compute.const_get(@provider.to_sym).new @regions
|
32
|
+
rescue Exception => e
|
33
|
+
raise "Provider '#{@provider}' is not supported: #{e}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def get_region_config(options)
|
38
|
+
@dployr.config.get_region options[:name], options[:provider], options[:region]
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -1,24 +1,41 @@
|
|
1
|
-
require '
|
2
|
-
require 'dployr/utils'
|
3
|
-
require 'dployr/compute/aws'
|
4
|
-
require 'colorize'
|
1
|
+
require 'dployr/commands/base'
|
5
2
|
|
6
3
|
module Dployr
|
7
4
|
module Commands
|
8
|
-
class Config
|
5
|
+
class Config < Base
|
9
6
|
|
10
|
-
|
11
|
-
|
12
|
-
def initialize(config, options)
|
7
|
+
def initialize(options)
|
8
|
+
super options
|
13
9
|
begin
|
14
|
-
|
15
|
-
|
10
|
+
create
|
11
|
+
render_file
|
16
12
|
rescue Exception => e
|
17
13
|
@log.error e
|
18
|
-
|
14
|
+
exit 1
|
19
15
|
end
|
20
16
|
end
|
21
|
-
|
17
|
+
|
18
|
+
def render_file
|
19
|
+
raise "Dployrfile was not found" if @dployr.file_path.nil?
|
20
|
+
raise "Configuration is missing" unless @dployr.config.exists?
|
21
|
+
begin
|
22
|
+
if @name and options[:provider] and options[:region]
|
23
|
+
config = get_region_config options
|
24
|
+
elsif @name
|
25
|
+
config = @dployr.config.get_config @name, @attrs
|
26
|
+
else
|
27
|
+
config = @dployr.config.get_config_all @attrs
|
28
|
+
end
|
29
|
+
unless config.nil?
|
30
|
+
puts config.to_yaml
|
31
|
+
else
|
32
|
+
@log.info "Missing configuration data"
|
33
|
+
end
|
34
|
+
rescue Exception => e
|
35
|
+
raise "Cannot generate the config: #{e}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
22
39
|
end
|
23
40
|
end
|
24
41
|
end
|
@@ -1,40 +1,37 @@
|
|
1
|
-
require '
|
2
|
-
require 'dployr/utils'
|
1
|
+
require 'dployr/commands/base'
|
3
2
|
require 'dployr/compute/aws'
|
4
|
-
require 'colorize'
|
5
3
|
|
6
4
|
module Dployr
|
7
5
|
module Commands
|
8
|
-
class Execute
|
6
|
+
class Execute < Base
|
9
7
|
|
10
|
-
|
11
|
-
|
12
|
-
def initialize(config, options, stages)
|
8
|
+
def initialize(options, stages)
|
9
|
+
super options
|
13
10
|
begin
|
14
|
-
|
11
|
+
create
|
12
|
+
config = get_region_config options
|
13
|
+
|
15
14
|
@name = config[:attributes]["name"]
|
16
15
|
@provider = options[:provider].upcase
|
17
16
|
@region = options[:region]
|
18
|
-
@attributes = config[:attributes]
|
19
17
|
|
20
18
|
puts "Connecting to #{@provider}...".yellow
|
21
|
-
@client = Dployr::Compute.const_get(@provider.to_sym).new
|
22
|
-
|
19
|
+
@client = Dployr::Compute.const_get(@provider.to_sym).new @region
|
20
|
+
|
23
21
|
puts "Looking for #{@name} in #{@region}...".yellow
|
24
|
-
@ip = @client.get_ip
|
22
|
+
@ip = @client.get_ip @name
|
25
23
|
if @ip
|
26
24
|
puts "#{@name} found with IP #{@ip}".yellow
|
27
25
|
else
|
28
26
|
raise "#{@name} not found"
|
29
27
|
end
|
30
|
-
|
28
|
+
|
31
29
|
stages.each do |stage|
|
32
30
|
Dployr::Scripts::Hook.new @ip, config, stage
|
33
31
|
end
|
34
|
-
|
35
32
|
rescue Exception => e
|
36
|
-
|
37
|
-
|
33
|
+
self.log.error e
|
34
|
+
exit 1
|
38
35
|
end
|
39
36
|
end
|
40
37
|
|
@@ -1,44 +1,40 @@
|
|
1
|
-
require '
|
2
|
-
require 'dployr/utils'
|
1
|
+
require 'dployr/commands/base'
|
3
2
|
require 'dployr/compute/aws'
|
4
|
-
require 'colorize'
|
5
3
|
|
6
4
|
module Dployr
|
7
5
|
module Commands
|
8
|
-
class
|
6
|
+
class ProvisionTest < Base
|
9
7
|
|
10
|
-
|
11
|
-
|
12
|
-
def initialize(config, options, action)
|
8
|
+
def initialize(options, action)
|
9
|
+
super options
|
13
10
|
begin
|
14
|
-
|
11
|
+
create
|
12
|
+
config = get_region_config options
|
13
|
+
|
15
14
|
@name = config[:attributes]["name"]
|
16
15
|
@provider = options[:provider].upcase
|
17
16
|
@region = options[:region]
|
18
|
-
@attributes = config[:attributes]
|
19
|
-
@action = action
|
20
17
|
|
21
18
|
puts "Connecting to #{@provider}...".yellow
|
22
|
-
@client = Dployr::Compute.const_get(@provider.to_sym).new
|
23
|
-
|
19
|
+
@client = Dployr::Compute.const_get(@provider.to_sym).new @region
|
20
|
+
|
24
21
|
puts "Looking for #{@name} in #{@region}...".yellow
|
25
|
-
@ip = @client.get_ip
|
22
|
+
@ip = @client.get_ip @name
|
26
23
|
if @ip
|
27
24
|
puts "#{@name} found with IP #{@ip}".yellow
|
28
25
|
else
|
29
26
|
raise "#{@name} not found"
|
30
27
|
end
|
31
|
-
|
32
|
-
Dployr::Scripts::Default_Hooks.new @ip, config,
|
33
|
-
|
28
|
+
|
29
|
+
Dployr::Scripts::Default_Hooks.new @ip, config, action, self
|
34
30
|
rescue Exception => e
|
35
31
|
@log.error e
|
36
|
-
|
32
|
+
exit 1
|
37
33
|
end
|
38
34
|
end
|
39
35
|
|
40
36
|
def action
|
41
|
-
|
37
|
+
@ip
|
42
38
|
end
|
43
39
|
end
|
44
40
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'dployr/commands/base'
|
3
|
+
require 'dployr/compute/aws'
|
4
|
+
|
5
|
+
module Dployr
|
6
|
+
module Commands
|
7
|
+
class Ssh < Base
|
8
|
+
|
9
|
+
def initialize(options)
|
10
|
+
super options
|
11
|
+
begin
|
12
|
+
create
|
13
|
+
config = get_region_config options
|
14
|
+
|
15
|
+
@name = config[:attributes]["name"]
|
16
|
+
@provider = options[:provider].upcase
|
17
|
+
@region = options[:region]
|
18
|
+
|
19
|
+
puts "Connecting to #{@provider}...".yellow
|
20
|
+
@client = Dployr::Compute.const_get(@provider.to_sym).new @region
|
21
|
+
|
22
|
+
puts "Looking for #{@name} in #{@region}...".yellow
|
23
|
+
@ip = @client.get_ip @name
|
24
|
+
if @ip
|
25
|
+
puts "#{@name} found with IP #{@ip}".yellow
|
26
|
+
else
|
27
|
+
raise "#{@name} not found"
|
28
|
+
end
|
29
|
+
|
30
|
+
Dployr::Scripts::Ssh.new @ip, config
|
31
|
+
|
32
|
+
rescue Exception => e
|
33
|
+
self.log.error e
|
34
|
+
exit 1
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -1,46 +1,45 @@
|
|
1
|
-
require '
|
2
|
-
require 'dployr/utils'
|
1
|
+
require 'dployr/commands/base'
|
3
2
|
require 'dployr/compute/aws'
|
4
|
-
require '
|
3
|
+
require 'dployr/compute/gce'
|
5
4
|
|
6
5
|
module Dployr
|
7
6
|
module Commands
|
8
|
-
class Start
|
7
|
+
class Start < Base
|
9
8
|
|
10
|
-
|
11
|
-
|
12
|
-
def initialize(config, options)
|
9
|
+
def initialize(options)
|
10
|
+
super options
|
13
11
|
begin
|
14
|
-
|
12
|
+
create
|
13
|
+
config = get_region_config options
|
14
|
+
|
15
15
|
@name = config[:attributes]["name"]
|
16
16
|
@provider = options[:provider].upcase
|
17
17
|
@region = options[:region]
|
18
18
|
@attributes = config[:attributes]
|
19
19
|
|
20
20
|
puts "Connecting to #{@provider}...".yellow
|
21
|
-
@client = Dployr::Compute.const_get(@provider.to_sym).new
|
22
|
-
|
21
|
+
@client = Dployr::Compute.const_get(@provider.to_sym).new @region
|
22
|
+
|
23
23
|
puts "Looking for #{@name} in #{@region}...".yellow
|
24
|
-
@ip = @client.get_ip
|
25
|
-
|
24
|
+
@ip = @client.get_ip @name
|
25
|
+
|
26
26
|
Dployr::Scripts::Default_Hooks.new @ip, config, "start", self
|
27
|
-
|
28
27
|
rescue Exception => e
|
29
28
|
@log.error e
|
30
|
-
|
29
|
+
exit 1
|
31
30
|
end
|
32
31
|
end
|
33
|
-
|
32
|
+
|
34
33
|
def action
|
35
34
|
if @ip
|
36
35
|
puts "#{@name} found with IP #{@ip}".yellow
|
37
36
|
else
|
38
|
-
@ip = @client.start
|
37
|
+
@ip = @client.start @attributes, @region
|
39
38
|
puts "Startded instance for #{@name} in #{@region} with IP #{@ip} succesfully".yellow
|
40
39
|
end
|
41
|
-
|
40
|
+
@ip
|
42
41
|
end
|
43
|
-
|
42
|
+
|
44
43
|
end
|
45
44
|
end
|
46
45
|
end
|
@@ -1,49 +1,47 @@
|
|
1
|
-
require '
|
2
|
-
require 'dployr/utils'
|
1
|
+
require 'dployr/commands/base'
|
3
2
|
require 'dployr/compute/aws'
|
4
|
-
require 'colorize'
|
5
3
|
|
6
4
|
module Dployr
|
7
5
|
module Commands
|
8
|
-
class
|
6
|
+
class StopDestroy < Base
|
9
7
|
|
10
|
-
|
11
|
-
|
12
|
-
def initialize(config, options, action)
|
8
|
+
def initialize(options, action)
|
9
|
+
super options
|
13
10
|
begin
|
14
|
-
|
11
|
+
create
|
12
|
+
config = get_region_config options
|
13
|
+
|
15
14
|
@name = config[:attributes]["name"]
|
16
15
|
@provider = options[:provider].upcase
|
17
16
|
@region = options[:region]
|
18
17
|
@attributes = config[:attributes]
|
19
18
|
@action = action
|
20
|
-
|
19
|
+
|
21
20
|
puts "Connecting to #{@provider}...".yellow
|
22
|
-
@client = Dployr::Compute.const_get(@provider.to_sym).new
|
23
|
-
|
21
|
+
@client = Dployr::Compute.const_get(@provider.to_sym).new @region
|
22
|
+
|
24
23
|
puts "Looking for #{@name} in #{@region}...".yellow
|
25
|
-
@ip = @client.get_ip
|
24
|
+
@ip = @client.get_ip @name
|
26
25
|
if @ip
|
27
26
|
puts "#{@name} found with IP #{@ip}".yellow
|
28
27
|
else
|
29
28
|
puts "#{@name} not found".yellow
|
30
29
|
end
|
31
|
-
|
32
|
-
Dployr::Scripts::Default_Hooks.new @ip, config,
|
33
|
-
|
30
|
+
|
31
|
+
Dployr::Scripts::Default_Hooks.new @ip, config, action, self
|
34
32
|
rescue Exception => e
|
35
33
|
@log.error e
|
36
|
-
|
34
|
+
exit 1
|
37
35
|
end
|
38
36
|
end
|
39
|
-
|
40
|
-
def action
|
37
|
+
|
38
|
+
def action
|
41
39
|
puts "#{@action.capitalize}ing #{@name} in #{@region}...".yellow
|
42
|
-
@client.send(@action.to_sym, @name)
|
40
|
+
@client.send(@action.to_sym, @name)
|
43
41
|
puts "#{@name} #{@action}ed sucesfully".yellow
|
44
|
-
|
42
|
+
@ip
|
45
43
|
end
|
46
|
-
|
44
|
+
|
47
45
|
end
|
48
46
|
end
|
49
47
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Dployr
|
2
|
+
module Commands
|
3
|
+
module Utils
|
4
|
+
|
5
|
+
module_function
|
6
|
+
|
7
|
+
def parse_matrix(str)
|
8
|
+
hash = {}
|
9
|
+
str.split(';').each do |val|
|
10
|
+
val = val.split '='
|
11
|
+
hash[val.first.strip] = val.last.strip
|
12
|
+
end if str.is_a? String
|
13
|
+
hash
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse_flags(str)
|
17
|
+
hash = {}
|
18
|
+
str.gsub(/\s+/, ' ').strip.split(' ').each_slice(2) do |val|
|
19
|
+
key = val.first
|
20
|
+
if val.first.is_a? String
|
21
|
+
key = key.gsub(/^\-+/, '').strip
|
22
|
+
hash[key] = (val.last or '').strip
|
23
|
+
end
|
24
|
+
end if str.is_a? String
|
25
|
+
hash
|
26
|
+
end
|
27
|
+
|
28
|
+
def parse_attributes(attributes)
|
29
|
+
if attributes.is_a? String
|
30
|
+
if attributes[0] == '-'
|
31
|
+
parse_flags attributes
|
32
|
+
else
|
33
|
+
parse_matrix attributes
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|