chef-zero 4.3.2 → 4.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ac78b39c39c21f9ec236e084f3cced3e9fede287
4
- data.tar.gz: 1c0e8b64a786743b5be5911d96fe02bfdb1efb59
3
+ metadata.gz: ef05ae4a7aef9de3c3a3da1ec5fbd5143a1a0457
4
+ data.tar.gz: 01e350ee9b6ff42d42550e8bafa07e1de7d1d629
5
5
  SHA512:
6
- metadata.gz: e8957ed6a69e8e3f1f28a5fcbafc114f1013d4e2e35f83ba71cc4050780da5f829a69de6dac9c93a77b4594e3c48019f86e524b0de5555b0fbe1f7700748279f
7
- data.tar.gz: 295f5763b15e6167a106beffbf175436d2d89975a3a90b60e7a714ff3176b1d3a8c2d65bed01f758b2412e574e547fb4678d76988abf34cfd51a1c15572149e3
6
+ metadata.gz: ac2bff1a5f4518d05262d3660ad96709ec60f495706bd0700d4084cdc1a6e14cca17a458a69067bf5da9a14024c7222a17e0bb8de2a582205d9ebe0acbd560e6
7
+ data.tar.gz: 727e1693591ff8b7e7c03c3fd9b0e57868429fe9c1286b8b59cb65850d54b8d90cf54e6bc2a9c4353c1f1cff5081e7853400be21ef50ac1cd98b0bc2ff63a1cb
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ gem 'rest-client', :github => 'chef/rest-client'
5
+
6
+ gem 'oc-chef-pedant', :github => 'chef/chef-server'
7
+
8
+ # gem 'oc-chef-pedant', :path => "../chef-server"
9
+
10
+ # bundler resolve failure on "rspec_junit_formatter"
11
+ # gem 'chef-pedant', :github => 'opscode/chef-pedant', :ref => "server-cli-option"
12
+
13
+ gem 'chef', :github => 'chef/chef'
14
+ # gem 'chef', :path => "../chef"
data/Rakefile CHANGED
@@ -15,6 +15,18 @@ task :pedant do
15
15
  require File.expand_path('spec/run_oc_pedant')
16
16
  end
17
17
 
18
+ desc "run pedant with CHEF_FS set"
19
+ task :cheffs do
20
+ ENV['CHEF_FS'] = "yes"
21
+ require File.expand_path('spec/run_oc_pedant')
22
+ end
23
+
24
+ desc "run pedant with FILE_STORE set"
25
+ task :filestore do
26
+ ENV['FILE_STORE'] = "yes"
27
+ require File.expand_path('spec/run_oc_pedant')
28
+ end
29
+
18
30
  desc "run oc pedant"
19
31
  task :oc_pedant do
20
32
  require File.expand_path('spec/run_oc_pedant')
data/bin/chef-zero CHANGED
@@ -55,6 +55,11 @@ OptionParser.new do |opts|
55
55
  options[:log_file] = value
56
56
  end
57
57
 
58
+ opts.on("--enterprise", "Whether to run in enterprise mode") do |value|
59
+ options[:single_org] = nil
60
+ options[:osc_compat] = false
61
+ end
62
+
58
63
  opts.on("--multi-org", "Whether to run in multi-org mode") do |value|
59
64
  options[:single_org] = nil
60
65
  end
data/chef-zero.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/lib')
2
+ require 'chef_zero/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'chef-zero'
6
+ s.version = ChefZero::VERSION
7
+ s.platform = Gem::Platform::RUBY
8
+ s.summary = 'Self-contained, easy-setup, fast-start in-memory Chef server for testing and solo setup purposes'
9
+ s.description = s.summary
10
+ s.author = 'John Keiser'
11
+ s.email = 'jkeiser@opscode.com'
12
+ s.homepage = 'http://www.opscode.com'
13
+ s.license = 'Apache 2.0'
14
+
15
+ s.add_dependency 'mixlib-log', '~> 1.3'
16
+ s.add_dependency 'hashie', '>= 2.0', '< 4.0'
17
+ s.add_dependency 'uuidtools', '~> 2.1'
18
+ s.add_dependency 'ffi-yajl', '~> 2.2'
19
+ s.add_dependency 'rack'
20
+
21
+ s.add_development_dependency 'pry'
22
+ s.add_development_dependency 'pry-byebug'
23
+ s.add_development_dependency 'pry-stack_explorer'
24
+ s.add_development_dependency 'rake'
25
+ s.add_development_dependency 'rspec'
26
+ s.add_development_dependency 'github_changelog_generator'
27
+
28
+ s.bindir = 'bin'
29
+ s.executables = ['chef-zero']
30
+ s.require_path = 'lib'
31
+ s.files = %w(LICENSE README.md Gemfile Rakefile) + Dir.glob('*.gemspec') +
32
+ Dir.glob('{lib,spec}/**/*', File::FNM_DOTMATCH).reject {|f| File.directory?(f) }
33
+ end
@@ -79,7 +79,8 @@ module ChefZero
79
79
  data_bag_item
80
80
  end
81
81
 
82
- def self.normalize_cookbook(endpoint, org_prefix, cookbook, name, version, base_uri, method)
82
+ def self.normalize_cookbook(endpoint, org_prefix, cookbook, name, version, base_uri, method,
83
+ is_cookbook_artifact=false)
83
84
  # TODO I feel dirty
84
85
  if method != 'PUT'
85
86
  cookbook.each_pair do |key, value|
@@ -92,24 +93,28 @@ module ChefZero
92
93
  end
93
94
  end
94
95
  cookbook['name'] ||= "#{name}-#{version}"
95
- # TODO this feels wrong, but the real chef server doesn't expand this default
96
- # cookbook['version'] ||= version
97
- cookbook['cookbook_name'] ||= name
96
+ # TODO it feels wrong, but the real chef server doesn't expand 'version', so we don't either.
97
+
98
98
  cookbook['frozen?'] ||= false
99
99
  cookbook['metadata'] ||= {}
100
100
  cookbook['metadata']['version'] ||= version
101
- # Sad to not be expanding defaults just because Chef doesn't :(
102
- # cookbook['metadata']['name'] ||= name
103
- # cookbook['metadata']['description'] ||= "A fabulous new cookbook"
101
+
102
+ # defaults set by the client and not the Server:
103
+ # metadata[name, description, maintainer, maintainer_email, license]
104
+
104
105
  cookbook['metadata']['long_description'] ||= ""
105
- # cookbook['metadata']['maintainer'] ||= "YOUR_COMPANY_NAME"
106
- # cookbook['metadata']['maintainer_email'] ||= "YOUR_EMAIL"
107
- # cookbook['metadata']['license'] ||= "none"
108
106
  cookbook['metadata']['dependencies'] ||= {}
109
107
  cookbook['metadata']['attributes'] ||= {}
110
108
  cookbook['metadata']['recipes'] ||= {}
111
109
  end
112
- cookbook['json_class'] ||= 'Chef::CookbookVersion'
110
+
111
+ if is_cookbook_artifact
112
+ cookbook.delete('json_class')
113
+ else
114
+ cookbook['cookbook_name'] ||= name
115
+ cookbook['json_class'] ||= 'Chef::CookbookVersion'
116
+ end
117
+
113
118
  cookbook['chef_type'] ||= 'cookbook_version'
114
119
  if method == 'MIN'
115
120
  cookbook['metadata'].delete('attributes')
@@ -166,6 +171,20 @@ module ChefZero
166
171
  node
167
172
  end
168
173
 
174
+ def self.normalize_policy(policy, name, revision)
175
+ policy['name'] ||= name
176
+ policy['revision_id'] ||= revision
177
+ policy['run_list'] ||= []
178
+ policy['cookbook_locks'] ||= {}
179
+ policy
180
+ end
181
+
182
+ def self.normalize_policy_group(policy_group, name)
183
+ policy_group[name] ||= 'name'
184
+ policy_group['policies'] ||= {}
185
+ policy_group
186
+ end
187
+
169
188
  def self.normalize_organization(org, name)
170
189
  org['name'] ||= name
171
190
  org['full_name'] ||= name
@@ -155,6 +155,8 @@ module ChefZero
155
155
  'checksums' => {}
156
156
  },
157
157
  'nodes' => {},
158
+ 'policies' => {},
159
+ 'policy_groups' => {},
158
160
  'roles' => {},
159
161
  'sandboxes' => {},
160
162
  'users' => {},
@@ -378,11 +380,12 @@ module ChefZero
378
380
  # Non-default containers do not get superusers added to them,
379
381
  # because reasons.
380
382
  unless path.size == 4 && path[0] == 'organizations' && path[2] == 'containers' && !exists?(path)
381
- owners |= superusers
383
+ owners += superusers
382
384
  end
383
385
  end
384
386
 
385
- owners.uniq
387
+ # we don't de-dup this list, because pedant expects to see ["pivotal", "pivotal"] in some cases.
388
+ owners
386
389
  end
387
390
 
388
391
  def default_acl(acl_path, acl={})
@@ -0,0 +1,24 @@
1
+ require 'chef_zero/chef_data/data_normalizer'
2
+
3
+ module ChefZero
4
+ module Endpoints
5
+ class CookbookArtifactsCookbookEndpoint < RestBase
6
+ # GET /organizations/ORG/cookbook_artifacts/COOKBOOK
7
+ def get(request)
8
+ cookbook_name = request.rest_path.last
9
+ cookbook_url = build_uri(request.base_uri, request.rest_path)
10
+ response_data = {}
11
+ versions = []
12
+
13
+ list_data(request).each do |identifier|
14
+ artifact_url = build_uri(request.base_uri, request.rest_path + [cookbook_name, identifier])
15
+ versions << { url: artifact_url, identifier: identifier }
16
+ end
17
+
18
+ response_data[cookbook_name] = { url: cookbook_url, versions: versions }
19
+
20
+ return json_response(200, response_data)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,67 @@
1
+ require 'chef_zero/chef_data/data_normalizer'
2
+
3
+ module ChefZero
4
+ module Endpoints
5
+ class CookbookArtifactsCookbookIdentifierEndpoint < ChefZero::Endpoints::CookbookVersionEndpoint
6
+ # these endpoints are almost, but not quite, not entirely unlike the corresponding /cookbooks endpoints.
7
+ # it could all be refactored for maximum reuse, but they're short REST methods with well-defined
8
+ # behavioral specs (pedant), so there's not a huge benefit.
9
+
10
+ # GET /organizations/ORG/cookbook_artifacts/NAME/IDENTIFIER
11
+ def get(request)
12
+ cookbook_data = normalize(request, parse_json(get_data(request)))
13
+ return json_response(200, cookbook_data)
14
+ end
15
+
16
+ # PUT /organizations/ORG/cookbook_artifacts/COOKBOOK/IDENTIFIER
17
+ def put(request)
18
+ if exists_data?(request)
19
+ return error(409, "Cookbooks cannot be modified, and a cookbook with this identifier already exists.")
20
+ end
21
+
22
+ set_data(request, nil, request.body, :create_dir)
23
+
24
+ return already_json_response(201, request.body)
25
+ end
26
+
27
+ # DELETE /organizations/ORG/cookbook_artifacts/COOKBOOK/IDENTIFIER
28
+ def delete(request)
29
+ begin
30
+ doomed_cookbook_json = get_data(request)
31
+ identified_cookbook_data = normalize(request, parse_json(doomed_cookbook_json))
32
+ delete_data(request)
33
+
34
+ # go through the recipes and delete stuff in the file store.
35
+ hoover_unused_checksums(get_checksums(doomed_cookbook_json), request, 'cookbook_artifacts')
36
+
37
+ # if this was the last revision, delete the directory so future requests will 404, instead of
38
+ # returning 200 with an empty list.
39
+ artifact_path = request.rest_path[0..-2]
40
+ if list_data(request, artifact_path).size == 0
41
+ delete_data_dir(request, artifact_path)
42
+ end
43
+
44
+ json_response(200, identified_cookbook_data)
45
+ rescue RestErrorResponse => ex
46
+ if ex.response_code == 404
47
+ error(404, "not_found")
48
+ else
49
+ raise
50
+ end
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def make_file_store_path(rest_path, recipe)
57
+ rest_path.first(2) + ["file_store", "checksums", recipe["checksum"]]
58
+ end
59
+
60
+ def normalize(request, cookbook_artifact_data)
61
+ ChefData::DataNormalizer.normalize_cookbook(self, request.rest_path[0..1],
62
+ cookbook_artifact_data, request.rest_path[3], request.rest_path[4],
63
+ request.base_uri, request.method, true)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,34 @@
1
+ require 'chef_zero/chef_data/data_normalizer'
2
+
3
+ module ChefZero
4
+ module Endpoints
5
+ class CookbookArtifactsEndpoint < RestBase
6
+ # GET /organizations/ORG/cookbook_artifacts
7
+ def get(request)
8
+ data = {}
9
+
10
+ artifacts = begin
11
+ list_data(request)
12
+ rescue Exception => e
13
+ if e.response_code == 404
14
+ return already_json_response(200, "{}")
15
+ end
16
+ end
17
+
18
+ artifacts.each do |cookbook_artifact|
19
+ cookbook_url = build_uri(request.base_uri, request.rest_path + [cookbook_artifact])
20
+
21
+ versions = []
22
+ list_data(request, request.rest_path + [cookbook_artifact]).each do |identifier|
23
+ artifact_url = build_uri(request.base_uri, request.rest_path + [cookbook_artifact, identifier])
24
+ versions << { url: artifact_url, identifier: identifier }
25
+ end
26
+
27
+ data[cookbook_artifact] = { url: cookbook_url, versions: versions }
28
+ end
29
+
30
+ return json_response(200, data)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -85,10 +85,10 @@ module ChefZero
85
85
 
86
86
  private
87
87
 
88
- def hoover_unused_checksums(deleted_checksums, request)
89
- data_store.list(request.rest_path[0..1] + ['cookbooks']).each do |cookbook_name|
90
- data_store.list(request.rest_path[0..1] + ['cookbooks', cookbook_name]).each do |version|
91
- cookbook = data_store.get(request.rest_path[0..1] + ['cookbooks', cookbook_name, version], request)
88
+ def hoover_unused_checksums(deleted_checksums, request, data_type='cookbooks')
89
+ data_store.list(request.rest_path[0..1] + [data_type]).each do |cookbook_name|
90
+ data_store.list(request.rest_path[0..1] + [data_type, cookbook_name]).each do |version|
91
+ cookbook = data_store.get(request.rest_path[0..1] + [data_type, cookbook_name, version], request)
92
92
  deleted_checksums = deleted_checksums - get_checksums(cookbook)
93
93
  end
94
94
  end
@@ -0,0 +1,31 @@
1
+
2
+ # pedant makes a couple of Solr-related calls from its search_utils.rb file that we can't work around (e.g.
3
+ # with monkeypatching). the necessary Pedant::Config values are set in run_oc_pedant.rb. --cdoherty
4
+ module ChefZero
5
+ module Endpoints
6
+ class DummyEndpoint < RestBase
7
+ # called by #direct_solr_query, once each for roles, nodes, and data bag items. each RSpec example makes
8
+ # 3 calls, with the expected sequence of return values [0, 1, 0].
9
+ def get(request)
10
+
11
+ # this could be made less brittle, but if things change to have more than 3 cycles, we should really
12
+ # be notified by a spec failure.
13
+ @mock_values ||= ([0, 1, 0] * 3).map { |val| make_response(val) }
14
+
15
+ retval = @mock_values.shift
16
+ json_response(200, retval)
17
+ end
18
+
19
+ # called by #force_solr_commit in pedant's , which doesn't check the return value.
20
+ def post(request)
21
+ # sure thing!
22
+ json_response(200, { message: "This dummy POST endpoint didn't do anything." })
23
+ end
24
+
25
+ def make_response(value)
26
+ { "response" => { "numFound" => value } }
27
+ end
28
+ end
29
+ end
30
+ end
31
+
@@ -6,6 +6,20 @@ module ChefZero
6
6
  module Endpoints
7
7
  # /nodes/ID
8
8
  class NodeEndpoint < RestObjectEndpoint
9
+ def put(request)
10
+ data = parse_json(request.body)
11
+
12
+ if data.has_key?("policy_name") && policy_name_invalid?(data["policy_name"])
13
+ return error(400, "Field 'policy_name' invalid", :pretty => false)
14
+ end
15
+
16
+ if data.has_key?("policy_group") && policy_name_invalid?(data["policy_group"])
17
+ return error(400, "Field 'policy_group' invalid", :pretty => false)
18
+ end
19
+
20
+ super(request)
21
+ end
22
+
9
23
  def populate_defaults(request, response_json)
10
24
  node = FFI_Yajl::Parser.parse(response_json, :create_additions => false)
11
25
  node = ChefData::DataNormalizer.normalize_node(node, request.rest_path[3])
@@ -0,0 +1,35 @@
1
+ require 'ffi_yajl'
2
+ require 'chef_zero/endpoints/rest_object_endpoint'
3
+ require 'chef_zero/chef_data/data_normalizer'
4
+
5
+ module ChefZero
6
+ module Endpoints
7
+ # /nodes
8
+ class NodesEndpoint < RestListEndpoint
9
+
10
+ def post(request)
11
+ # /nodes validation
12
+ if request.rest_path.last == "nodes"
13
+ data = parse_json(request.body)
14
+
15
+ if data.has_key?("policy_name") && policy_name_invalid?(data["policy_name"])
16
+ return error(400, "Field 'policy_name' invalid", :pretty => false)
17
+ end
18
+
19
+ if data.has_key?("policy_group") && policy_name_invalid?(data["policy_group"])
20
+ return error(400, "Field 'policy_group' invalid", :pretty => false)
21
+ end
22
+ end
23
+
24
+ super(request)
25
+ end
26
+
27
+ def populate_defaults(request, response_json)
28
+ node = FFI_Yajl::Parser.parse(response_json, :create_additions => false)
29
+ node = ChefData::DataNormalizer.normalize_node(node, request.rest_path[3])
30
+ FFI_Yajl::Encoder.encode(node, :pretty => true)
31
+ end
32
+ end
33
+ end
34
+ end
35
+
@@ -1,151 +1,26 @@
1
- require 'ffi_yajl'
2
-
3
- require 'chef_zero/endpoints/rest_object_endpoint'
4
1
  require 'chef_zero/chef_data/data_normalizer'
5
2
 
6
3
  module ChefZero
7
4
  module Endpoints
8
- # /policies/:group/:name
9
- class PoliciesEndpoint < RestObjectEndpoint
10
- def initialize(server)
11
- super(server, 'id')
12
- end
13
-
5
+ # /organizations/ORG/policies
6
+ class PoliciesEndpoint < RestBase
7
+ # GET /organizations/ORG/policies
14
8
  def get(request)
15
- already_json_response(200, get_data(request))
16
- end
17
-
18
- # Right now we're allowing PUT to create.
19
- def put(request)
20
- error = validate(request)
21
- return error if error
22
-
23
- code =
24
- if data_store.exists?(request.rest_path)
25
- set_data(request, request.rest_path, request.body, :data_store_exceptions)
26
- 200
27
- else
28
- name = request.rest_path[4]
29
- data_store.create(request.rest_path[0..3], name, request.body, :create_dir)
30
- 201
31
- end
32
- already_json_response(code, request.body)
33
- end
34
-
35
- def delete(request)
36
- result = get_data(request, request.rest_path)
37
- delete_data(request, request.rest_path, :data_store_exceptions)
38
- already_json_response(200, result)
39
- end
40
-
41
- private
42
-
43
- def validate(request)
44
- req_object = validate_json(request.body)
45
- validate_revision_id(request, req_object) ||
46
- validate_name(request, req_object) ||
47
- validate_run_list(req_object) ||
48
- validate_each_run_list_item(req_object) ||
49
- validate_cookbook_locks_collection(req_object) ||
50
- validate_each_cookbook_locks_item(req_object)
51
- end
52
-
53
- def validate_json(request_body)
54
- FFI_Yajl::Parser.parse(request_body)
55
- # TODO: rescue parse error, return 400
56
- # error(400, "Must specify #{identity_keys.map { |k| k.inspect }.join(' or ')} in JSON")
57
- end
58
-
59
- def validate_revision_id(request, req_object)
60
- if !req_object.key?("revision_id")
61
- error(400, "Field 'revision_id' missing")
62
- elsif req_object["revision_id"].empty?
63
- error(400, "Field 'revision_id' invalid")
64
- elsif req_object["revision_id"].size > 255
65
- error(400, "Field 'revision_id' invalid")
66
- elsif req_object["revision_id"] !~ /^[\-[:alnum:]_\.\:]+$/
67
- error(400, "Field 'revision_id' invalid")
68
- end
69
- end
70
-
71
- def validate_name(request, req_object)
72
- if !req_object.key?("name")
73
- error(400, "Field 'name' missing")
74
- elsif req_object["name"] != (uri_policy_name = URI.decode(request.rest_path[4]))
75
- error(400, "Field 'name' invalid : #{uri_policy_name} does not match #{req_object["name"]}")
76
- elsif req_object["name"].size > 255
77
- error(400, "Field 'name' invalid")
78
- elsif req_object["name"] !~ /^[\-[:alnum:]_\.\:]+$/
79
- error(400, "Field 'name' invalid")
80
- end
81
- end
82
-
83
- def validate_run_list(req_object)
84
- if !req_object.key?("run_list")
85
- error(400, "Field 'run_list' missing")
86
- elsif !req_object["run_list"].kind_of?(Array)
87
- error(400, "Field 'run_list' is not a valid run list")
88
- end
89
- end
90
-
91
- def validate_each_run_list_item(req_object)
92
- req_object["run_list"].each do |run_list_item|
93
- if res_400 = validate_run_list_item(run_list_item)
94
- return res_400
95
- end
96
- end
97
- nil
98
- end
9
+ response_data = {}
10
+ policy_names = list_data(request)
11
+ policy_names.each do |policy_name|
12
+ policy_path = request.rest_path + [policy_name]
13
+ policy_uri = build_uri(request.base_uri, policy_path)
14
+ revisions = list_data(request, policy_path + ["revisions"])
99
15
 
100
- def validate_run_list_item(run_list_item)
101
- if !run_list_item.kind_of?(String)
102
- error(400, "Field 'run_list' is not a valid run list")
103
- elsif run_list_item !~ /\Arecipe\[[^\s]+::[^\s]+\]\Z/
104
- error(400, "Field 'run_list' is not a valid run list")
16
+ response_data[policy_name] = {
17
+ uri: policy_uri,
18
+ revisions: hashify_list(revisions)
19
+ }
105
20
  end
106
- end
107
21
 
108
- def validate_cookbook_locks_collection(req_object)
109
- if !req_object.key?("cookbook_locks")
110
- error(400, "Field 'cookbook_locks' missing")
111
- elsif !req_object["cookbook_locks"].kind_of?(Hash)
112
- error(400, "Field 'cookbook_locks' invalid")
113
- end
22
+ return json_response(200, response_data)
114
23
  end
115
-
116
- def validate_each_cookbook_locks_item(req_object)
117
- req_object["cookbook_locks"].each do |cookbook_name, lock|
118
- if res_400 = validate_cookbook_locks_item(cookbook_name, lock)
119
- return res_400
120
- end
121
- end
122
- nil
123
- end
124
-
125
- def validate_cookbook_locks_item(cookbook_name, lock)
126
- if !lock.kind_of?(Hash)
127
- error(400, "cookbook_lock entries must be a JSON object")
128
- elsif !lock.key?("identifier")
129
- error(400, "Field 'identifier' missing")
130
- elsif lock["identifier"].size > 255
131
- error(400, "Field 'identifier' invalid")
132
- elsif !lock.key?("version")
133
- error(400, "Field 'version' missing")
134
- elsif lock.key?("dotted_decimal_identifier")
135
- unless valid_version?(lock["dotted_decimal_identifier"])
136
- error(400, "Field 'dotted_decimal_identifier' is not a valid version")
137
- end
138
- end
139
- end
140
-
141
- def valid_version?(version_string)
142
- Gem::Version.new(version_string)
143
- true
144
- rescue ArgumentError
145
- false
146
- end
147
-
148
24
  end
149
25
  end
150
26
  end
151
-