chef-dk 0.10.0 → 0.11.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/Gemfile +5 -0
- data/README.md +19 -4
- data/Rakefile +9 -0
- data/chef-dk.gemspec +3 -1
- data/lib/chef-dk/chef_runner.rb +9 -0
- data/lib/chef-dk/command/export.rb +6 -0
- data/lib/chef-dk/command/generator_commands.rb +3 -3
- data/lib/chef-dk/command/generator_commands/base.rb +27 -0
- data/lib/chef-dk/command/update.rb +19 -0
- data/lib/chef-dk/configurable.rb +13 -1
- data/lib/chef-dk/exceptions.rb +3 -0
- data/lib/chef-dk/policyfile/cookbook_location_specification.rb +13 -0
- data/lib/chef-dk/policyfile/cookbook_locks.rb +1 -1
- data/lib/chef-dk/policyfile/dsl.rb +40 -2
- data/lib/chef-dk/policyfile_compiler.rb +43 -4
- data/lib/chef-dk/policyfile_services/export_repo.rb +156 -51
- data/lib/chef-dk/policyfile_services/install.rb +1 -0
- data/lib/chef-dk/policyfile_services/push_archive.rb +33 -2
- data/lib/chef-dk/skeletons/code_generator/files/default/chefignore +7 -5
- data/lib/chef-dk/skeletons/code_generator/files/default/repo/policies/README.md +1 -1
- data/lib/chef-dk/version.rb +1 -1
- data/lib/kitchen/provisioner/policyfile_zero.rb +8 -3
- data/spec/shared/custom_generator_cookbook.rb +15 -2
- data/spec/unit/chef_runner_spec.rb +28 -0
- data/spec/unit/command/export_spec.rb +11 -0
- data/spec/unit/command/generator_commands/base_spec.rb +136 -0
- data/spec/unit/command/update_spec.rb +24 -0
- data/spec/unit/configurable_spec.rb +41 -0
- data/spec/unit/fixtures/configurable/test_config_loader.rb +5 -0
- data/spec/unit/fixtures/configurable/test_configurable.rb +10 -0
- data/spec/unit/policyfile/cookbook_location_specification_spec.rb +21 -1
- data/spec/unit/policyfile/cookbook_locks_spec.rb +1 -1
- data/spec/unit/policyfile_demands_spec.rb +206 -0
- data/spec/unit/policyfile_evaluation_spec.rb +85 -0
- data/spec/unit/policyfile_lock_serialization_spec.rb +1 -1
- data/spec/unit/policyfile_services/export_repo_spec.rb +78 -36
- data/spec/unit/policyfile_services/install_spec.rb +20 -0
- data/spec/unit/policyfile_services/push_archive_spec.rb +41 -8
- metadata +27 -11
@@ -21,6 +21,8 @@ require 'zlib'
|
|
21
21
|
|
22
22
|
require 'archive/tar/minitar'
|
23
23
|
|
24
|
+
require 'chef/cookbook/chefignore'
|
25
|
+
|
24
26
|
require 'chef-dk/service_exceptions'
|
25
27
|
require 'chef-dk/policyfile_lock'
|
26
28
|
require 'chef-dk/policyfile/storage_config'
|
@@ -95,9 +97,11 @@ module ChefDK
|
|
95
97
|
with_staging_dir do
|
96
98
|
create_repo_structure
|
97
99
|
copy_cookbooks
|
98
|
-
|
100
|
+
create_policyfile_repo_item
|
101
|
+
create_policy_group_repo_item
|
99
102
|
copy_policyfile_lock
|
100
103
|
create_client_rb
|
104
|
+
create_readme_md
|
101
105
|
if archive?
|
102
106
|
create_archive
|
103
107
|
else
|
@@ -138,8 +142,10 @@ module ChefDK
|
|
138
142
|
|
139
143
|
def create_repo_structure
|
140
144
|
FileUtils.mkdir_p(export_dir)
|
141
|
-
FileUtils.mkdir_p(
|
142
|
-
FileUtils.mkdir_p(
|
145
|
+
FileUtils.mkdir_p(dot_chef_staging_dir)
|
146
|
+
FileUtils.mkdir_p(cookbook_artifacts_staging_dir)
|
147
|
+
FileUtils.mkdir_p(policies_staging_dir)
|
148
|
+
FileUtils.mkdir_p(policy_groups_staging_dir)
|
143
149
|
end
|
144
150
|
|
145
151
|
def copy_cookbooks
|
@@ -149,13 +155,13 @@ module ChefDK
|
|
149
155
|
end
|
150
156
|
|
151
157
|
def copy_cookbook(lock)
|
152
|
-
dirname = "#{lock.name}-#{lock.
|
153
|
-
export_path = File.join(staging_dir, "
|
158
|
+
dirname = "#{lock.name}-#{lock.identifier}"
|
159
|
+
export_path = File.join(staging_dir, "cookbook_artifacts", dirname)
|
154
160
|
metadata_rb_path = File.join(export_path, "metadata.rb")
|
155
|
-
FileUtils.
|
161
|
+
FileUtils.mkdir(export_path) if not File.directory?(export_path)
|
162
|
+
copy_unignored_cookbook_files(lock, export_path)
|
156
163
|
FileUtils.rm_f(metadata_rb_path)
|
157
164
|
metadata = lock.cookbook_version.metadata
|
158
|
-
metadata.version(lock.dotted_decimal_identifier)
|
159
165
|
|
160
166
|
metadata_json_path = File.join(export_path, "metadata.json")
|
161
167
|
|
@@ -164,23 +170,47 @@ module ChefDK
|
|
164
170
|
end
|
165
171
|
end
|
166
172
|
|
167
|
-
def
|
168
|
-
|
173
|
+
def copy_unignored_cookbook_files(lock, export_path)
|
174
|
+
cookbook_files_to_copy(lock.cookbook_path).each do |rel_path|
|
175
|
+
full_source_path = File.join(lock.cookbook_path, rel_path)
|
176
|
+
full_dest_path = File.join(export_path, rel_path)
|
177
|
+
dest_dirname = File.dirname(full_dest_path)
|
178
|
+
FileUtils.mkdir_p(dest_dirname) unless File.directory?(dest_dirname)
|
179
|
+
FileUtils.cp(full_source_path, full_dest_path)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def cookbook_files_to_copy(cookbook_path)
|
184
|
+
cookbook_loader_for(cookbook_path).cookbook_version.manifest_records_by_path.keys
|
185
|
+
end
|
169
186
|
|
170
|
-
|
187
|
+
def cookbook_loader_for(cookbook_path)
|
188
|
+
loader = Chef::Cookbook::CookbookVersionLoader.new(cookbook_path, chefignore_for(cookbook_path))
|
189
|
+
loader.load!
|
190
|
+
loader
|
191
|
+
end
|
171
192
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
193
|
+
def chefignore_for(cookbook_path)
|
194
|
+
Chef::Cookbook::Chefignore.new(File.join(cookbook_path, "chefignore"))
|
195
|
+
end
|
196
|
+
|
197
|
+
def create_policyfile_repo_item
|
198
|
+
File.open(policyfile_repo_item_path, "wb+") do |f|
|
199
|
+
f.print(FFI_Yajl::Encoder.encode(policyfile_lock.to_lock, pretty: true ))
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def create_policy_group_repo_item
|
204
|
+
data = {
|
205
|
+
"policies" => {
|
206
|
+
policyfile_lock.name => {
|
207
|
+
"revision_id" => policyfile_lock.revision_id
|
208
|
+
}
|
209
|
+
}
|
180
210
|
}
|
181
211
|
|
182
|
-
File.open(
|
183
|
-
f.print(FFI_Yajl::Encoder.encode(
|
212
|
+
File.open(policy_group_repo_item_path, "wb+") do |f|
|
213
|
+
f.print(FFI_Yajl::Encoder.encode(data, pretty: true ))
|
184
214
|
end
|
185
215
|
end
|
186
216
|
|
@@ -197,32 +227,89 @@ module ChefDK
|
|
197
227
|
# The settings in this file will configure chef to apply the exported policy in
|
198
228
|
# this directory. To use it, run:
|
199
229
|
#
|
200
|
-
# chef-client -
|
230
|
+
# chef-client -z
|
201
231
|
#
|
202
232
|
|
203
|
-
|
233
|
+
policy_name '#{policy_name}'
|
234
|
+
policy_group 'local'
|
204
235
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
236
|
+
use_policyfile true
|
237
|
+
policy_document_native_api true
|
238
|
+
|
239
|
+
# In order to use this repo, you need a version of Chef Client and Chef Zero
|
240
|
+
# that supports policyfile "native mode" APIs:
|
241
|
+
current_version = Gem::Version.new(Chef::VERSION)
|
242
|
+
unless Gem::Requirement.new(">= 12.7").satisfied_by?(current_version)
|
243
|
+
puts("!" * 80)
|
244
|
+
puts(<<-MESSAGE)
|
245
|
+
This Chef Repo requires features introduced in Chef 12.7, but you are using
|
246
|
+
Chef \#{Chef::VERSION}. Please upgrade to Chef 12.7 or later.
|
247
|
+
MESSAGE
|
248
|
+
puts("!" * 80)
|
249
|
+
exit!(1)
|
250
|
+
end
|
210
251
|
|
211
252
|
CONFIG
|
212
253
|
end
|
213
254
|
end
|
214
255
|
|
256
|
+
def create_readme_md
|
257
|
+
File.open(readme_staging_path, "wb+") do |f|
|
258
|
+
f.print( <<-README )
|
259
|
+
# Exported Chef Repository for Policy '#{policy_name}'
|
260
|
+
|
261
|
+
Policy revision: #{policyfile_lock.revision_id}
|
262
|
+
|
263
|
+
This directory contains all the cookbooks and configuration necessary for Chef
|
264
|
+
to converge a system using this exported policy. To converge a system with the
|
265
|
+
exported policy, use a privileged account to run `chef-client -z` from the
|
266
|
+
directory containing the exported policy.
|
267
|
+
|
268
|
+
## Contents:
|
269
|
+
|
270
|
+
### Policyfile.lock.json
|
271
|
+
|
272
|
+
A copy of the exported policy, used by the `chef push-archive` command.
|
273
|
+
|
274
|
+
### .chef/config.rb
|
275
|
+
|
276
|
+
A configuration file for Chef Client. This file configures Chef Client to use
|
277
|
+
the correct `policy_name` and `policy_group` for this exported repository. Chef
|
278
|
+
Client will use this configuration automatically if you've set your working
|
279
|
+
directory properly.
|
280
|
+
|
281
|
+
### cookbook_artifacts/
|
282
|
+
|
283
|
+
All of the cookbooks required by the policy will be stored in this directory.
|
284
|
+
|
285
|
+
### policies/
|
286
|
+
|
287
|
+
A different copy of the exported policy, used by the `chef-client` command.
|
288
|
+
|
289
|
+
### policy_groups/
|
290
|
+
|
291
|
+
Policy groups are used by Chef Server to manage multiple revisions of the same
|
292
|
+
policy. However, exported policies contain only a single policy revision, so
|
293
|
+
this policy group name is hardcoded to "local" and should not be changed.
|
294
|
+
|
295
|
+
README
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
215
299
|
def mv_staged_repo
|
216
300
|
# If we got here, either these dirs are empty/don't exist or force is
|
217
301
|
# set to true.
|
218
|
-
FileUtils.rm_rf(
|
219
|
-
FileUtils.rm_rf(
|
220
|
-
|
221
|
-
FileUtils.
|
222
|
-
|
223
|
-
FileUtils.mv(
|
302
|
+
FileUtils.rm_rf(cookbook_artifacts_dir)
|
303
|
+
FileUtils.rm_rf(policies_dir)
|
304
|
+
FileUtils.rm_rf(policy_groups_dir)
|
305
|
+
FileUtils.rm_rf(dot_chef_dir)
|
306
|
+
|
307
|
+
FileUtils.mv(cookbook_artifacts_staging_dir, export_dir)
|
308
|
+
FileUtils.mv(policies_staging_dir, export_dir)
|
309
|
+
FileUtils.mv(policy_groups_staging_dir, export_dir)
|
224
310
|
FileUtils.mv(lockfile_staging_path, export_dir)
|
225
|
-
FileUtils.mv(
|
311
|
+
FileUtils.mv(dot_chef_staging_dir, export_dir)
|
312
|
+
FileUtils.mv(readme_staging_path, export_dir)
|
226
313
|
end
|
227
314
|
|
228
315
|
def validate_lockfile
|
@@ -261,37 +348,51 @@ CONFIG
|
|
261
348
|
end
|
262
349
|
|
263
350
|
def conflicting_fs_entries
|
264
|
-
Dir.glob(File.join(
|
265
|
-
Dir.glob(File.join(
|
351
|
+
Dir.glob(File.join(cookbook_artifacts_dir, "*")) +
|
352
|
+
Dir.glob(File.join(policies_dir, "*")) +
|
353
|
+
Dir.glob(File.join(policy_groups_dir, "*")) +
|
266
354
|
Dir.glob(File.join(export_dir, "Policyfile.lock.json"))
|
267
355
|
end
|
268
356
|
|
269
|
-
def
|
270
|
-
File.join(export_dir, "
|
357
|
+
def cookbook_artifacts_dir
|
358
|
+
File.join(export_dir, "cookbook_artifacts")
|
359
|
+
end
|
360
|
+
|
361
|
+
def policies_dir
|
362
|
+
File.join(export_dir, "policies")
|
363
|
+
end
|
364
|
+
|
365
|
+
def policy_groups_dir
|
366
|
+
File.join(export_dir, "policy_groups")
|
367
|
+
end
|
368
|
+
|
369
|
+
def dot_chef_dir
|
370
|
+
File.join(export_dir, ".chef")
|
271
371
|
end
|
272
372
|
|
273
|
-
def
|
274
|
-
|
373
|
+
def policyfile_repo_item_path
|
374
|
+
basename = "#{policyfile_lock.name}-#{policyfile_lock.revision_id}"
|
375
|
+
File.join(staging_dir, "policies", "#{basename}.json")
|
275
376
|
end
|
276
377
|
|
277
|
-
def
|
278
|
-
File.join(
|
378
|
+
def policy_group_repo_item_path
|
379
|
+
File.join(staging_dir, "policy_groups", "local.json")
|
279
380
|
end
|
280
381
|
|
281
|
-
def
|
282
|
-
"
|
382
|
+
def dot_chef_staging_dir
|
383
|
+
File.join(staging_dir, ".chef")
|
283
384
|
end
|
284
385
|
|
285
|
-
def
|
286
|
-
File.join(staging_dir, "
|
386
|
+
def cookbook_artifacts_staging_dir
|
387
|
+
File.join(staging_dir, "cookbook_artifacts")
|
287
388
|
end
|
288
389
|
|
289
|
-
def
|
290
|
-
File.join(staging_dir, "
|
390
|
+
def policies_staging_dir
|
391
|
+
File.join(staging_dir, "policies")
|
291
392
|
end
|
292
393
|
|
293
|
-
def
|
294
|
-
File.join(staging_dir, "
|
394
|
+
def policy_groups_staging_dir
|
395
|
+
File.join(staging_dir, "policy_groups")
|
295
396
|
end
|
296
397
|
|
297
398
|
def lockfile_staging_path
|
@@ -299,7 +400,11 @@ CONFIG
|
|
299
400
|
end
|
300
401
|
|
301
402
|
def client_rb_staging_path
|
302
|
-
File.join(
|
403
|
+
File.join(dot_chef_staging_dir, "config.rb")
|
404
|
+
end
|
405
|
+
|
406
|
+
def readme_staging_path
|
407
|
+
File.join(staging_dir, "README.md")
|
303
408
|
end
|
304
409
|
|
305
410
|
end
|
@@ -104,6 +104,7 @@ module ChefDK
|
|
104
104
|
ui.msg ""
|
105
105
|
|
106
106
|
ui.msg "Lockfile written to #{policyfile_lock_expanded_path}"
|
107
|
+
ui.msg "Policy revision id: #{policyfile_lock.revision_id}"
|
107
108
|
rescue => error
|
108
109
|
raise PolicyfileInstallError.new("Failed to generate Policyfile.lock", error)
|
109
110
|
end
|
@@ -86,12 +86,22 @@ module ChefDK
|
|
86
86
|
def read_policyfile_lock(staging_dir)
|
87
87
|
policyfile_lock_path = File.join(staging_dir, "Policyfile.lock.json")
|
88
88
|
|
89
|
+
if looks_like_old_format_archive?(staging_dir)
|
90
|
+
raise InvalidPolicyArchive, <<-MESSAGE
|
91
|
+
This archive is in an unsupported format.
|
92
|
+
|
93
|
+
This archive was created with an older version of ChefDK. This version of
|
94
|
+
ChefDK does not support archives in the older format. Re-create the archive
|
95
|
+
with a newer version of ChefDK or downgrade ChefDK.
|
96
|
+
MESSAGE
|
97
|
+
end
|
98
|
+
|
89
99
|
unless File.exist?(policyfile_lock_path)
|
90
100
|
raise InvalidPolicyArchive, "Archive does not contain a Policyfile.lock.json"
|
91
101
|
end
|
92
102
|
|
93
|
-
unless File.directory?(File.join(staging_dir, "
|
94
|
-
raise InvalidPolicyArchive, "Archive does not contain a
|
103
|
+
unless File.directory?(File.join(staging_dir, "cookbook_artifacts"))
|
104
|
+
raise InvalidPolicyArchive, "Archive does not contain a cookbook_artifacts directory"
|
95
105
|
end
|
96
106
|
|
97
107
|
|
@@ -167,6 +177,27 @@ module ChefDK
|
|
167
177
|
end
|
168
178
|
end
|
169
179
|
|
180
|
+
def looks_like_old_format_archive?(staging_dir)
|
181
|
+
cookbooks_dir = File.join(staging_dir, "cookbooks")
|
182
|
+
data_bags_dir = File.join(staging_dir, "data_bags")
|
183
|
+
|
184
|
+
cookbook_artifacts_dir = File.join(staging_dir, "cookbook_artifacts")
|
185
|
+
policies_dir = File.join(staging_dir, "policies")
|
186
|
+
policy_groups_dir = File.join(staging_dir, "policy_groups")
|
187
|
+
|
188
|
+
# Old archives just had these two dirs
|
189
|
+
have_old_dirs = File.exist?(cookbooks_dir) && File.exist?(data_bags_dir)
|
190
|
+
|
191
|
+
# New archives created by `chef export` will have all of these; it's
|
192
|
+
# also possible we'll encounter an "artisanal" archive, which might
|
193
|
+
# only be missing one of these by accident. In that case we want to
|
194
|
+
# trigger a different error than we're detecting here.
|
195
|
+
have_any_new_dirs = File.exist?(cookbook_artifacts_dir) ||
|
196
|
+
File.exist?(policies_dir) ||
|
197
|
+
File.exist?(policy_groups_dir)
|
198
|
+
|
199
|
+
have_old_dirs && !have_any_new_dirs
|
200
|
+
end
|
170
201
|
|
171
202
|
end
|
172
203
|
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
# Put files/directories that should be ignored in this file when uploading
|
2
|
-
#
|
2
|
+
# to a chef-server or supermarket.
|
3
3
|
# Lines that start with '# ' are comments.
|
4
4
|
|
5
5
|
# OS generated files #
|
@@ -56,6 +56,11 @@ Guardfile
|
|
56
56
|
Procfile
|
57
57
|
.kitchen*
|
58
58
|
.rubocop.yml
|
59
|
+
spec/*
|
60
|
+
Rakefile
|
61
|
+
.travis.yml
|
62
|
+
.foodcritic
|
63
|
+
.codeclimate.yml
|
59
64
|
|
60
65
|
# SCM #
|
61
66
|
#######
|
@@ -82,6 +87,7 @@ tmp
|
|
82
87
|
CONTRIBUTING*
|
83
88
|
CHANGELOG*
|
84
89
|
TESTING*
|
90
|
+
MAINTAINERS.toml
|
85
91
|
|
86
92
|
# Strainer #
|
87
93
|
############
|
@@ -94,7 +100,3 @@ Strainerfile
|
|
94
100
|
###########
|
95
101
|
.vagrant
|
96
102
|
Vagrantfile
|
97
|
-
|
98
|
-
# Travis #
|
99
|
-
##########
|
100
|
-
.travis.yml
|
@@ -13,7 +13,7 @@ This will create a lockfile `policies/my-app-frontend.lock.json`.
|
|
13
13
|
To update locked dependencies, run `chef update` like this:
|
14
14
|
|
15
15
|
```
|
16
|
-
chef update policies/my-app-
|
16
|
+
chef update policies/my-app-frontend.rb
|
17
17
|
```
|
18
18
|
|
19
19
|
You can upload the policy (with associated cookbooks) to the server
|
data/lib/chef-dk/version.rb
CHANGED
@@ -62,6 +62,7 @@ module Kitchen
|
|
62
62
|
# Since it makes no sense to modify these, they are hardcoded elsewhere.
|
63
63
|
default_config :client_rb, {}
|
64
64
|
default_config :json_attributes, true
|
65
|
+
default_config :named_run_list, nil
|
65
66
|
default_config :chef_zero_host, nil
|
66
67
|
default_config :chef_zero_port, 8889
|
67
68
|
default_config :policyfile, "Policyfile.rb"
|
@@ -111,6 +112,10 @@ module Kitchen
|
|
111
112
|
args << "--logfile #{config[:log_file]}"
|
112
113
|
end
|
113
114
|
|
115
|
+
if config[:named_run_list]
|
116
|
+
args << "--named-run-list #{config[:named_run_list]}"
|
117
|
+
end
|
118
|
+
|
114
119
|
wrap_shell_code(
|
115
120
|
[cmd, *args].join(" ").
|
116
121
|
tap { |str| str.insert(0, reload_ps1_path) if windows_os? }
|
@@ -172,9 +177,9 @@ module Kitchen
|
|
172
177
|
|
173
178
|
data["use_policyfile"] = true
|
174
179
|
data["versioned_cookbooks"] = true
|
175
|
-
data["
|
176
|
-
|
177
|
-
data["policy_document_native_api"] =
|
180
|
+
data["policy_name"] = policy_exporter.policy_name
|
181
|
+
data["policy_group"] = "local"
|
182
|
+
data["policy_document_native_api"] = true
|
178
183
|
|
179
184
|
info("Preparing client.rb")
|
180
185
|
debug("Creating client.rb from #{data.inspect}")
|
@@ -10,6 +10,9 @@ shared_examples_for "custom generator cookbook" do
|
|
10
10
|
let(:default_generator_cookbook_path) { File.expand_path('lib/chef-dk/skeletons/code_generator', project_root) }
|
11
11
|
|
12
12
|
let(:generator_cookbook_path) { File.join(tempdir, 'a_generator_cookbook') }
|
13
|
+
let(:generator_copyright_holder) { 'Chef' }
|
14
|
+
let(:generator_email) { 'mail@chef.io' }
|
15
|
+
let(:generator_license) { 'Free as in Beer'}
|
13
16
|
|
14
17
|
let(:argv) { [generator_arg, "--generator-cookbook", generator_cookbook_path] }
|
15
18
|
|
@@ -32,7 +35,18 @@ shared_examples_for "custom generator cookbook" do
|
|
32
35
|
|
33
36
|
let(:argv) { [generator_arg] }
|
34
37
|
|
35
|
-
let(:
|
38
|
+
let(:generator_config) do
|
39
|
+
double("Generator Config Context",
|
40
|
+
license: generator_license,
|
41
|
+
copyright_holder: generator_copyright_holder,
|
42
|
+
email: generator_email)
|
43
|
+
end
|
44
|
+
|
45
|
+
let(:chefdk_config) do
|
46
|
+
double("Mixlib::Config context for ChefDK",
|
47
|
+
generator_cookbook: generator_cookbook_path,
|
48
|
+
generator: generator_config)
|
49
|
+
end
|
36
50
|
|
37
51
|
before do
|
38
52
|
allow(code_generator).to receive(:chefdk_config).and_return(chefdk_config)
|
@@ -114,4 +128,3 @@ shared_examples_for "custom generator cookbook" do
|
|
114
128
|
end
|
115
129
|
end
|
116
130
|
end
|
117
|
-
|