chef-dk 0.5.1 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/chef-dk/builtin_commands.rb +2 -0
- data/lib/chef-dk/chef_runner.rb +5 -1
- data/lib/chef-dk/command/provision.rb +399 -0
- data/lib/chef-dk/cookbook_profiler/git.rb +59 -3
- data/lib/chef-dk/exceptions.rb +1 -13
- data/lib/chef-dk/service_exceptions.rb +38 -22
- data/lib/chef-dk/skeletons/code_generator/files/default/Berksfile +1 -1
- data/lib/chef-dk/skeletons/code_generator/templates/default/metadata.rb.erb +5 -6
- data/lib/chef-dk/skeletons/code_generator/templates/default/recipe_spec.rb.erb +0 -3
- data/lib/chef-dk/skeletons/code_generator/templates/default/serverspec_default_spec.rb.erb +1 -4
- data/lib/chef-dk/version.rb +1 -1
- data/spec/unit/chef_runner_spec.rb +8 -1
- data/spec/unit/command/provision_spec.rb +536 -0
- data/spec/unit/cookbook_profiler/git_spec.rb +99 -62
- metadata +107 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c6b56f8142a1f44e274b102d857a291813f4808b
|
4
|
+
data.tar.gz: 64405a6fee63415614dc9d98073866d0c89d849f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5dd5c2a1d046ef6252b2115b2c2db68365a55d047563ef1269c5bcab7be6c514bb5a8183fe6147e935dfac529e4f548271c32fab91cfc26fb4b3ab19a37f5b3c
|
7
|
+
data.tar.gz: 63805b53f8a1f61ea924c4960d81f4c770e0d86093b0b50313e20dbd1b589b6d97130b6d0589d1e36440e05d6a52485f6ad646b68c30c2316f40850efda4e12a
|
@@ -35,6 +35,8 @@ ChefDK.commands do |c|
|
|
35
35
|
|
36
36
|
c.builtin "diff", :Diff, desc: "Generate an itemized diff of two Policyfile lock documents"
|
37
37
|
|
38
|
+
c.builtin "provision", :Provision, desc: "Provision VMs and clusters via cookbook"
|
39
|
+
|
38
40
|
c.builtin "export", :Export, desc: "Export a policy lock as a Chef Zero code repo"
|
39
41
|
|
40
42
|
c.builtin "verify", :Verify, desc: "Test the embedded ChefDK applications"
|
data/lib/chef-dk/chef_runner.rb
CHANGED
@@ -16,6 +16,7 @@
|
|
16
16
|
#
|
17
17
|
|
18
18
|
require 'chef-dk/exceptions'
|
19
|
+
require 'chef-dk/service_exceptions'
|
19
20
|
require 'chef'
|
20
21
|
|
21
22
|
module ChefDK
|
@@ -58,7 +59,10 @@ module ChefDK
|
|
58
59
|
end
|
59
60
|
|
60
61
|
def formatter
|
61
|
-
@formatter ||=
|
62
|
+
@formatter ||=
|
63
|
+
Chef::EventDispatch::Dispatcher.new.tap do |d|
|
64
|
+
d.register(Chef::Formatters.new(:doc, stdout, stderr))
|
65
|
+
end
|
62
66
|
end
|
63
67
|
|
64
68
|
def configure
|
@@ -0,0 +1,399 @@
|
|
1
|
+
#
|
2
|
+
# Copyright:: Copyright (c) 2014 Chef Software Inc.
|
3
|
+
# License:: Apache License, Version 2.0
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
#
|
17
|
+
|
18
|
+
require 'chef-dk/command/base'
|
19
|
+
require 'chef-dk/configurable'
|
20
|
+
require 'chef-dk/chef_runner'
|
21
|
+
require 'chef-dk/policyfile_services/push'
|
22
|
+
|
23
|
+
require 'chef/provisioning'
|
24
|
+
|
25
|
+
module ChefDK
|
26
|
+
|
27
|
+
module ProvisioningData
|
28
|
+
|
29
|
+
def self.reset
|
30
|
+
@context = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.context
|
34
|
+
@context ||= Context.new
|
35
|
+
end
|
36
|
+
|
37
|
+
class Context
|
38
|
+
|
39
|
+
attr_accessor :action
|
40
|
+
|
41
|
+
attr_accessor :node_name
|
42
|
+
|
43
|
+
attr_accessor :enable_policyfile
|
44
|
+
|
45
|
+
attr_accessor :policy_group
|
46
|
+
|
47
|
+
attr_accessor :policy_name
|
48
|
+
|
49
|
+
attr_accessor :extra_chef_config
|
50
|
+
|
51
|
+
def initialize
|
52
|
+
@extra_chef_config = ""
|
53
|
+
end
|
54
|
+
|
55
|
+
def convergence_options
|
56
|
+
{
|
57
|
+
chef_server: Chef::Config.chef_server_url,
|
58
|
+
chef_config: chef_config
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
def chef_config
|
63
|
+
config=<<-CONFIG
|
64
|
+
# SSL Settings:
|
65
|
+
ssl_verify_mode #{Chef::Config.ssl_verify_mode.inspect}
|
66
|
+
|
67
|
+
CONFIG
|
68
|
+
if enable_policyfile
|
69
|
+
policyfile_config=<<-CONFIG
|
70
|
+
# Policyfile Settings:
|
71
|
+
use_policyfile true
|
72
|
+
policy_document_native_api true
|
73
|
+
|
74
|
+
policy_group "#{policy_group}"
|
75
|
+
policy_name "#{policy_name}"
|
76
|
+
|
77
|
+
CONFIG
|
78
|
+
config << policyfile_config
|
79
|
+
end
|
80
|
+
|
81
|
+
config << extra_chef_config.to_s
|
82
|
+
config
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
module Command
|
89
|
+
|
90
|
+
class Provision < Base
|
91
|
+
|
92
|
+
banner(<<-E)
|
93
|
+
Usage: chef provision POLICY_GROUP --policy-name POLICY_NAME [options]
|
94
|
+
chef provision POLICY_GROUP --sync [POLICYFILE_PATH] [options]
|
95
|
+
chef provision --no-policy [options]
|
96
|
+
|
97
|
+
`chef provision` invokes an embedded chef-client run to provision machines
|
98
|
+
using Chef Provisioning. If not otherwise specified, `chef provision` will
|
99
|
+
expect to find a cookbook named 'provision' in the current working directory.
|
100
|
+
It runs a recipe in this cookbook which should use Chef Provisioning to create
|
101
|
+
one or more machines (or other infrastructure).
|
102
|
+
|
103
|
+
`chef provision` provides three forms of operation:
|
104
|
+
|
105
|
+
### chef provision POLICY_GROUP --policy-name POLICY_NAME
|
106
|
+
|
107
|
+
In the first form of the command, `chef provision` creates machines that will
|
108
|
+
operate in policyfile mode. The chef configuration passed to the cookbook will
|
109
|
+
set the policy group and policy name as given.
|
110
|
+
|
111
|
+
### chef provision POLICY_GROUP --sync [POLICYFILE_PATH] [options]
|
112
|
+
|
113
|
+
In the second form of the command, `chef provision` create machines that will
|
114
|
+
operate in policyfile mode and syncronizes a local policyfile to the server
|
115
|
+
before converging the machine(s) defined in the provision cookbook.
|
116
|
+
|
117
|
+
### chef provision --no-policy [options]
|
118
|
+
|
119
|
+
In the third form of the command, `chef provision` expects to create machines
|
120
|
+
that will not operate in policyfile mode.
|
121
|
+
|
122
|
+
Note that this command is considered beta. Behavior, the APIs that pass CLI
|
123
|
+
data to chef-client, and argument names may change as more experience is gained
|
124
|
+
from real-world usage.
|
125
|
+
|
126
|
+
Chef Provisioning is documented at https://docs.chef.io/provisioning.html
|
127
|
+
|
128
|
+
Options:
|
129
|
+
|
130
|
+
E
|
131
|
+
include Configurable
|
132
|
+
|
133
|
+
option :config_file,
|
134
|
+
short: "-c CONFIG_FILE",
|
135
|
+
long: "--config CONFIG_FILE",
|
136
|
+
description: "Path to configuration file"
|
137
|
+
|
138
|
+
option :policy_name,
|
139
|
+
short: "-p POLICY_NAME",
|
140
|
+
long: "--policy-name POLICY_NAME",
|
141
|
+
description: "Set the default policy name for provisioned machines"
|
142
|
+
|
143
|
+
option :sync,
|
144
|
+
short: "-s [POLICYFILE_PATH]",
|
145
|
+
long: "--sync [POLICYFILE_PATH]",
|
146
|
+
description: "Push policyfile to the server before converging node(s)"
|
147
|
+
|
148
|
+
option :enable_policyfile,
|
149
|
+
long: "--[no-]policy",
|
150
|
+
description: "Enable/disable policyfile integration (defaults to enabled, use --no-policy to disable)",
|
151
|
+
default: true
|
152
|
+
|
153
|
+
option :destroy,
|
154
|
+
short: "-d",
|
155
|
+
long: "--destroy",
|
156
|
+
description: "Set default machine action to :destroy",
|
157
|
+
default: false,
|
158
|
+
boolean: true
|
159
|
+
|
160
|
+
option :machine_recipe,
|
161
|
+
short: "-r RECIPE",
|
162
|
+
long: "--recipe RECIPE",
|
163
|
+
description: "Machine recipe to use",
|
164
|
+
default: "default"
|
165
|
+
|
166
|
+
option :cookbook,
|
167
|
+
long: "--cookbook COOKBOOK_PATH",
|
168
|
+
description: "Path to your provisioning cookbook",
|
169
|
+
default: "./provision"
|
170
|
+
|
171
|
+
option :node_name,
|
172
|
+
short: "-n NODE_NAME",
|
173
|
+
long: "--node-name NODE_NAME",
|
174
|
+
description: "Set default node name (may be overriden by provisioning cookbook)"
|
175
|
+
|
176
|
+
option :debug,
|
177
|
+
short: "-D",
|
178
|
+
long: "--debug",
|
179
|
+
description: "Enable stacktraces and other debug output",
|
180
|
+
default: false
|
181
|
+
|
182
|
+
|
183
|
+
attr_reader :params
|
184
|
+
attr_reader :policyfile_relative_path
|
185
|
+
attr_reader :policy_group
|
186
|
+
|
187
|
+
attr_accessor :ui
|
188
|
+
|
189
|
+
def initialize(*args)
|
190
|
+
super
|
191
|
+
|
192
|
+
@ui = UI.new
|
193
|
+
|
194
|
+
@policyfile_relative_path = nil
|
195
|
+
@policy_group = nil
|
196
|
+
|
197
|
+
@provisioning_cookbook_path = nil
|
198
|
+
@provisioning_cookbook_name = nil
|
199
|
+
end
|
200
|
+
|
201
|
+
def run(params = [])
|
202
|
+
return 1 unless apply_params!(params)
|
203
|
+
chef_config # force chef config to load
|
204
|
+
return 1 unless check_cookbook_and_recipe_path
|
205
|
+
|
206
|
+
push.run if sync_policy?
|
207
|
+
|
208
|
+
setup_context
|
209
|
+
|
210
|
+
chef_runner.converge
|
211
|
+
0
|
212
|
+
rescue ChefRunnerError, PolicyfileServiceError => e
|
213
|
+
handle_error(e)
|
214
|
+
1
|
215
|
+
# Chef Provisioning doesn't fail gracefully when a driver is missing:
|
216
|
+
# https://github.com/chef/chef-provisioning/issues/338
|
217
|
+
rescue StandardError, LoadError => error
|
218
|
+
ui.err("Error: #{error.message}")
|
219
|
+
1
|
220
|
+
end
|
221
|
+
|
222
|
+
# An instance of ChefRunner. Calling ChefRunner#converge will trigger
|
223
|
+
# convergence and generate the desired code.
|
224
|
+
def chef_runner
|
225
|
+
@chef_runner ||= ChefRunner.new(provisioning_cookbook_path, ["recipe[#{provisioning_cookbook_name}::#{recipe}]"])
|
226
|
+
end
|
227
|
+
|
228
|
+
def push
|
229
|
+
@push ||= PolicyfileServices::Push.new(policyfile: policyfile_relative_path,
|
230
|
+
ui: ui,
|
231
|
+
policy_group: policy_group,
|
232
|
+
config: chef_config,
|
233
|
+
root_dir: Dir.pwd)
|
234
|
+
end
|
235
|
+
|
236
|
+
def setup_context
|
237
|
+
ProvisioningData.context.tap do |c|
|
238
|
+
|
239
|
+
c.action = default_action
|
240
|
+
c.node_name = node_name
|
241
|
+
|
242
|
+
c.enable_policyfile = enable_policyfile?
|
243
|
+
|
244
|
+
if enable_policyfile?
|
245
|
+
c.policy_group = policy_group
|
246
|
+
c.policy_name = policy_name
|
247
|
+
end
|
248
|
+
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def policy_name
|
253
|
+
if sync_policy?
|
254
|
+
push.policy_data["name"]
|
255
|
+
else
|
256
|
+
config[:policy_name]
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def default_action
|
261
|
+
if config[:destroy]
|
262
|
+
:destroy
|
263
|
+
else
|
264
|
+
:converge
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def node_name
|
269
|
+
config[:node_name]
|
270
|
+
end
|
271
|
+
|
272
|
+
def recipe
|
273
|
+
config[:machine_recipe]
|
274
|
+
end
|
275
|
+
|
276
|
+
# Gives the `cookbook_path` in the chef-client sense, which is the
|
277
|
+
# directory that contains the provisioning cookbook.
|
278
|
+
def provisioning_cookbook_path
|
279
|
+
detect_provisioning_cookbook_name_and_path! unless @provisioning_cookbook_path
|
280
|
+
@provisioning_cookbook_path
|
281
|
+
end
|
282
|
+
|
283
|
+
# The name of the provisioning cookbook
|
284
|
+
def provisioning_cookbook_name
|
285
|
+
detect_provisioning_cookbook_name_and_path! unless @provisioning_cookbook_name
|
286
|
+
@provisioning_cookbook_name
|
287
|
+
end
|
288
|
+
|
289
|
+
def cookbook_path
|
290
|
+
config[:cookbook]
|
291
|
+
end
|
292
|
+
|
293
|
+
def enable_policyfile?
|
294
|
+
config[:enable_policyfile]
|
295
|
+
end
|
296
|
+
|
297
|
+
def apply_params!(params)
|
298
|
+
remaining_args = parse_options(params)
|
299
|
+
if enable_policyfile?
|
300
|
+
handle_policy_argv(remaining_args)
|
301
|
+
else
|
302
|
+
handle_no_policy_argv(remaining_args)
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
def debug?
|
307
|
+
!!config[:debug]
|
308
|
+
end
|
309
|
+
|
310
|
+
def sync_policy?
|
311
|
+
config.key?(:sync)
|
312
|
+
end
|
313
|
+
|
314
|
+
private
|
315
|
+
|
316
|
+
def detect_provisioning_cookbook_name_and_path!
|
317
|
+
given_path = File.expand_path(cookbook_path, Dir.pwd)
|
318
|
+
@provisioning_cookbook_name = File.basename(given_path)
|
319
|
+
@provisioning_cookbook_path = File.dirname(given_path)
|
320
|
+
end
|
321
|
+
|
322
|
+
def check_cookbook_and_recipe_path
|
323
|
+
if !File.exist?(cookbook_expanded_path)
|
324
|
+
ui.err("ERROR: Provisioning cookbook not found at path #{cookbook_expanded_path}")
|
325
|
+
false
|
326
|
+
elsif !File.exist?(provisioning_recipe_path)
|
327
|
+
ui.err("ERROR: Provisioning recipe not found at path #{provisioning_recipe_path}")
|
328
|
+
false
|
329
|
+
else
|
330
|
+
true
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
def provisioning_recipe_path
|
335
|
+
File.join(cookbook_expanded_path, 'recipes', "#{recipe}.rb")
|
336
|
+
end
|
337
|
+
|
338
|
+
def cookbook_expanded_path
|
339
|
+
File.join(chef_runner.cookbook_path, provisioning_cookbook_name)
|
340
|
+
end
|
341
|
+
|
342
|
+
def handle_no_policy_argv(remaining_args)
|
343
|
+
if remaining_args.empty?
|
344
|
+
true
|
345
|
+
else
|
346
|
+
ui.err("The --no-policy flag cannot be combined with policyfile arguments")
|
347
|
+
ui.err("")
|
348
|
+
ui.err(opt_parser)
|
349
|
+
return false
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
def handle_policy_argv(remaining_args)
|
354
|
+
if remaining_args.size > 1
|
355
|
+
ui.err("Too many arguments")
|
356
|
+
ui.err("")
|
357
|
+
ui.err(opt_parser)
|
358
|
+
false
|
359
|
+
elsif remaining_args.size < 1
|
360
|
+
ui.err("You must specify a POLICY_GROUP or disable policyfiles with --no-policy")
|
361
|
+
ui.err("")
|
362
|
+
ui.err(opt_parser)
|
363
|
+
false
|
364
|
+
elsif !sync_policy? && config[:policy_name].nil?
|
365
|
+
ui.err("You must pass either --sync or --policy-name to provision machines in policyfile mode")
|
366
|
+
ui.err("")
|
367
|
+
ui.err(opt_parser)
|
368
|
+
false
|
369
|
+
elsif sync_policy? && config[:policy_name]
|
370
|
+
ui.err("The --policy-name and --sync arguments cannot be combined")
|
371
|
+
ui.err("")
|
372
|
+
ui.err(opt_parser)
|
373
|
+
false
|
374
|
+
elsif sync_policy?
|
375
|
+
@policy_group = remaining_args[0]
|
376
|
+
@policyfile_relative_path = config[:sync]
|
377
|
+
true
|
378
|
+
elsif config[:policy_name]
|
379
|
+
@policy_group = remaining_args[0]
|
380
|
+
true
|
381
|
+
else
|
382
|
+
raise BUG, "Cannot properly parse input argv '#{ARGV.inspect}'"
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
def handle_error(error)
|
387
|
+
ui.err("Error: #{error.message}")
|
388
|
+
if error.respond_to?(:reason)
|
389
|
+
ui.err("Reason: #{error.reason}")
|
390
|
+
ui.err("")
|
391
|
+
ui.err(error.extended_error_info) if debug?
|
392
|
+
ui.err(error.cause.backtrace.join("\n")) if debug?
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
@@ -27,6 +27,8 @@ module ChefDK
|
|
27
27
|
|
28
28
|
def initialize(cookbook_path)
|
29
29
|
@cookbook_path = cookbook_path
|
30
|
+
@unborn_branch = nil
|
31
|
+
@unborn_branch_ref = nil
|
30
32
|
end
|
31
33
|
|
32
34
|
def profile_data
|
@@ -46,6 +48,15 @@ module ChefDK
|
|
46
48
|
|
47
49
|
def revision
|
48
50
|
git!("rev-parse HEAD").stdout.strip
|
51
|
+
rescue Mixlib::ShellOut::ShellCommandFailed => e
|
52
|
+
# We may have an "unborn" branch, i.e. one with no commits.
|
53
|
+
if unborn_branch_ref
|
54
|
+
nil
|
55
|
+
else
|
56
|
+
# if we got here, but verify_ref_cmd didn't error, we don't know why
|
57
|
+
# the original git command failed, so re-raise.
|
58
|
+
raise e
|
59
|
+
end
|
49
60
|
end
|
50
61
|
|
51
62
|
def clean?
|
@@ -58,6 +69,15 @@ module ChefDK
|
|
58
69
|
|
59
70
|
def synchronized_remotes
|
60
71
|
@synchronized_remotes ||= git!("branch -r --contains #{revision}").stdout.lines.map(&:strip)
|
72
|
+
rescue Mixlib::ShellOut::ShellCommandFailed => e
|
73
|
+
# We may have an "unborn" branch, i.e. one with no commits.
|
74
|
+
if unborn_branch_ref
|
75
|
+
[]
|
76
|
+
else
|
77
|
+
# if we got here, but verify_ref_cmd didn't error, we don't know why
|
78
|
+
# the original git command failed, so re-raise.
|
79
|
+
raise e
|
80
|
+
end
|
61
81
|
end
|
62
82
|
|
63
83
|
def remote
|
@@ -78,18 +98,54 @@ module ChefDK
|
|
78
98
|
end
|
79
99
|
|
80
100
|
def current_branch
|
81
|
-
@current_branch ||=
|
101
|
+
@current_branch ||= detect_current_branch
|
82
102
|
end
|
83
103
|
|
84
104
|
private
|
85
105
|
|
86
106
|
def git!(subcommand, options={})
|
87
|
-
|
88
|
-
cmd = system_command("git #{subcommand}", options)
|
107
|
+
cmd = git(subcommand, options)
|
89
108
|
cmd.error!
|
90
109
|
cmd
|
91
110
|
end
|
92
111
|
|
112
|
+
def git(subcommand, options={})
|
113
|
+
options = { cwd: cookbook_path }.merge(options)
|
114
|
+
system_command("git #{subcommand}", options)
|
115
|
+
end
|
116
|
+
|
117
|
+
def detect_current_branch
|
118
|
+
branch = git!('rev-parse --abbrev-ref HEAD').stdout.strip
|
119
|
+
@unborn_branch = false
|
120
|
+
branch
|
121
|
+
rescue Mixlib::ShellOut::ShellCommandFailed => e
|
122
|
+
# We may have an "unborn" branch, i.e. one with no commits.
|
123
|
+
if unborn_branch_ref
|
124
|
+
unborn_branch_ref
|
125
|
+
else
|
126
|
+
# if we got here, but verify_ref_cmd didn't error, we don't know why
|
127
|
+
# the original git command failed, so re-raise.
|
128
|
+
raise e
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def unborn_branch_ref
|
133
|
+
@unborn_branch_ref ||=
|
134
|
+
begin
|
135
|
+
strict_branch_ref = git!("symbolic-ref -q HEAD").stdout.strip
|
136
|
+
verify_ref_cmd = git("show-ref --verify #{strict_branch_ref}")
|
137
|
+
if verify_ref_cmd.error?
|
138
|
+
@unborn_branch = true
|
139
|
+
strict_branch_ref
|
140
|
+
else
|
141
|
+
# if we got here, but verify_ref_cmd didn't error, then `git
|
142
|
+
# rev-parse` is probably failing for a reason we haven't anticipated.
|
143
|
+
# Calling code should detect this and re-raise.
|
144
|
+
nil
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
93
149
|
end
|
94
150
|
end
|
95
151
|
end
|