logical-construct 0.0.5 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/flight-deck +3 -0
- data/doc/DESIGN +48 -0
- data/doc/EC2-baking-notes +70 -0
- data/doc/ExampleStartupRakefile +152 -0
- data/doc/ExampleTargetRakefile +4 -0
- data/doc/TODO +148 -0
- data/doc/Vb-EC2-translation-notes +96 -0
- data/doc/hating-chef +32 -0
- data/lib/logical-construct/archive-tasks.rb +307 -0
- data/lib/logical-construct/ground-control.rb +4 -1
- data/lib/logical-construct/ground-control/build-plan.rb +95 -0
- data/lib/logical-construct/ground-control/core.rb +1 -1
- data/lib/logical-construct/ground-control/generate-manifest.rb +67 -0
- data/lib/logical-construct/ground-control/provision.rb +73 -168
- data/lib/logical-construct/ground-control/run-on-target.rb +1 -1
- data/lib/logical-construct/ground-control/setup.rb +1 -4
- data/lib/logical-construct/ground-control/setup/copy-files.rb +2 -2
- data/lib/logical-construct/ground-control/tools.rb +66 -0
- data/lib/logical-construct/node-client.rb +112 -0
- data/lib/logical-construct/plan.rb +2 -0
- data/lib/logical-construct/plan/core.rb +45 -0
- data/lib/logical-construct/plan/standalone-bundle.rb +80 -0
- data/lib/logical-construct/port-open-check.rb +41 -0
- data/lib/logical-construct/protocol.rb +2 -0
- data/lib/logical-construct/protocol/plan-validation.rb +46 -0
- data/lib/logical-construct/protocol/ssh-tunnel.rb +127 -0
- data/lib/logical-construct/protocol/vocabulary.rb +8 -0
- data/lib/logical-construct/target/Implement.rake +8 -0
- data/lib/logical-construct/target/command-line.rb +90 -0
- data/lib/logical-construct/target/flight-deck.rb +341 -0
- data/lib/logical-construct/target/implementation.rb +33 -0
- data/lib/logical-construct/target/plan-records.rb +317 -0
- data/lib/logical-construct/target/resolution-server.rb +153 -0
- data/lib/logical-construct/target/{unpack-cookbook.rb → unpack-plan.rb} +8 -4
- data/lib/logical-construct/template-file.rb +41 -0
- data/lib/templates/Rakefile.erb +8 -0
- data/spec/ground-control/smoke-test.rb +8 -7
- data/spec/node_resolution.rb +62 -0
- data/spec/target/plan-records.rb +142 -0
- data/spec/target/provisioning.rb +21 -0
- data/spec_help/file-sandbox.rb +12 -6
- data/spec_help/fixtures/Manifest +1 -0
- data/spec_help/fixtures/source/one.tbz +1 -0
- data/spec_help/fixtures/source/three.tbz +1 -0
- data/spec_help/fixtures/source/two.tbz +1 -0
- data/spec_help/spec_helper.rb +5 -7
- metadata +165 -72
- data/lib/logical-construct/ground-control/setup/build-files.rb +0 -93
- data/lib/logical-construct/ground-control/setup/create-construct-directory.rb +0 -22
- data/lib/logical-construct/ground-control/setup/install-init.rb +0 -32
- data/lib/logical-construct/resolving-task.rb +0 -141
- data/lib/logical-construct/satisfiable-task.rb +0 -87
- data/lib/logical-construct/target.rb +0 -4
- data/lib/logical-construct/target/chef-solo.rb +0 -37
- data/lib/logical-construct/target/platforms.rb +0 -51
- data/lib/logical-construct/target/platforms/aws.rb +0 -8
- data/lib/logical-construct/target/platforms/default/chef-config.rb +0 -134
- data/lib/logical-construct/target/platforms/default/resolve-configuration.rb +0 -44
- data/lib/logical-construct/target/platforms/default/volume.rb +0 -11
- data/lib/logical-construct/target/platforms/virtualbox.rb +0 -8
- data/lib/logical-construct/target/platforms/virtualbox/volume.rb +0 -15
- data/lib/logical-construct/target/provision.rb +0 -36
- data/lib/logical-construct/target/sinatra-resolver.rb +0 -99
- data/lib/logical-construct/testing/resolve-configuration.rb +0 -32
- data/lib/logical-construct/testing/resolving-task.rb +0 -15
- data/lib/templates/chef.rb.erb +0 -9
- data/lib/templates/construct.init.d.erb +0 -18
- data/lib/templates/resolver/finished.html.erb +0 -1
- data/lib/templates/resolver/index.html.erb +0 -17
- data/lib/templates/resolver/task-file-form.html.erb +0 -6
- data/lib/templates/resolver/task-form.html.erb +0 -6
- data/spec/resolution.rb +0 -147
- data/spec/target/chef-config.rb +0 -67
- data/spec/target/chef-solo.rb +0 -55
- data/spec/target/platforms.rb +0 -36
- data/spec/target/smoke-test.rb +0 -45
- data/spec_help/ungemmer.rb +0 -36
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'logical-construct/node-client'
|
2
|
+
|
3
|
+
module LogicalConstruct
|
4
|
+
class GenerateManifest < Mattock::Tasklib
|
5
|
+
default_namespace :manifest
|
6
|
+
|
7
|
+
setting :plan_archives, []
|
8
|
+
setting :graph_format, :jsonld
|
9
|
+
setting :target_address, 'localhost'
|
10
|
+
setting :target_port, 51076
|
11
|
+
|
12
|
+
def default_configuration(provision)
|
13
|
+
super
|
14
|
+
self.plan_archives = provision.proxy_value.plan_archives
|
15
|
+
self.target_address = provision.proxy_value.target_address
|
16
|
+
self.target_port = provision.proxy_value.local_target_port
|
17
|
+
end
|
18
|
+
|
19
|
+
def node_url
|
20
|
+
"http://#{target_address}:#{target_port}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def define
|
24
|
+
in_namespace do
|
25
|
+
desc "Dump manifest (mostly for debugging)"
|
26
|
+
task :dump, [:format] do |task, args|
|
27
|
+
require 'rdf/turtle'
|
28
|
+
format = args[:format] || graph_format
|
29
|
+
format = format.to_sym
|
30
|
+
|
31
|
+
base_url = "urn:manifest"
|
32
|
+
|
33
|
+
graph = ::RDF::Graph.new
|
34
|
+
focus = RoadForest::RDF::GraphFocus.new(base_url, graph)
|
35
|
+
builder = NodeClient::ManifestBuilder.new(focus)
|
36
|
+
|
37
|
+
plan_archives.each do |archive|
|
38
|
+
builder.add(archive)
|
39
|
+
end
|
40
|
+
|
41
|
+
puts(RDF::Writer.for(format).buffer(:base_uri => base_url) do |writer|
|
42
|
+
focus.relevant_prefixes.each do |prefix, uri|
|
43
|
+
writer.prefix(prefix, uri)
|
44
|
+
end
|
45
|
+
writer.insert(graph)
|
46
|
+
end)
|
47
|
+
end
|
48
|
+
|
49
|
+
task :deliver do |task|
|
50
|
+
client = NodeClient.new
|
51
|
+
client.node_url = node_url
|
52
|
+
client.plan_archives = plan_archives
|
53
|
+
client.deliver_manifest
|
54
|
+
end
|
55
|
+
|
56
|
+
task :fulfill do |task|
|
57
|
+
client = NodeClient.new
|
58
|
+
client.node_url = node_url
|
59
|
+
client.plan_archives = plan_archives
|
60
|
+
client.deliver_plans
|
61
|
+
end
|
62
|
+
end
|
63
|
+
task self[:dump] => root_task
|
64
|
+
task self[:deliver] => root_task
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -1,204 +1,109 @@
|
|
1
1
|
require 'mattock'
|
2
|
-
|
3
|
-
require 'logical-construct/
|
2
|
+
|
3
|
+
require 'logical-construct/ground-control/generate-manifest'
|
4
|
+
require 'logical-construct/ground-control/build-plan'
|
5
|
+
require 'logical-construct/protocol/ssh-tunnel'
|
4
6
|
|
5
7
|
module LogicalConstruct
|
6
8
|
module GroundControl
|
7
9
|
class Provision < Mattock::Tasklib
|
8
|
-
class WebConfigure < Mattock::Task
|
9
|
-
include ResolutionProtocol
|
10
|
-
|
11
|
-
setting :target_protocol, "http"
|
12
|
-
setting :target_address, nil
|
13
|
-
setting :target_port, 51076
|
14
|
-
runtime_setting :target_url
|
15
|
-
setting :resolutions
|
16
|
-
runtime_setting :web_resolutions
|
17
|
-
|
18
|
-
def resolve_runtime_configuration
|
19
|
-
super
|
20
|
-
self.target_url ||= "#{target_protocol}://#{target_address}:#{target_port}/"
|
21
|
-
self.web_resolutions = Hash[resolutions.map do |name, value|
|
22
|
-
[web_path(name), value]
|
23
|
-
end]
|
24
|
-
end
|
25
|
-
|
26
|
-
def resolve(path)
|
27
|
-
resolved = web_resolutions.fetch(path)
|
28
|
-
if resolved.respond_to? :call
|
29
|
-
resolved = resolved.call
|
30
|
-
end
|
31
|
-
return resolved
|
32
|
-
rescue KeyError
|
33
|
-
puts "Can't find a resolution for #{path} in #{web_resolutions.keys.inspect} (ex #{resolutions.keys})"
|
34
|
-
raise
|
35
|
-
end
|
36
|
-
|
37
|
-
def uri(options)
|
38
|
-
uri_class = URI.scheme_list[target_protocol.upcase]
|
39
|
-
uri_hash = {:host => target_address, :port => target_port}
|
40
|
-
return uri_class.build(uri_hash.merge(options)).to_s
|
41
|
-
end
|
42
|
-
|
43
|
-
def resolution_needed
|
44
|
-
index = RestClient.get(uri(:path => '/'))
|
45
|
-
body = Nokogiri::HTML(index.body)
|
46
|
-
return body.xpath("//a[@rel='requirement']")
|
47
|
-
end
|
48
|
-
|
49
|
-
|
50
|
-
#XXX I would like this to become an actual RESTful client at some
|
51
|
-
#point, but seems it would mean building it from scratch
|
52
|
-
def action
|
53
|
-
require 'uri'
|
54
|
-
require 'rest-client'
|
55
|
-
require 'nokogiri'
|
56
|
-
|
57
|
-
until (link = resolution_needed.first).nil?
|
58
|
-
href = link['href']
|
59
|
-
begin
|
60
|
-
response = RestClient.post(uri(:path => href), :data => resolve(href))
|
61
|
-
rescue RestClient::InternalServerError => ex
|
62
|
-
require 'tempfile'
|
63
|
-
file = Tempfile.open('provision-error.html')
|
64
|
-
path = file.path
|
65
|
-
file.close!
|
66
|
-
|
67
|
-
File::open(path, "w") do |file|
|
68
|
-
file.write ex.http_body
|
69
|
-
end
|
70
|
-
puts "Written error response to #{path}"
|
71
|
-
puts "Try: chromium #{path}"
|
72
|
-
fail "Unsuccessful response from server!"
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
10
|
default_namespace :provision
|
79
11
|
|
80
|
-
setting :valise
|
81
12
|
setting :target_protocol, "http"
|
82
13
|
setting(:target_address, nil).isnt(:copiable)
|
83
|
-
setting :
|
84
|
-
setting :
|
85
|
-
setting :marshalling_path, "marshall"
|
86
|
-
|
87
|
-
setting(:secret_data, nested {
|
88
|
-
setting :path
|
89
|
-
setting :tarball_path
|
90
|
-
setting :file_list
|
91
|
-
})
|
92
|
-
|
93
|
-
setting(:normal_data, nested {
|
94
|
-
setting :path
|
95
|
-
setting :tarball_path
|
96
|
-
setting :file_list
|
97
|
-
})
|
98
|
-
|
99
|
-
setting(:cookbooks, nested {
|
100
|
-
setting :path
|
101
|
-
setting :tarball_path
|
102
|
-
setting :file_list
|
103
|
-
})
|
104
|
-
|
105
|
-
setting :json_attribs_path
|
106
|
-
setting :roles
|
107
|
-
setting :node_attribs
|
108
|
-
setting :json_attribs, ""
|
109
|
-
|
110
|
-
def default_configuration(core)
|
111
|
-
core.copy_settings_to(self)
|
112
|
-
super
|
113
|
-
self.cookbooks.path = "cookbooks"
|
114
|
-
self.secret_data.path = "data-bags/secret"
|
115
|
-
self.normal_data.path = "data-bags"
|
116
|
-
self.resolutions = {}
|
117
|
-
self.roles = {}
|
118
|
-
self.node_attribs = { "run_list" => [] }
|
119
|
-
end
|
120
|
-
|
121
|
-
def resolve_configuration
|
122
|
-
super
|
123
|
-
self.json_attribs_path ||= File::join(marshalling_path, "node.json")
|
14
|
+
setting :local_target_port, 51076
|
15
|
+
setting :remote_target_port, 30712
|
124
16
|
|
125
|
-
|
126
|
-
self.secret_data.file_list ||= Rake::FileList[secret_data.path + "/**/*"].exclude(%r{[.]sw[.]$})
|
127
|
-
self.normal_data.file_list ||=
|
128
|
-
Rake::FileList[normal_data.path + "/**/*"].exclude(%r{^#{secret_data.path}}).exclude(%r{[.]sw[.]$})
|
17
|
+
setting :plan_archives, []
|
129
18
|
|
130
|
-
|
131
|
-
|
132
|
-
self.normal_data.tarball_path ||= File::join(marshalling_path, "normal_data_bags.tgz")
|
19
|
+
dir(:marshalling, "marshall")
|
20
|
+
dir(:plan_source, "plans")
|
133
21
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
resolutions["chef_config:secret_data_tarball"] ||= proc do
|
139
|
-
File::open(secret_data.tarball_path, "rb")
|
140
|
-
end
|
141
|
-
|
142
|
-
resolutions["chef_config:normal_data_tarball"] ||= proc do
|
143
|
-
File::open(normal_data.tarball_path, "rb")
|
144
|
-
end
|
22
|
+
def resolve_configuration
|
23
|
+
resolve_paths
|
24
|
+
super
|
145
25
|
end
|
146
26
|
|
147
|
-
include Mattock::CommandLineDSL
|
148
27
|
def define
|
28
|
+
manifest = nil
|
149
29
|
in_namespace do
|
150
|
-
directory
|
30
|
+
directory marshalling.absolute_path
|
151
31
|
|
152
32
|
task :collect, [:ipaddr] do |task, args|
|
153
33
|
self.target_address = args[:ipaddr]
|
154
34
|
end
|
155
35
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
self.json_attribs = JSON.pretty_generate(node_attribs)
|
161
|
-
resolutions["chef_config:json_attribs"] ||= json_attribs
|
36
|
+
tunnel = LogicalConstruct::SSHTunnel.new do |tunnel|
|
37
|
+
tunnel.target_address = proxy_value.target_address
|
38
|
+
copy_settings_to(tunnel)
|
39
|
+
self.local_target_port = tunnel.proxy_value.local_target_port
|
162
40
|
end
|
163
41
|
|
164
|
-
|
165
|
-
|
166
|
-
|
42
|
+
manifest = LogicalConstruct::GenerateManifest.new(self)
|
43
|
+
|
44
|
+
start_flight = Mattock::Rake::RemoteCommandTask.define_task(:start_flight => :collect) do |start_flight|
|
45
|
+
start_flight.remote_server.address = proxy_value.target_address
|
46
|
+
start_flight.command =
|
47
|
+
Mattock::CommandLine.new("nohup",
|
48
|
+
"/opt/logical-construct/bin/flight-deck",
|
49
|
+
"-C start_server")
|
50
|
+
start_flight.command.redirect_stdin("/dev/null")
|
51
|
+
start_flight.command.redirect_stdout("/opt/logical-construct/flight_deck_server.log")
|
52
|
+
start_flight.command.copy_stream_to(2, 1)
|
53
|
+
start_flight.command.redirections << "&"
|
167
54
|
end
|
168
55
|
|
169
|
-
|
170
|
-
|
56
|
+
start_resolution = Mattock::Rake::RemoteCommandTask.define_task(:start_resolution => :deliver_manifest) do |start_flight|
|
57
|
+
start_flight.remote_server.address = proxy_value.target_address
|
58
|
+
start_flight.verbose = 3
|
59
|
+
start_flight.command =
|
60
|
+
Mattock::CommandLine.new("nohup",
|
61
|
+
"/opt/logical-construct/bin/flight-deck")
|
62
|
+
#TODO: Mattock CommandLine needs a "background"
|
63
|
+
#TODO: RemoteCommandTask should wrap the whole
|
64
|
+
#nohup-redirect-background thing
|
65
|
+
start_flight.command.redirect_stdin("/dev/null")
|
66
|
+
start_flight.command.redirect_stdout("/opt/logical-construct/flight_deck.log")
|
67
|
+
start_flight.command.copy_stream_to(2, 1)
|
68
|
+
start_flight.command.redirections << "&"
|
171
69
|
end
|
172
70
|
|
173
|
-
|
174
|
-
cmd("tar",
|
175
|
-
"--exclude **/*.sw?",
|
176
|
-
"--exclude #{secret_data.path}",
|
177
|
-
"-czf", normal_data.tarball_path, normal_data.path).must_succeed!
|
178
|
-
end
|
71
|
+
task manifest.root_task => :collect
|
179
72
|
|
180
|
-
|
181
|
-
|
182
|
-
|
73
|
+
task_spine(:start_flight, :deliver_manifest, :fulfill_manifest, :start_resolution, :complete_provision)
|
74
|
+
task :deliver_manifest => manifest[:deliver]
|
75
|
+
task :fulfill_manifest => manifest[:fulfill]
|
183
76
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
77
|
+
tunnel.wrap(manifest[:deliver])
|
78
|
+
tunnel.wrap(manifest[:fulfill])
|
79
|
+
task :complete_provision => tunnel[:close_tunnel]
|
80
|
+
end
|
81
|
+
|
82
|
+
desc "Provision :ipaddr with specified configs"
|
83
|
+
task root_task, [:ipaddr] => self[:complete_provision]
|
84
|
+
end
|
85
|
+
|
86
|
+
def plan_task(name, &block)
|
87
|
+
plan = BuildPlan.new(self) do |build|
|
88
|
+
build.basename = name
|
89
|
+
yield build if block_given?
|
90
|
+
end
|
91
|
+
task self[:manifest] => plan.archive_path
|
193
92
|
|
194
|
-
|
195
|
-
|
196
|
-
|
93
|
+
in_namespace do
|
94
|
+
namespace :package do
|
95
|
+
desc "Compile and archive plan #{name.inspect}"
|
96
|
+
task name => plan.archive_path
|
197
97
|
end
|
198
98
|
end
|
199
99
|
|
200
|
-
|
201
|
-
|
100
|
+
plan_archives << plan.archive_path
|
101
|
+
end
|
102
|
+
|
103
|
+
def plans(*names)
|
104
|
+
names.each do |name|
|
105
|
+
plan_task(name)
|
106
|
+
end
|
202
107
|
end
|
203
108
|
end
|
204
109
|
end
|
@@ -18,7 +18,7 @@ module LogicalConstruct
|
|
18
18
|
def remote_task(name, comment = nil)
|
19
19
|
in_namespace do
|
20
20
|
desc comment unless comment.nil?
|
21
|
-
Mattock::RemoteCommandTask.
|
21
|
+
Mattock::Rake::RemoteCommandTask.define_task(name) do |task|
|
22
22
|
task.ssh_options += SSH_OPTIONS
|
23
23
|
|
24
24
|
task.runtime_definition do |task|
|
@@ -6,7 +6,7 @@ module LogicalConstruct
|
|
6
6
|
|
7
7
|
settings(
|
8
8
|
:remote_server => nested( :address => nil, :user => "root"),
|
9
|
-
:construct_dir => "/
|
9
|
+
:construct_dir => "/opt/logical-construct"
|
10
10
|
)
|
11
11
|
nil_fields :valise, :platform
|
12
12
|
|
@@ -47,7 +47,4 @@ module LogicalConstruct
|
|
47
47
|
end
|
48
48
|
end
|
49
49
|
|
50
|
-
require 'logical-construct/ground-control/setup/build-files'
|
51
|
-
require 'logical-construct/ground-control/setup/create-construct-directory'
|
52
50
|
require 'logical-construct/ground-control/setup/copy-files'
|
53
|
-
require 'logical-construct/ground-control/setup/install-init'
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'mattock/command-task'
|
2
2
|
|
3
3
|
module LogicalConstruct
|
4
|
-
class RemoteCopyFile < Mattock::CommandTask
|
4
|
+
class RemoteCopyFile < Mattock::Rake::CommandTask
|
5
5
|
nil_fields :destination_address
|
6
6
|
nil_fields :source_dir, :destination_dir, :basename
|
7
7
|
required_fields :source_path, :destination_path
|
@@ -58,7 +58,7 @@ module LogicalConstruct
|
|
58
58
|
|
59
59
|
def define
|
60
60
|
in_namespace do
|
61
|
-
RemoteCopyFile.
|
61
|
+
RemoteCopyFile.define_task(self, :construct_dir) do |task|
|
62
62
|
task.runtime_definition do
|
63
63
|
task.remote_server = remote_server
|
64
64
|
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'mattock'
|
2
|
+
|
3
|
+
module LogicalConstruct
|
4
|
+
module GroundControl
|
5
|
+
class Tools < ::Mattock::Tasklib
|
6
|
+
include Mattock::CommandLineDSL
|
7
|
+
|
8
|
+
default_namespace :tools
|
9
|
+
|
10
|
+
dir(:plans, "plans")
|
11
|
+
|
12
|
+
def resolve_configuration
|
13
|
+
resolve_paths
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def plans_dir
|
18
|
+
plans.absolute_path.sub(%r{/$},'')
|
19
|
+
end
|
20
|
+
|
21
|
+
def define
|
22
|
+
in_namespace do
|
23
|
+
namespace :create_plan do
|
24
|
+
rule %r{\A#{plans_dir}/[^/]*\Z} do |task, args|
|
25
|
+
cmd("mkdir", "-p", task.name).must_succeed! #ok
|
26
|
+
end
|
27
|
+
|
28
|
+
rule(%r{\A#{plans_dir}/[^/]*/plan\.rake\Z}, [:name] => ['%d']) do |task, args|
|
29
|
+
require 'logical-construct/target/implementation'
|
30
|
+
|
31
|
+
File::open(task.name, "w") do |file|
|
32
|
+
indent = 16
|
33
|
+
file.write(<<-EOR.gsub(/^#{" "*indent}/,''))
|
34
|
+
require 'logical-construct/plan'
|
35
|
+
include LogicalConstruct::Plan
|
36
|
+
|
37
|
+
core = Core.new do |core|
|
38
|
+
core.namespace_name = :#{args[:name]}
|
39
|
+
core.plan_rakefile.absolute_path = __FILE__
|
40
|
+
end
|
41
|
+
|
42
|
+
core.in_namespace do
|
43
|
+
#Plan tasks go here
|
44
|
+
#
|
45
|
+
#Important tasks to make dependencies to:
|
46
|
+
|
47
|
+
EOR
|
48
|
+
|
49
|
+
Target::Implementation.task_list.each do |task_name|
|
50
|
+
file.puts(" #task :#{task_name}")
|
51
|
+
end
|
52
|
+
|
53
|
+
file.puts("end")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
desc "Create a new plan to be part of a provisioning"
|
59
|
+
task :create_plan, [:name] do |task, args|
|
60
|
+
Rake::Task[File::join(plans_dir, args[:name], "plan.rake")].invoke(args[:name])
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|