cloudshaper 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.travis.yml +3 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +27 -0
- data/LICENSE +23 -0
- data/README.md +162 -0
- data/Rakefile +9 -0
- data/TODO.md +7 -0
- data/examples/secretconfig/README.md +3 -0
- data/examples/secretconfig/atlas.json +5 -0
- data/examples/secretconfig/aws.json +6 -0
- data/examples/secretconfig/cloudflare.json +6 -0
- data/examples/secretconfig/cloudstack.json +7 -0
- data/examples/secretconfig/digitalocean.json +5 -0
- data/examples/secretconfig/dme.json +6 -0
- data/examples/secretconfig/heroku.json +6 -0
- data/examples/secretconfig/mailgun.json +5 -0
- data/examples/simple_app.rb +61 -0
- data/lib/tasks/tasks.rb +12 -0
- data/lib/tasks/terraform.rake +127 -0
- data/lib/terraform_dsl.rb +6 -0
- data/lib/terraform_dsl/aws/remote_s3.rb +20 -0
- data/lib/terraform_dsl/aws/tagging.rb +23 -0
- data/lib/terraform_dsl/command.rb +58 -0
- data/lib/terraform_dsl/module.rb +38 -0
- data/lib/terraform_dsl/output.rb +6 -0
- data/lib/terraform_dsl/provider.rb +13 -0
- data/lib/terraform_dsl/remote.rb +31 -0
- data/lib/terraform_dsl/resource.rb +44 -0
- data/lib/terraform_dsl/secrets.rb +29 -0
- data/lib/terraform_dsl/stack.rb +79 -0
- data/lib/terraform_dsl/stack_element.rb +62 -0
- data/lib/terraform_dsl/stack_module.rb +121 -0
- data/lib/terraform_dsl/stack_modules.rb +24 -0
- data/lib/terraform_dsl/stacks.rb +64 -0
- data/lib/terraform_dsl/variable.rb +6 -0
- data/lib/terraform_dsl/version.rb +3 -0
- data/terraform_dsl.gemspec +25 -0
- data/test/stack_module_test.rb +152 -0
- data/test/stack_test.rb +5 -0
- data/test/test_helper.rb +4 -0
- metadata +131 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 720390f8ef2c81bf799239154777e8b0177a7d4b
|
4
|
+
data.tar.gz: 952a846501c2d3d5e32b34800cd1d70fd9b7e81d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 610e4fc75444a2e2721aaccf6a0cfc76e8c56a979848a65d500f0d87934661d91f81cc06e2d91744450d742a357a4067d1f2352eb4efae53e8e462d40d76e5a1
|
7
|
+
data.tar.gz: 8def0b2aef1901b0e1b84fba5c3d061aa9bdab0062dca156200efe18f402a2d4bfe3f3c448e8a8b297b44bffaff74170874dfc78f42ebedf1855e148bc8c2608
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
cloudshaper (0.0.4)
|
5
|
+
rake (~> 10.4)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
docile (1.1.5)
|
11
|
+
json (1.8.2)
|
12
|
+
minitest (5.6.1)
|
13
|
+
rake (10.4.2)
|
14
|
+
simplecov (0.10.0)
|
15
|
+
docile (~> 1.1.0)
|
16
|
+
json (~> 1.8)
|
17
|
+
simplecov-html (~> 0.10.0)
|
18
|
+
simplecov-html (0.10.0)
|
19
|
+
|
20
|
+
PLATFORMS
|
21
|
+
ruby
|
22
|
+
|
23
|
+
DEPENDENCIES
|
24
|
+
bundler
|
25
|
+
cloudshaper!
|
26
|
+
minitest (~> 5.6)
|
27
|
+
simplecov (= 0.10.0)
|
data/LICENSE
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
The MIT License (MIT)
|
4
|
+
|
5
|
+
Copyright (c) [year] [fullname]
|
6
|
+
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
9
|
+
in the Software without restriction, including without limitation the rights
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
12
|
+
furnished to do so, subject to the following conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
15
|
+
copies or substantial portions of the Software.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
23
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
[![Build Status](https://travis-ci.org/dalehamel/terraform_dsl.svg)](https://travis-ci.org/dalehamel/terraform_dsl)
|
2
|
+
|
3
|
+
# Terraform DSL
|
4
|
+
|
5
|
+
This is a simple DSL for wrapping hashicorp's [terraform configuration](https://terraform.io/docs/configuration/index.html).
|
6
|
+
|
7
|
+
Terraform is an infrastructure-as-code tool for managing infrastructure by defining stack element in a DSL.
|
8
|
+
|
9
|
+
This ruby DSL It supports almost identical syntax, and generates JSON that the terraform go application will understand.
|
10
|
+
|
11
|
+
## Differences from terraform's HCL configuration
|
12
|
+
|
13
|
+
There are a few ways to access stack element variables and attributes for different purposes:
|
14
|
+
|
15
|
+
* get(:var) - This gets the actual value of a variable at generation time. This is needed when passing variables to submodules
|
16
|
+
* var(:var) - This references the terraform interpolation syntax of a variable.
|
17
|
+
* value\_of(:var) - This references the value of a variable for a stack element by type and name using interpolation syntax.
|
18
|
+
* id\_of(:var) - This references the id of a stack element by type and name using interpolation syntax.
|
19
|
+
|
20
|
+
Since 'module' is a keyword in ruby, and even though it is technically OK to use it as a function name, we use 'submodule' instead of module, to make editors happy and avoid using the keyword.
|
21
|
+
* submodules are terraform modules under the hood - you can use normal terraform module syntax if you wish
|
22
|
+
* If you reference your source as 'module\_MODULENAME', you can reference another ruby module!
|
23
|
+
* This module will be resolved at runtime, not compile time
|
24
|
+
* Modules nested most deeply are resolved first. The root module is resolved last.
|
25
|
+
|
26
|
+
We allow variables to have array values. This is very useful as a flow-control technique to DRY up modules, and leverage the full power of the ruby DSL.
|
27
|
+
However, since terraform does not allow variables to have array values, we automatically flatten them to a comma-separated string. This variable may or may not be used by terraform, if you reference it explicity using one of the interpolated accessors mentioned above. If it is not used, it doesn't matter and will simply be ignored.
|
28
|
+
|
29
|
+
Keep in mind that when specifying a hash as a value within the DSL, you must use brackets around the braces ( call as ({key: value}) instead of {key: value}, this is an unfortunate but necessary syntax).
|
30
|
+
|
31
|
+
# Usage
|
32
|
+
|
33
|
+
To use this gem, you will need to:
|
34
|
+
|
35
|
+
* define some stack modules
|
36
|
+
* configure secrets for your app
|
37
|
+
* define those stacks using your modules in yaml
|
38
|
+
* create a simple rake file
|
39
|
+
|
40
|
+
Once you've done this, you can run various rake tasks to manage your stacks.
|
41
|
+
|
42
|
+
If you get lost, take a look at the [sample app](https://github.com/dalehamel/terraform_dsl_sample)
|
43
|
+
|
44
|
+
## Stack modules
|
45
|
+
|
46
|
+
In terraform, everything is defined in terms of 'modules'. Even if you don't use the 'module' (submodule in our case) keyword, you're implicitly working within the 'root' module.
|
47
|
+
|
48
|
+
Create a stack module, like one of our [examples](examples), such as our [simple app](examples/simple_app.rb)
|
49
|
+
|
50
|
+
Generally, you need to do:
|
51
|
+
|
52
|
+
```
|
53
|
+
require 'terraform_dsl'
|
54
|
+
```
|
55
|
+
|
56
|
+
And then subclass Terraform::StackModule
|
57
|
+
|
58
|
+
```
|
59
|
+
class MyAwesomeStackModule < Terraform::StackModule
|
60
|
+
```
|
61
|
+
|
62
|
+
Within that class, define resources using a similar syntax to [terraform's configuration](https://terraform.io/docs/configuration/index.html).
|
63
|
+
|
64
|
+
## Submodules
|
65
|
+
|
66
|
+
You may even reference other stack modules that you've defined! Just call the module by using "module\_MODULENAME" as the 'source' value.
|
67
|
+
|
68
|
+
Using submodules are a fantastic way to DRY up your infrastructure! Since they are just terraform modules under the hood, you can specify variables as input (to configure the module) and specify outputs of the module, so that other modules may interpolate them for their own resources.
|
69
|
+
|
70
|
+
## Configuration
|
71
|
+
|
72
|
+
### Secrets
|
73
|
+
|
74
|
+
Create a file at config/secrets.json that contains secrets needed for you providers.
|
75
|
+
|
76
|
+
Specify the secrets as a JSON hash like so:
|
77
|
+
|
78
|
+
```
|
79
|
+
{
|
80
|
+
"aws": {
|
81
|
+
"AWS_ACCESS_KEY_ID": "ACCESS_KEY",
|
82
|
+
"AWS_SECRET_ACCESS_KEY": "SECRET_KEY"
|
83
|
+
}
|
84
|
+
}
|
85
|
+
```
|
86
|
+
|
87
|
+
For other providers, see [example configs](examples/secretconfig)
|
88
|
+
|
89
|
+
**Note** do not commit plaintext secrets.json to your repository. We recommend you use [ejson](https://github.com/Shopify/ejson) to store your secrets, and [capistrano-ejson](https://github.com/Shopify/capistrano-ejson) to decrypt them in production.
|
90
|
+
**Note** Secrets are never written to module files, as a safeguared to prevent them from accidentally being committed. Instead, they are passed as environment vairables.
|
91
|
+
|
92
|
+
### YAML
|
93
|
+
|
94
|
+
After setting up your rakefile, as below, just run:
|
95
|
+
|
96
|
+
```
|
97
|
+
bundle exec rake terraform:init
|
98
|
+
```
|
99
|
+
|
100
|
+
This will set up your stacks.yml with an initial config template, which you can customize for your chosen stack module
|
101
|
+
|
102
|
+
```
|
103
|
+
common:
|
104
|
+
remote:
|
105
|
+
s3:
|
106
|
+
bucket: quartermaster-terraform
|
107
|
+
region: us-east-1
|
108
|
+
stacks:
|
109
|
+
- name: teststack
|
110
|
+
uuid: 1433296428_0safh8-Y # must be unique
|
111
|
+
description: just a test stack
|
112
|
+
root: simpleapp
|
113
|
+
variables:
|
114
|
+
flavor: t1.micro
|
115
|
+
key: SOMESSHKEY
|
116
|
+
```
|
117
|
+
|
118
|
+
You may also specify a 'common' block, that will be merged into all stacks.
|
119
|
+
|
120
|
+
Terraform stacks need somewhere to store their state. By default, this will be the local filesystem.
|
121
|
+
|
122
|
+
It's highly recommended that you use a [remote backend](https://www.terraform.io/docs/commands/remote-config.html) instead, so that you can share your stacks.
|
123
|
+
|
124
|
+
### Tasks
|
125
|
+
|
126
|
+
Create a rake file that loads your modules, and calls Terraform::Tasks.loadall.
|
127
|
+
|
128
|
+
```
|
129
|
+
## Loads terraform tasks and modules
|
130
|
+
require 'terraform_dsl'
|
131
|
+
Terraform::Tasks.loadall
|
132
|
+
|
133
|
+
```
|
134
|
+
|
135
|
+
This will add some terraform tasks for managing your terraform stacks:
|
136
|
+
|
137
|
+
```
|
138
|
+
rake terraform:apply[name] # Apply pending changes for a stack
|
139
|
+
rake terraform:apply_all # Apply all pending stack changes
|
140
|
+
rake terraform:destroy[name] # Destroy a stack
|
141
|
+
rake terraform:get[name] # Fetch modules for a stack
|
142
|
+
rake terraform:get_all # Fetch modules for all stacks
|
143
|
+
rake terraform:init # Initialize stacks.yml if it does not exist
|
144
|
+
rake terraform:list # List all available stacks
|
145
|
+
rake terraform:load # Loads available stack modules
|
146
|
+
rake terraform:plan[name] # Show pending changes for a stack
|
147
|
+
rake terraform:pull[name] # Pulls stack state from remote location
|
148
|
+
rake terraform:pull_all # Pulls stack states from remote location
|
149
|
+
rake terraform:push[name] # Push stack state to remote location
|
150
|
+
rake terraform:push_all # Push stack states to remote location
|
151
|
+
rake terraform:remote_config[name] # Sets up remote config for a stack
|
152
|
+
rake terraform:remote_config_all # Sets up remote config for all stacks that support it
|
153
|
+
rake terraform:show[name] # Show details about a stack by name
|
154
|
+
rake terraform:show_all # Show all pending stack changes
|
155
|
+
rake terraform:uuid # Generate a UUID for a stack, so stacks do not clobber each other
|
156
|
+
```
|
157
|
+
|
158
|
+
# Credits
|
159
|
+
|
160
|
+
Inspired by [terraframe](https://github.com/eropple/terraframe), a very similar but less generic and complete terraform DSL.
|
161
|
+
|
162
|
+
[license](LICENSE)
|
data/Rakefile
ADDED
data/TODO.md
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
* Allow variables to include arrays natively
|
2
|
+
* Detect arrays, and flatten into strings (terraform doesn't support arrays for variable values :sad\_panda:)
|
3
|
+
+ Improve test coverage
|
4
|
+
+ Come up with a way to lock S3 states
|
5
|
+
+ Improve documentation
|
6
|
+
+ Strategy to ship modules around
|
7
|
+
+ Gems?
|
@@ -0,0 +1,61 @@
|
|
1
|
+
class SimpleApp < StackTemplate
|
2
|
+
variable(:flavor) { default 'm1.small' }
|
3
|
+
variable(:ami) { default 'ami-50948838' }
|
4
|
+
variable(:region) { default 'us-east-1' }
|
5
|
+
variable(:availability_zone) { default 'us-east-1b' }
|
6
|
+
variable(:cidr) { default '10.1.2.0/24' }
|
7
|
+
variable(:name) { default 'default' }
|
8
|
+
variable(:key) {}
|
9
|
+
|
10
|
+
provider 'aws' do
|
11
|
+
region var(:region)
|
12
|
+
end
|
13
|
+
|
14
|
+
resource 'aws_security_group', :basic_web do
|
15
|
+
name 'simple_app_web'
|
16
|
+
description 'Basic security group with port 22, 80, 443 open to the world.'
|
17
|
+
|
18
|
+
ingress do
|
19
|
+
from_port 22
|
20
|
+
to_port 22
|
21
|
+
protocol 'tcp'
|
22
|
+
cidr_blocks ['0.0.0.0/0']
|
23
|
+
end
|
24
|
+
|
25
|
+
ingress do
|
26
|
+
from_port 80
|
27
|
+
to_port 80
|
28
|
+
protocol 'tcp'
|
29
|
+
cidr_blocks ['0.0.0.0/0']
|
30
|
+
end
|
31
|
+
|
32
|
+
ingress do
|
33
|
+
from_port 443
|
34
|
+
to_port 443
|
35
|
+
protocol 'tcp'
|
36
|
+
cidr_blocks ['0.0.0.0/0']
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
resource 'aws_vpc', :simple_vpc do
|
41
|
+
cidr_block var(:cidr)
|
42
|
+
tags ({
|
43
|
+
'Name' => var(:name)
|
44
|
+
})
|
45
|
+
end
|
46
|
+
|
47
|
+
resource 'aws_subnet', :simple_subnet do
|
48
|
+
vpc_id id_of('aws_vpc', :simple_vpc)
|
49
|
+
cidr_block var(:cidr)
|
50
|
+
end
|
51
|
+
|
52
|
+
resource 'aws_instance', :simple_app do
|
53
|
+
security_groups [value_of('aws_security_group', :basic_web, :name)]
|
54
|
+
instance_type var(:flavor)
|
55
|
+
ami var(:ami)
|
56
|
+
key_name var(:key)
|
57
|
+
tags ({
|
58
|
+
'Name' => "#{var(:name)}.ec2.shopify.com"
|
59
|
+
})
|
60
|
+
end
|
61
|
+
end
|
data/lib/tasks/tasks.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'rake'
|
2
|
+
|
3
|
+
module Terraform
|
4
|
+
# Loads all rake tasks when terraform_dsl is included by a rake script
|
5
|
+
class Tasks
|
6
|
+
def self.loadall
|
7
|
+
Dir.glob("#{File.join(File.dirname(__dir__), 'tasks')}/*.rake").each { |r| load r }
|
8
|
+
template_path = ENV['TERRAFORM_TEMPLATE_PATH'] || 'templates'
|
9
|
+
Dir.glob("#{File.join(Dir.pwd, template_path)}/*.rb").each { |t| require_relative t }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'terraform_dsl'
|
2
|
+
|
3
|
+
namespace 'terraform' do
|
4
|
+
desc 'Loads available stack modules'
|
5
|
+
task :load do
|
6
|
+
Terraform::Stacks.load
|
7
|
+
end
|
8
|
+
|
9
|
+
desc 'Initialize stacks.yml if it does not exist'
|
10
|
+
task :init do
|
11
|
+
Terraform::Stacks.init
|
12
|
+
end
|
13
|
+
|
14
|
+
desc 'Fetch modules for a stack'
|
15
|
+
task :get, [:name] => :load do |_t, args|
|
16
|
+
stack = Terraform::Stacks.stacks[args[:name]]
|
17
|
+
stack.get
|
18
|
+
end
|
19
|
+
|
20
|
+
desc 'Fetch modules for all stacks'
|
21
|
+
task get_all: :load do
|
22
|
+
Terraform::Stacks.stacks.each do |_name, stack|
|
23
|
+
stack.get
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
desc 'List all available stacks'
|
28
|
+
task list: :load do
|
29
|
+
Terraform::Stacks.stacks.each do |name, _stack|
|
30
|
+
puts name
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
desc 'Show details about a stack by name'
|
35
|
+
task :show, [:name] => :load do |_t, args|
|
36
|
+
fail 'Specify a name' unless args[:name]
|
37
|
+
stack = Terraform::Stacks.stacks[args[:name]]
|
38
|
+
puts stack
|
39
|
+
stack.plan
|
40
|
+
end
|
41
|
+
|
42
|
+
desc 'Show all pending stack changes'
|
43
|
+
task show_all: [:load, :get_all] do
|
44
|
+
Terraform::Stacks.stacks.each do |_name, stack|
|
45
|
+
puts stack
|
46
|
+
stack.plan
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
desc 'Show pending changes for a stack'
|
51
|
+
task :plan, [:name] => :load do |_t, args|
|
52
|
+
fail 'Specify a name' unless args[:name]
|
53
|
+
stack = Terraform::Stacks.stacks[args[:name]]
|
54
|
+
stack.plan
|
55
|
+
end
|
56
|
+
|
57
|
+
desc 'Apply pending changes for a stack'
|
58
|
+
task :apply, [:name] => :load do |_t, args|
|
59
|
+
fail 'Specify a name' unless args[:name]
|
60
|
+
stack = Terraform::Stacks.stacks[args[:name]]
|
61
|
+
stack.apply
|
62
|
+
end
|
63
|
+
|
64
|
+
desc 'Apply all pending stack changes'
|
65
|
+
task apply_all: :load do
|
66
|
+
Terraform::Stacks.stacks.each do |_name, stack|
|
67
|
+
puts stack
|
68
|
+
stack.apply
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
desc 'Destroy a stack'
|
73
|
+
task :destroy, [:name] => :load do |_t, args|
|
74
|
+
fail 'Specify a name' unless args[:name]
|
75
|
+
stack = Terraform::Stacks.stacks[args[:name]]
|
76
|
+
stack.destroy
|
77
|
+
end
|
78
|
+
|
79
|
+
desc 'Push stack state to remote location'
|
80
|
+
task :push, [:name] => [:load, :remote_config] do |_t, args|
|
81
|
+
stack = Terraform::Stacks.stacks[args[:name]]
|
82
|
+
stack.push
|
83
|
+
end
|
84
|
+
|
85
|
+
desc 'Push stack states to remote location'
|
86
|
+
task push_all: [:load, :remote_config_all] do
|
87
|
+
Terraform::Stacks.stacks.each do |_name, stack|
|
88
|
+
puts stack
|
89
|
+
stack.push
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
desc 'Pulls stack state from remote location'
|
94
|
+
task :pull, [:name] => [:load, :remote_config] do |_t, args|
|
95
|
+
stack = Terraform::Stacks.stacks[args[:name]]
|
96
|
+
stack.pull
|
97
|
+
end
|
98
|
+
|
99
|
+
desc 'Pulls stack states from remote location'
|
100
|
+
task pull_all: [:load, :remote_config_all] do
|
101
|
+
Terraform::Stacks.stacks.each do |_name, stack|
|
102
|
+
puts stack
|
103
|
+
stack.pull
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
desc 'Sets up remote config for a stack'
|
108
|
+
task :remote_config, [:name] => [:load] do |_t, args|
|
109
|
+
stack = Terraform::Stacks.stacks[args[:name]]
|
110
|
+
stack.remote_config
|
111
|
+
end
|
112
|
+
|
113
|
+
desc 'Sets up remote config for all stacks that support it'
|
114
|
+
task remote_config_all: :load do
|
115
|
+
Terraform::Stacks.stacks.each do |_name, stack|
|
116
|
+
puts stack
|
117
|
+
stack.remote_config
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
desc 'Generate a UUID for a stack, so stacks do not clobber each other'
|
122
|
+
task :uuid do
|
123
|
+
uuid = Terraform::Stacks.uuid
|
124
|
+
puts "uuid: #{uuid}"
|
125
|
+
puts 'Add this as a field to a new stack to prevent clobbering stacks with the same name'
|
126
|
+
end
|
127
|
+
end
|