vagrant-masonry 0.13.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 +7 -0
- data/.gitignore +3 -0
- data/.yardopts +6 -0
- data/CHANGELOG +190 -0
- data/Gemfile +16 -0
- data/LICENSE +15 -0
- data/README.md +129 -0
- data/docs/GettingStarted.markdown +175 -0
- data/examples/Vagrantfile +1 -0
- data/examples/roles.yaml +29 -0
- data/examples/vms.yaml +12 -0
- data/lib/config_builder.rb +24 -0
- data/lib/config_builder/action/load_extensions.rb +14 -0
- data/lib/config_builder/class_registry.rb +72 -0
- data/lib/config_builder/extension_handler.rb +22 -0
- data/lib/config_builder/filter.rb +6 -0
- data/lib/config_builder/filter/boxes.rb +22 -0
- data/lib/config_builder/filter/roles.rb +149 -0
- data/lib/config_builder/filter_stack.rb +37 -0
- data/lib/config_builder/loader.rb +23 -0
- data/lib/config_builder/loader/yaml.rb +44 -0
- data/lib/config_builder/loader/yaml_erb.rb +24 -0
- data/lib/config_builder/model.rb +67 -0
- data/lib/config_builder/model/base.rb +101 -0
- data/lib/config_builder/model/network/forwarded_port.rb +37 -0
- data/lib/config_builder/model/network/private_network.rb +15 -0
- data/lib/config_builder/model/provider/azure.rb +66 -0
- data/lib/config_builder/model/provider/libvirt.rb +108 -0
- data/lib/config_builder/model/provider/virtualbox.rb +35 -0
- data/lib/config_builder/model/provider/vmware.rb +40 -0
- data/lib/config_builder/model/provider/vmware_fusion.rb +8 -0
- data/lib/config_builder/model/provider/vmware_workstation.rb +8 -0
- data/lib/config_builder/model/provider/vsphere.rb +30 -0
- data/lib/config_builder/model/provisioner/file.rb +24 -0
- data/lib/config_builder/model/provisioner/puppet.rb +37 -0
- data/lib/config_builder/model/provisioner/puppet_server.rb +27 -0
- data/lib/config_builder/model/provisioner/shell.rb +27 -0
- data/lib/config_builder/model/root.rb +69 -0
- data/lib/config_builder/model/ssh.rb +110 -0
- data/lib/config_builder/model/synced_folder.rb +43 -0
- data/lib/config_builder/model/vm.rb +235 -0
- data/lib/config_builder/model/winrm.rb +56 -0
- data/lib/config_builder/model_delegator.rb +30 -0
- data/lib/config_builder/plugin.rb +15 -0
- data/lib/config_builder/runner.rb +33 -0
- data/lib/config_builder/version.rb +3 -0
- data/lib/vagrant-masonry.rb +1 -0
- data/spec/config_builder/filter/boxes_spec.rb +87 -0
- data/spec/config_builder/filter/roles_spec.rb +287 -0
- data/spec/config_builder/loader/yaml_spec.rb +76 -0
- data/spec/config_builder/model/provider/vmware_fusion_spec.rb +29 -0
- data/spec/spec_helper.rb +4 -0
- data/templates/locales/en.yml +11 -0
- data/vagrant-masonry.gemspec +24 -0
- metadata +128 -0
@@ -0,0 +1,56 @@
|
|
1
|
+
# Vagrant WinRM credential model.
|
2
|
+
#
|
3
|
+
# @see http://docs.vagrantup.com/v2/vagrantfile
|
4
|
+
class ConfigBuilder::Model::WinRM < ConfigBuilder::Model::Base
|
5
|
+
# @!attribute [rw] username
|
6
|
+
# @return [String] This sets the username that Vagrant will WinRM as by
|
7
|
+
# default. Providers are free to override this if they detect a more
|
8
|
+
# appropriate user. By default this is "vagrant," since that is what most
|
9
|
+
# public boxes are made as.
|
10
|
+
def_model_attribute :username
|
11
|
+
|
12
|
+
# @!attribute [rw] password
|
13
|
+
# @return [String] This sets a password that Vagrant will use to
|
14
|
+
# authenticate the WinRM user.
|
15
|
+
def_model_attribute :password
|
16
|
+
|
17
|
+
# @!attribute [rw] host
|
18
|
+
# @return [String] The hostname or IP to WinRM into. By default this is
|
19
|
+
# empty, because the provider usually figures this out for you.
|
20
|
+
def_model_attribute :host
|
21
|
+
|
22
|
+
# @!attribute [rw] port
|
23
|
+
# @return [Fixnum] The port to WinRM into. By default this is port 5985.
|
24
|
+
def_model_attribute :port
|
25
|
+
|
26
|
+
# @!attribute [rw] guest_port
|
27
|
+
# @return [Fixnum] The port on the guest that WinRM is running on.
|
28
|
+
# This is used by some providers to detect forwarded ports for WinRM.
|
29
|
+
# For example, if this is set to 5985 (the default), and Vagrant detects
|
30
|
+
# a forwarded port to port 5985 on the guest from port 4567 on the host,
|
31
|
+
# Vagrant will attempt to use port 4567 to talk to the guest if there is
|
32
|
+
# no other option.
|
33
|
+
def_model_attribute :guest_port
|
34
|
+
|
35
|
+
# @!attribute [rw] max_tries
|
36
|
+
# @return [Fixnum] Maximum number of retry attempts. By default this is 20.
|
37
|
+
def_model_attribute :max_tries
|
38
|
+
|
39
|
+
# @!attribute [rw] timeout
|
40
|
+
# @return [Fixnum] The timeout in seconds. By default this is 1800 seconds.
|
41
|
+
def_model_attribute :timeout
|
42
|
+
|
43
|
+
def to_proc
|
44
|
+
Proc.new do |global_config|
|
45
|
+
winrm = global_config.winrm
|
46
|
+
|
47
|
+
with_attr(:username) { |val| winrm.username = val }
|
48
|
+
with_attr(:password) { |val| winrm.password = val }
|
49
|
+
with_attr(:host) { |val| winrm.host = val }
|
50
|
+
with_attr(:guest) { |val| winrm.guest = val }
|
51
|
+
with_attr(:guest_port) { |val| winrm.guest_port = val }
|
52
|
+
with_attr(:max_tries) { |val| winrm.max_tries = val }
|
53
|
+
with_attr(:timeout) { |val| winrm.timeout = val }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module ConfigBuilder
|
2
|
+
module ModelDelegator
|
3
|
+
|
4
|
+
def model_delegators
|
5
|
+
self.class.model_delegators
|
6
|
+
end
|
7
|
+
|
8
|
+
def eval_models(config)
|
9
|
+
model_delegators.each do |model|
|
10
|
+
meth = "eval_#{model}"
|
11
|
+
send(meth, config)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.included(klass)
|
16
|
+
klass.extend ClassMethods
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
def def_model_delegator(identifier)
|
21
|
+
def_model_attribute(identifier)
|
22
|
+
model_delegators << identifier
|
23
|
+
end
|
24
|
+
|
25
|
+
def model_delegators
|
26
|
+
(@models ||= [])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'vagrant'
|
2
|
+
|
3
|
+
require 'config_builder/action/load_extensions'
|
4
|
+
|
5
|
+
module VagrantPlugins
|
6
|
+
module ConfigBuilder
|
7
|
+
class Plugin < Vagrant.plugin('2')
|
8
|
+
name "Generate Vagrant configuration from logic-less data sources"
|
9
|
+
|
10
|
+
action_hook('ConfigBuilder: load extensions', :environment_load) do |hook|
|
11
|
+
hook.prepend(::ConfigBuilder::Action::LoadExtensions)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'config_builder/loader'
|
2
|
+
require 'config_builder/filter_stack'
|
3
|
+
require 'config_builder/model'
|
4
|
+
require 'config_builder/extension_handler'
|
5
|
+
|
6
|
+
module ConfigBuilder
|
7
|
+
class Runner
|
8
|
+
|
9
|
+
def run(identifier, method, value)
|
10
|
+
load_extensions
|
11
|
+
|
12
|
+
data = ConfigBuilder::Loader.generate(identifier, method, value)
|
13
|
+
filtered_data = run_filters(data)
|
14
|
+
model = generate_model(filtered_data)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def load_extensions
|
20
|
+
ext = ConfigBuilder::ExtensionHandler.new
|
21
|
+
ext.load_from_plugins
|
22
|
+
end
|
23
|
+
|
24
|
+
def run_filters(data)
|
25
|
+
stack = ConfigBuilder::FilterStack.new
|
26
|
+
stack.filter(data)
|
27
|
+
end
|
28
|
+
|
29
|
+
def generate_model(filtered_hash)
|
30
|
+
ConfigBuilder::Model.generate(filtered_hash)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'config_builder'
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ConfigBuilder::Filter::Boxes do
|
4
|
+
|
5
|
+
def dup(o)
|
6
|
+
Marshal.load(Marshal.dump(o))
|
7
|
+
end
|
8
|
+
|
9
|
+
let(:boxes) do
|
10
|
+
{
|
11
|
+
'first_box' => 'http://box_host/first_box.box',
|
12
|
+
'second_box' => 'http://box_host/second_box.box',
|
13
|
+
'third_box' => 'http://box_host/third_box.box',
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
let(:vms) do
|
18
|
+
[
|
19
|
+
{'name' => 'one', 'box' => 'first_box'},
|
20
|
+
{'name' => 'two', 'box' => 'second_box'},
|
21
|
+
{'name' => 'four', 'box' => 'fourth_box'},
|
22
|
+
{
|
23
|
+
'name' => 'five',
|
24
|
+
'box' => 'fifth_box',
|
25
|
+
'box_url' => 'https://another_box_host/fifth_box.box',
|
26
|
+
},
|
27
|
+
]
|
28
|
+
end
|
29
|
+
|
30
|
+
describe 'without a boxes key' do
|
31
|
+
let(:config) do
|
32
|
+
{
|
33
|
+
'vms' => vms,
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
before { subject.set_config(config) }
|
38
|
+
|
39
|
+
it "doesn't modify any boxes" do
|
40
|
+
input = dup(config)
|
41
|
+
output = subject.run
|
42
|
+
expect(output).to eq config
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe 'when the boxes key is given' do
|
47
|
+
let(:config) do
|
48
|
+
{
|
49
|
+
'boxes' => boxes,
|
50
|
+
'vms' => vms,
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
before { subject.set_config(config) }
|
55
|
+
|
56
|
+
it "removes the 'boxes' key" do
|
57
|
+
output = subject.run
|
58
|
+
expect(output).to_not have_key 'boxes'
|
59
|
+
end
|
60
|
+
|
61
|
+
it "adds the 'box_url' to VMs whose 'box' value is in the 'boxes' list" do
|
62
|
+
output = subject.run
|
63
|
+
vms = output['vms']
|
64
|
+
|
65
|
+
expect(vms[0]['box_url']).to eq 'http://box_host/first_box.box'
|
66
|
+
expect(vms[1]['box_url']).to eq 'http://box_host/second_box.box'
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "if the VM 'box' value doesn't have a match in the box list" do
|
70
|
+
it "doesn't modify the 'box_url'" do
|
71
|
+
output = subject.run
|
72
|
+
vms = output['vms']
|
73
|
+
|
74
|
+
expect(vms[2]['box_url']).to be_nil
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe "if the VM 'box_url' is already set" do
|
79
|
+
it "doesn't modify the 'box_url'" do
|
80
|
+
output = subject.run
|
81
|
+
vms = output['vms']
|
82
|
+
|
83
|
+
expect(vms[3]['box_url']).to eq 'https://another_box_host/fifth_box.box'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,287 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ConfigBuilder::Filter::Roles do
|
4
|
+
|
5
|
+
def dup(o)
|
6
|
+
Marshal.load(Marshal.dump(o))
|
7
|
+
end
|
8
|
+
|
9
|
+
let(:roles) do
|
10
|
+
{
|
11
|
+
'shell-provisioner' => {
|
12
|
+
'provisioners' => [
|
13
|
+
{'type' => 'shell', 'inline' => '/usr/bin/sl'},
|
14
|
+
],
|
15
|
+
},
|
16
|
+
'puppet-provisioner' => {
|
17
|
+
'provisioners' => [
|
18
|
+
{'type' => 'puppet', 'manifest' => 'sl.pp'},
|
19
|
+
{'type' => 'puppet', 'manifest' => 'starwars.pp'},
|
20
|
+
],
|
21
|
+
},
|
22
|
+
'potato-provisioner' => {
|
23
|
+
'provisioners' => [
|
24
|
+
{'type' => 'potato', 'potato' => 'POHTAHTO.pp'},
|
25
|
+
],
|
26
|
+
},
|
27
|
+
'folders-12' => {
|
28
|
+
'synced_folders' => [
|
29
|
+
{'guest_path' => '/guest-1', 'host_path' => './host-1'},
|
30
|
+
{'guest_path' => '/guest-2', 'host_path' => './host-2'},
|
31
|
+
],
|
32
|
+
},
|
33
|
+
'folders-34' => {
|
34
|
+
'synced_folders' => [
|
35
|
+
{'guest_path' => '/guest-3', 'host_path' => './host-3'},
|
36
|
+
{'guest_path' => '/guest-4', 'host_path' => './host-4'},
|
37
|
+
],
|
38
|
+
},
|
39
|
+
'shared-networks' => {
|
40
|
+
'private_networks' => [
|
41
|
+
{'ip' => '1.2.3.4'}
|
42
|
+
]
|
43
|
+
}
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
let(:vms) do
|
48
|
+
[
|
49
|
+
{'name' => 'master'},
|
50
|
+
{'name' => 'debian-6-agent'},
|
51
|
+
]
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
describe "without a top level roles key" do
|
56
|
+
|
57
|
+
let(:config) do
|
58
|
+
{'vms' => vms}
|
59
|
+
end
|
60
|
+
|
61
|
+
before do
|
62
|
+
subject.set_config(dup(config))
|
63
|
+
end
|
64
|
+
|
65
|
+
it "doesn't alter the structure" do
|
66
|
+
input = dup(config)
|
67
|
+
output = subject.run
|
68
|
+
|
69
|
+
expect(output).to eq config
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe 'removing the role' do
|
74
|
+
let(:config) do
|
75
|
+
{
|
76
|
+
'vms' => [{'name' => 'master'}],
|
77
|
+
'roles' => roles,
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
before do
|
82
|
+
subject.set_config(dup(config))
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'strips out the roles key' do
|
86
|
+
output = subject.run
|
87
|
+
expect(output).to_not have_key 'roles'
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe 'and a vm with no roles' do
|
92
|
+
let(:vms) { [{'name' => 'master'}] }
|
93
|
+
|
94
|
+
let(:config) do
|
95
|
+
{
|
96
|
+
'vms' => vms,
|
97
|
+
'roles' => roles,
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
before do
|
102
|
+
subject.set_config(dup(config))
|
103
|
+
end
|
104
|
+
|
105
|
+
it "doesn't alter the vm" do
|
106
|
+
output = subject.run
|
107
|
+
expect(output['vms']).to eq vms
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe 'and one vm' do
|
112
|
+
describe 'with one role' do
|
113
|
+
let(:vms) { [{'name' => 'master', 'roles' => 'shell-provisioner'}] }
|
114
|
+
|
115
|
+
let(:config) do
|
116
|
+
{
|
117
|
+
'vms' => vms,
|
118
|
+
'roles' => roles,
|
119
|
+
}
|
120
|
+
end
|
121
|
+
|
122
|
+
before do
|
123
|
+
subject.set_config(dup(config))
|
124
|
+
end
|
125
|
+
|
126
|
+
let(:filtered_vm) do
|
127
|
+
output = subject.run
|
128
|
+
output['vms'][0]
|
129
|
+
end
|
130
|
+
|
131
|
+
it "removes the 'roles' key" do
|
132
|
+
expect(filtered_vm).to_not have_key 'roles'
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'applies the role' do
|
136
|
+
expected = [{
|
137
|
+
'type' => 'shell',
|
138
|
+
'inline' => '/usr/bin/sl',
|
139
|
+
}]
|
140
|
+
|
141
|
+
expect(filtered_vm['provisioners']).to eq expected
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
describe 'with two roles' do
|
146
|
+
let(:vms) do
|
147
|
+
[{
|
148
|
+
'name' => 'master',
|
149
|
+
'roles' => ['shell-provisioner', 'folders-12'],
|
150
|
+
}]
|
151
|
+
end
|
152
|
+
|
153
|
+
let(:config) do
|
154
|
+
{
|
155
|
+
'vms' => vms,
|
156
|
+
'roles' => roles,
|
157
|
+
}
|
158
|
+
end
|
159
|
+
|
160
|
+
before do
|
161
|
+
subject.set_config(dup(config))
|
162
|
+
end
|
163
|
+
|
164
|
+
let(:filtered_vm) do
|
165
|
+
output = subject.run
|
166
|
+
output['vms'][0]
|
167
|
+
end
|
168
|
+
|
169
|
+
it 'applies all of the roles' do
|
170
|
+
expected_prov = [{
|
171
|
+
'type' => 'shell',
|
172
|
+
'inline' => '/usr/bin/sl',
|
173
|
+
}]
|
174
|
+
|
175
|
+
expected_folder = [
|
176
|
+
{'guest_path' => '/guest-1', 'host_path' => './host-1'},
|
177
|
+
{'guest_path' => '/guest-2', 'host_path' => './host-2'},
|
178
|
+
]
|
179
|
+
|
180
|
+
expect(filtered_vm['provisioners']).to eq expected_prov
|
181
|
+
expect(filtered_vm['synced_folders']).to eq expected_folder
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
describe "with multiple VMs and shared roles" do
|
187
|
+
let(:vms) do
|
188
|
+
[
|
189
|
+
{
|
190
|
+
'name' => 'master',
|
191
|
+
'roles' => ['shell-provisioner', 'potato-provisioner', 'folders-12', 'folders-34', 'shared-networks'],
|
192
|
+
},
|
193
|
+
{
|
194
|
+
'name' => 'agent',
|
195
|
+
'roles' => ['shell-provisioner', 'puppet-provisioner', 'folders-34', 'shared-networks'],
|
196
|
+
}
|
197
|
+
]
|
198
|
+
end
|
199
|
+
|
200
|
+
let(:config) do
|
201
|
+
{
|
202
|
+
'vms' => vms,
|
203
|
+
'roles' => roles,
|
204
|
+
}
|
205
|
+
end
|
206
|
+
|
207
|
+
before do
|
208
|
+
subject.set_config(dup(config))
|
209
|
+
end
|
210
|
+
|
211
|
+
let(:filtered_vms) do
|
212
|
+
output = subject.run
|
213
|
+
output['vms']
|
214
|
+
end
|
215
|
+
|
216
|
+
describe 'the first node' do
|
217
|
+
let(:vm) { filtered_vms[0] }
|
218
|
+
it 'has the provisioners set in the right order' do
|
219
|
+
expected = [
|
220
|
+
{'type' => 'potato', 'potato' => 'POHTAHTO.pp'},
|
221
|
+
{'type' => 'shell', 'inline' => '/usr/bin/sl'},
|
222
|
+
]
|
223
|
+
expect(vm['provisioners']).to eq expected
|
224
|
+
end
|
225
|
+
|
226
|
+
it 'has the synced folders set in the right order' do
|
227
|
+
expected = [
|
228
|
+
{'guest_path' => '/guest-3', 'host_path' => './host-3'},
|
229
|
+
{'guest_path' => '/guest-4', 'host_path' => './host-4'},
|
230
|
+
{'guest_path' => '/guest-1', 'host_path' => './host-1'},
|
231
|
+
{'guest_path' => '/guest-2', 'host_path' => './host-2'},
|
232
|
+
]
|
233
|
+
|
234
|
+
expect(vm['synced_folders']).to eq expected
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
describe 'the second node' do
|
239
|
+
let(:vm) { filtered_vms[1] }
|
240
|
+
|
241
|
+
it 'has the provisioners set in the right order' do
|
242
|
+
expected = [
|
243
|
+
{'type' => 'puppet', 'manifest' => 'sl.pp'},
|
244
|
+
{'type' => 'puppet', 'manifest' => 'starwars.pp'},
|
245
|
+
{'type' => 'shell', 'inline' => '/usr/bin/sl'},
|
246
|
+
]
|
247
|
+
expect(vm['provisioners']).to eq expected
|
248
|
+
end
|
249
|
+
|
250
|
+
it 'has the synced folders set in the right order' do
|
251
|
+
expected = [
|
252
|
+
{'guest_path' => '/guest-3', 'host_path' => './host-3'},
|
253
|
+
{'guest_path' => '/guest-4', 'host_path' => './host-4'},
|
254
|
+
]
|
255
|
+
|
256
|
+
expect(vm['synced_folders']).to eq expected
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
context 'when modifying an array inherited from a role' do
|
261
|
+
before :each do
|
262
|
+
filtered_vms[0]['private_networks'].push 'ip' => '5.6.7.8'
|
263
|
+
end
|
264
|
+
|
265
|
+
it 'other VMs using that role are not affected' do
|
266
|
+
expect(filtered_vms[1]['private_networks']).to eq roles['shared-networks']['private_networks']
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
describe 'when a class references a non-existent role' do
|
272
|
+
let(:config) do
|
273
|
+
{
|
274
|
+
'vms' => [{'name' => 'master', 'roles' => 'nope'}],
|
275
|
+
'roles' => {'yep' => {'box' => 'moxxi'}}
|
276
|
+
}
|
277
|
+
end
|
278
|
+
|
279
|
+
before do
|
280
|
+
subject.set_config(dup(config))
|
281
|
+
end
|
282
|
+
|
283
|
+
it 'raises an error' do
|
284
|
+
expect { subject.run }.to raise_error(/^Requested role "nope" is not defined/)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|