chef-dk 0.5.1 → 0.6.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 +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
|