chef_fixie 0.3.0 → 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/bin/chef_fixie +1 -1
- data/doc/BulkFixup.md +1 -1
- data/doc/CommonTasks.md +14 -3
- data/lib/chef_fixie.rb +7 -7
- data/lib/chef_fixie/authz_mapper.rb +26 -28
- data/lib/chef_fixie/authz_objects.rb +44 -41
- data/lib/chef_fixie/bulk_edit_permissions.rb +24 -20
- data/lib/chef_fixie/check_org_associations.rb +56 -58
- data/lib/chef_fixie/config.rb +58 -24
- data/lib/chef_fixie/console.rb +15 -10
- data/lib/chef_fixie/context.rb +2 -4
- data/lib/chef_fixie/sql.rb +12 -12
- data/lib/chef_fixie/sql_objects.rb +49 -38
- data/lib/chef_fixie/utility_helpers.rb +13 -9
- data/lib/chef_fixie/version.rb +1 -1
- data/spec/chef_fixie/acl_spec.rb +23 -25
- data/spec/chef_fixie/assoc_invite_spec.rb +5 -8
- data/spec/chef_fixie/check_org_associations_spec.rb +14 -17
- data/spec/chef_fixie/groups_spec.rb +7 -11
- data/spec/chef_fixie/org_spec.rb +4 -5
- data/spec/chef_fixie/orgs_spec.rb +6 -9
- data/spec/spec_helper.rb +5 -6
- metadata +18 -84
- data/bin/bundler +0 -16
- data/bin/chef-apply +0 -16
- data/bin/chef-client +0 -16
- data/bin/chef-shell +0 -16
- data/bin/chef-solo +0 -16
- data/bin/chef-zero +0 -16
- data/bin/coderay +0 -16
- data/bin/edit_json.rb +0 -16
- data/bin/erubis +0 -16
- data/bin/ffi-yajl-bench +0 -16
- data/bin/fixie~ +0 -231
- data/bin/htmldiff +0 -16
- data/bin/knife +0 -16
- data/bin/ldiff +0 -16
- data/bin/net-dhcp +0 -16
- data/bin/ohai +0 -16
- data/bin/prettify_json.rb +0 -16
- data/bin/pry +0 -16
- data/bin/rackup +0 -16
- data/bin/rake +0 -16
- data/bin/rdoc +0 -16
- data/bin/restclient +0 -16
- data/bin/ri +0 -16
- data/bin/rspec +0 -16
- data/bin/s3sh +0 -16
- data/bin/sequel +0 -16
- data/bin/serverspec-init +0 -16
- data/doc/AccessingSQL.md~ +0 -32
- data/doc/BulkFixup.md~ +0 -28
- data/doc/CommonTasks.md~ +0 -0
- data/doc/GETTING_STARTED.md~ +0 -6
- data/spec/chef_fixie/assoc_invite_spec.rb~ +0 -26
- data/spec/chef_fixie/check_org_associations_spec.rb~ +0 -34
- data/spec/chef_fixie/org_spec.rb~ +0 -53
data/lib/chef_fixie/sql.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
#
|
2
|
-
# Copyright (c) 2014-2015 Chef Software Inc.
|
2
|
+
# Copyright (c) 2014-2015 Chef Software Inc.
|
3
3
|
# License :: Apache License, Version 2.0
|
4
4
|
#
|
5
5
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
@@ -16,20 +16,20 @@
|
|
16
16
|
#
|
17
17
|
# Author: Mark Anderson <mark@chef.io>
|
18
18
|
|
19
|
-
require
|
20
|
-
require
|
21
|
-
require
|
19
|
+
require "ffi_yajl"
|
20
|
+
require "uuidtools"
|
21
|
+
require "sequel"
|
22
22
|
|
23
|
-
require_relative
|
23
|
+
require_relative "config"
|
24
24
|
|
25
25
|
Sequel.default_timezone = :utc
|
26
26
|
|
27
27
|
module ChefFixie
|
28
28
|
module Sql
|
29
|
-
|
29
|
+
|
30
30
|
class InvalidConfig < StandardError
|
31
31
|
end
|
32
|
-
|
32
|
+
|
33
33
|
# A connection string passed to Sequel.connect()
|
34
34
|
#
|
35
35
|
# Examples:
|
@@ -46,25 +46,25 @@ module ChefFixie
|
|
46
46
|
|
47
47
|
# Returns the connection string or raises an error if you didn't set one.
|
48
48
|
def self.connection_string
|
49
|
-
@connection_string ||= ChefFixie.configure {|x| x.sql_database }
|
49
|
+
@connection_string ||= ChefFixie.configure { |x| x.sql_database }
|
50
50
|
end
|
51
|
-
|
51
|
+
|
52
52
|
# Returns a Sequel::Data baseobject, which wraps access to the database.
|
53
53
|
def self.default_connection
|
54
54
|
@database ||= Sequel.connect(connection_string, :max_connections => 2)
|
55
55
|
# @database.loggers << Logger.new($stdout)
|
56
56
|
end
|
57
|
-
|
57
|
+
|
58
58
|
# Generate a new UUID. Currently uses the v1 UUID scheme.
|
59
59
|
def new_uuid
|
60
60
|
UUIDTools::UUID.timestamp_create.hexdigest
|
61
61
|
end
|
62
|
-
|
62
|
+
|
63
63
|
# Parse the portion of the object that's stored as a blob o' JSON
|
64
64
|
def from_json(serialized_data)
|
65
65
|
FFI_Yajl::Parser.parse(serialized_data, :symbolize_keys => true)
|
66
66
|
end
|
67
|
-
|
67
|
+
|
68
68
|
# Encode the portion of the object that's stored as a blob o' JSON
|
69
69
|
def as_json(data)
|
70
70
|
FFI_Yajl::Encoder.encode(data)
|
@@ -17,12 +17,12 @@
|
|
17
17
|
# Author: Mark Anderson <mark@chef.io>
|
18
18
|
#
|
19
19
|
|
20
|
-
require
|
21
|
-
require
|
20
|
+
require "pp"
|
21
|
+
require "sequel"
|
22
22
|
|
23
|
-
require_relative
|
24
|
-
require_relative
|
25
|
-
require_relative
|
23
|
+
require_relative "config"
|
24
|
+
require_relative "authz_objects"
|
25
|
+
require_relative "authz_mapper"
|
26
26
|
|
27
27
|
Sequel.extension :inflector
|
28
28
|
|
@@ -51,25 +51,28 @@ module ChefFixie
|
|
51
51
|
else
|
52
52
|
class_or_name.class.to_s
|
53
53
|
end
|
54
|
-
name.split(
|
54
|
+
name.split("::")[-1]
|
55
55
|
end
|
56
56
|
|
57
57
|
# The class for the table, e.g. Orgs
|
58
58
|
def self.table_class(name)
|
59
|
-
name =
|
59
|
+
name = to_name(name)
|
60
60
|
(base + name.to_s.pluralize.camelize).constantize
|
61
61
|
end
|
62
|
+
|
62
63
|
# The class for one instance of the object, e.g. Org
|
63
64
|
def self.object_class(name)
|
64
|
-
name =
|
65
|
+
name = to_name(name)
|
65
66
|
(base + name.to_s.singularize.camelize).constantize
|
66
67
|
end
|
68
|
+
|
67
69
|
def self.singular(name)
|
68
|
-
name =
|
70
|
+
name = to_name(name)
|
69
71
|
name.to_s.singularize
|
70
72
|
end
|
73
|
+
|
71
74
|
def self.plural(name)
|
72
|
-
name =
|
75
|
+
name = to_name(name)
|
73
76
|
name.to_s.pluralize
|
74
77
|
end
|
75
78
|
end
|
@@ -79,9 +82,11 @@ module ChefFixie
|
|
79
82
|
def initialize(data)
|
80
83
|
@data = data
|
81
84
|
end
|
85
|
+
|
82
86
|
def data
|
83
87
|
@data
|
84
88
|
end
|
89
|
+
|
85
90
|
def table
|
86
91
|
Relationships.table_class(self).new
|
87
92
|
end
|
@@ -90,26 +95,27 @@ module ChefFixie
|
|
90
95
|
def self.ro_access(*args)
|
91
96
|
args.each do |field|
|
92
97
|
fundef = "def #{field}; @data.#{field}; end"
|
93
|
-
|
98
|
+
class_eval(fundef)
|
94
99
|
end
|
95
100
|
end
|
96
101
|
# TODO figure out model for write access
|
97
102
|
|
98
103
|
def self.name_field(field)
|
99
104
|
fundef = "def name; @data.#{field}; end"
|
100
|
-
|
105
|
+
class_eval(fundef)
|
101
106
|
end
|
102
107
|
|
103
108
|
def self.std_timestamp
|
104
109
|
[:created_at, :updated_at].each do |i|
|
105
|
-
|
110
|
+
ro_access(i)
|
106
111
|
end
|
107
112
|
end
|
113
|
+
|
108
114
|
# Pretty much any object with an authz id has these fields
|
109
115
|
def self.std_authz
|
110
|
-
|
116
|
+
std_timestamp
|
111
117
|
[:authz_id, :last_updated_by].each do |i|
|
112
|
-
|
118
|
+
ro_access(i)
|
113
119
|
end
|
114
120
|
end
|
115
121
|
|
@@ -117,7 +123,7 @@ module ChefFixie
|
|
117
123
|
rows = table.by_id(id)
|
118
124
|
raise "id #{id} matches more than one object" if rows.all.count != 1
|
119
125
|
rows.inner.delete
|
120
|
-
if
|
126
|
+
if respond_to?(:authz_delete)
|
121
127
|
authz_delete
|
122
128
|
end
|
123
129
|
end
|
@@ -131,13 +137,14 @@ module ChefFixie
|
|
131
137
|
funname = Relationships.plural(object)
|
132
138
|
# defer evaluation of mapper to make sure we have a chance for everyone to initialize
|
133
139
|
fundef = "def #{funname}; Relationships.table_class(:#{object}).new.by_org_id(org_id); end"
|
134
|
-
|
140
|
+
class_eval(fundef)
|
135
141
|
end
|
136
142
|
end
|
137
143
|
|
138
144
|
def initialize(data)
|
139
145
|
super(data)
|
140
146
|
end
|
147
|
+
|
141
148
|
def org_id
|
142
149
|
data[:id]
|
143
150
|
end
|
@@ -145,16 +152,20 @@ module ChefFixie
|
|
145
152
|
def global_admins
|
146
153
|
name = self.name
|
147
154
|
global_admins_name = "#{name}_global_admins"
|
148
|
-
|
155
|
+
read_access_name = "#{name}_read_access_group"
|
156
|
+
ChefFixie::Sql::Groups.new[global_admins_name] || \
|
157
|
+
ChefFixie::Sql::Groups.new[read_access_name]
|
149
158
|
end
|
150
159
|
|
160
|
+
alias read_access_group global_admins
|
161
|
+
|
151
162
|
# Iterators for objects in authz; using containers to enumerate things
|
152
163
|
# It might be better to metaprogram this up instead,
|
153
164
|
#
|
154
165
|
# TODO Write some tests to validate that this stuff
|
155
166
|
# works, since it depends on a lot of name magic...
|
156
167
|
|
157
|
-
NAME_FIXUP = {"data" => "data_bags", "sandboxes" => nil}
|
168
|
+
NAME_FIXUP = { "data" => "data_bags", "sandboxes" => nil }
|
158
169
|
def objects_by_container_type(container)
|
159
170
|
name = NAME_FIXUP.has_key?(container) ? NAME_FIXUP[container] : container
|
160
171
|
return [] if name.nil?
|
@@ -172,7 +183,7 @@ module ChefFixie
|
|
172
183
|
yield objects
|
173
184
|
end
|
174
185
|
end
|
175
|
-
|
186
|
+
nil
|
176
187
|
end
|
177
188
|
|
178
189
|
def each_authz_object
|
@@ -181,7 +192,7 @@ module ChefFixie
|
|
181
192
|
yield object
|
182
193
|
end
|
183
194
|
end
|
184
|
-
|
195
|
+
nil
|
185
196
|
end
|
186
197
|
|
187
198
|
scoped_type :container, :group, :client,
|
@@ -253,7 +264,6 @@ module ChefFixie
|
|
253
264
|
# org_migration_state_id_seq policy_revisions
|
254
265
|
# policy_revisions_policy_groups_association sandboxed_checksums
|
255
266
|
|
256
|
-
|
257
267
|
class CookbookArtifact < SqlObject
|
258
268
|
include AuthzObjectMixin
|
259
269
|
def initialize(data)
|
@@ -338,31 +348,33 @@ module ChefFixie
|
|
338
348
|
def get_table
|
339
349
|
:unknown_table
|
340
350
|
end
|
351
|
+
|
341
352
|
def mk_element(x)
|
342
353
|
x
|
343
354
|
end
|
344
355
|
|
345
356
|
def initialize(tablespec = nil)
|
346
357
|
ChefFixie::Sql.default_connection
|
347
|
-
@inner = tablespec || Sequel::Model(
|
358
|
+
@inner = tablespec || Sequel::Model(get_table)
|
348
359
|
end
|
360
|
+
|
349
361
|
def inner
|
350
362
|
# Make sure we have init
|
351
363
|
@inner
|
352
364
|
end
|
353
365
|
|
354
366
|
def filter_core(field, exp)
|
355
|
-
self.class.new(inner.filter(field=>exp))
|
367
|
+
self.class.new(inner.filter(field => exp))
|
356
368
|
end
|
357
369
|
|
358
|
-
def all(max_count
|
370
|
+
def all(max_count = :default)
|
359
371
|
if max_count == :default
|
360
372
|
max_count = ChefFixie::Sql::SqlTable.max_count_default
|
361
373
|
end
|
362
374
|
if max_count != :all
|
363
|
-
return :too_many_results if
|
375
|
+
return :too_many_results if inner.count > max_count
|
364
376
|
end
|
365
|
-
elements = inner.all.map {|org| mk_element(org) }
|
377
|
+
elements = inner.all.map { |org| mk_element(org) }
|
366
378
|
end
|
367
379
|
|
368
380
|
#
|
@@ -371,7 +383,7 @@ module ChefFixie
|
|
371
383
|
# https://stackoverflow.com/questions/9658724/ruby-metaprogramming-class-eval/9658775#9658775
|
372
384
|
def self.primary(arg)
|
373
385
|
name = :"by_#{arg}"
|
374
|
-
|
386
|
+
class_eval("def [](arg); #{name}(arg).all(1).first; end")
|
375
387
|
|
376
388
|
listfun = <<EOLF
|
377
389
|
def list(max_count=:default)
|
@@ -383,26 +395,27 @@ def list(max_count=:default)
|
|
383
395
|
end
|
384
396
|
end
|
385
397
|
EOLF
|
386
|
-
|
398
|
+
class_eval(listfun)
|
387
399
|
end
|
388
400
|
|
389
401
|
def self.filter_by(*args)
|
390
402
|
args.each do |field|
|
391
403
|
name = "by_#{field}"
|
392
404
|
fundef = "def #{name}(exp); filter_core(:#{field},exp); end"
|
393
|
-
|
405
|
+
class_eval(fundef)
|
394
406
|
end
|
395
407
|
end
|
396
408
|
|
397
409
|
def self.table(name)
|
398
410
|
fundef = "def get_table; :#{name}; end"
|
399
|
-
|
411
|
+
class_eval(fundef)
|
400
412
|
end
|
413
|
+
|
401
414
|
# doesn't work yet
|
402
415
|
# element Org in class Orgs will fail because it can't find Org (undefined)
|
403
416
|
def self.element(name)
|
404
417
|
fundef = "ElementType = name; def mk_element(x); #{name}.new(x); end"
|
405
|
-
|
418
|
+
class_eval(fundef)
|
406
419
|
end
|
407
420
|
end
|
408
421
|
|
@@ -414,7 +427,7 @@ EOLF
|
|
414
427
|
primary :name
|
415
428
|
filter_by :name, :id, :full_name, :authz_id
|
416
429
|
|
417
|
-
GlobalOrg = "0"*32
|
430
|
+
GlobalOrg = "0" * 32
|
418
431
|
|
419
432
|
def self.org_guid_to_name(guid)
|
420
433
|
"global" if guid == GlobalOrg
|
@@ -435,7 +448,7 @@ EOLF
|
|
435
448
|
|
436
449
|
def by_org_id_user_id(org_id, user_id)
|
437
450
|
# db table constraint guarantees that this is unique
|
438
|
-
inner.filter(:org_id=>org_id, :user_id=>user_id).all.first
|
451
|
+
inner.filter(:org_id => org_id, :user_id => user_id).all.first
|
439
452
|
end
|
440
453
|
|
441
454
|
end
|
@@ -445,7 +458,7 @@ EOLF
|
|
445
458
|
|
446
459
|
def by_org_id_user_id(org_id, user_id)
|
447
460
|
# db table constraint guarantees that this is unique
|
448
|
-
inner.filter(:org_id=>org_id, :user_id=>user_id).all.first
|
461
|
+
inner.filter(:org_id => org_id, :user_id => user_id).all.first
|
449
462
|
end
|
450
463
|
end
|
451
464
|
class Users < SqlTable
|
@@ -547,7 +560,7 @@ EOLF
|
|
547
560
|
filter_by :name, :id, :org_id, :authz_id
|
548
561
|
end
|
549
562
|
|
550
|
-
class Roles
|
563
|
+
class Roles < SqlTable
|
551
564
|
table :roles
|
552
565
|
element Sql::Role
|
553
566
|
register_authz :role, :object
|
@@ -556,7 +569,5 @@ EOLF
|
|
556
569
|
filter_by :name, :id, :org_id, :authz_id, :last_updated_by
|
557
570
|
end
|
558
571
|
|
559
|
-
|
560
|
-
|
561
572
|
end
|
562
573
|
end
|
@@ -18,42 +18,46 @@
|
|
18
18
|
# Author: Mark Anderson <mark@chef.io>
|
19
19
|
#
|
20
20
|
|
21
|
-
require_relative
|
22
|
-
require_relative
|
23
|
-
require_relative
|
21
|
+
require_relative "config"
|
22
|
+
require_relative "authz_objects"
|
23
|
+
require_relative "authz_mapper"
|
24
24
|
|
25
25
|
module ChefFixie
|
26
26
|
module UtilityHelpers
|
27
27
|
def self.orgs
|
28
28
|
@orgs ||= ChefFixie::Sql::Orgs.new
|
29
29
|
end
|
30
|
+
|
30
31
|
def self.users
|
31
32
|
@users ||= ChefFixie::Sql::Users.new
|
32
33
|
end
|
34
|
+
|
33
35
|
def self.assocs
|
34
36
|
@assocs ||= ChefFixie::Sql::Associations.new
|
35
37
|
end
|
38
|
+
|
36
39
|
def self.invites
|
37
40
|
invites ||= ChefFixie::Sql::Invites.new
|
38
41
|
end
|
39
42
|
|
40
43
|
def self.make_user(user)
|
41
44
|
if user.is_a?(String)
|
42
|
-
|
45
|
+
users[user]
|
43
46
|
elsif user.is_a?(ChefFixie::Sql::User)
|
44
|
-
|
47
|
+
user
|
45
48
|
else
|
46
49
|
raise Exception "Expected a user, got a #{user.class}"
|
47
50
|
end
|
48
|
-
end
|
51
|
+
end
|
52
|
+
|
49
53
|
def self.make_org(org)
|
50
54
|
if org.is_a?(String)
|
51
|
-
|
55
|
+
orgs[org]
|
52
56
|
elsif org.is_a?(ChefFixie::Sql::Org)
|
53
|
-
|
57
|
+
org
|
54
58
|
else
|
55
59
|
raise Exception "Expected an org, got a #{org.class}"
|
56
60
|
end
|
57
|
-
end
|
61
|
+
end
|
58
62
|
end
|
59
63
|
end
|
data/lib/chef_fixie/version.rb
CHANGED
data/spec/chef_fixie/acl_spec.rb
CHANGED
@@ -1,46 +1,45 @@
|
|
1
1
|
|
2
|
-
require
|
2
|
+
require "rspec"
|
3
3
|
require "spec_helper"
|
4
|
-
require
|
5
|
-
require
|
4
|
+
require "chef_fixie"
|
5
|
+
require "chef_fixie/config"
|
6
6
|
|
7
7
|
RSpec.describe ChefFixie::Sql::Orgs, "ACL access" do
|
8
|
-
let (:test_org_name) { "ponyville"}
|
8
|
+
let (:test_org_name) { "ponyville" }
|
9
9
|
let (:orgs) { ChefFixie::Sql::Orgs.new }
|
10
10
|
let (:users) { ChefFixie::Sql::Users.new }
|
11
11
|
let (:test_org) { orgs[test_org_name] }
|
12
12
|
|
13
13
|
# TODO this should use a freshly created object and purge it afterwords.
|
14
14
|
# But we need to write the create object feature still
|
15
|
-
|
15
|
+
|
16
16
|
context "Fetch acl for actor (client)" do
|
17
17
|
let (:testclient) { test_org.clients.all.first }
|
18
|
-
let (:testuser) { users[
|
19
|
-
let (:pivotal) { users[
|
18
|
+
let (:testuser) { users["spitfire"] }
|
19
|
+
let (:pivotal) { users["pivotal"] }
|
20
20
|
let (:client_container) { test_org.containers["clients"] }
|
21
|
-
|
21
|
+
|
22
22
|
it "We can fetch the acl" do
|
23
23
|
acl = testclient.acl
|
24
|
-
expect(acl.keys).to include(* %w
|
24
|
+
expect(acl.keys).to include(* %w{create read update delete grant})
|
25
25
|
end
|
26
26
|
|
27
27
|
it "we can add a user to an ace" do
|
28
|
-
# This requires either a temp object or good cleanup
|
28
|
+
# This requires either a temp object or good cleanup
|
29
29
|
# acl = testclient.acl
|
30
30
|
# expect(acl["read"]["actors"].not_to include("wonderbolts")
|
31
|
-
|
31
|
+
|
32
32
|
testclient.ace_add(:read, testuser)
|
33
33
|
|
34
34
|
acl = testclient.acl
|
35
35
|
expect(acl["read"]["actors"]).to include([:global, testuser.name])
|
36
36
|
end
|
37
|
-
|
37
|
+
|
38
38
|
it "we can add then delete a user from an ace" do
|
39
39
|
testclient.ace_add(:read, testuser)
|
40
40
|
acl = testclient.acl
|
41
41
|
expect(acl["read"]["actors"]).to include([:global, testuser.name])
|
42
42
|
|
43
|
-
|
44
43
|
testclient.ace_delete(:read, testuser)
|
45
44
|
|
46
45
|
acl = testclient.acl
|
@@ -49,26 +48,26 @@ RSpec.describe ChefFixie::Sql::Orgs, "ACL access" do
|
|
49
48
|
|
50
49
|
it "we can copy users from another acl" do
|
51
50
|
testclient.ace_delete(:all, pivotal)
|
52
|
-
|
51
|
+
|
53
52
|
testclient.acl_add_from_object(client_container)
|
54
53
|
|
55
54
|
acl = testclient.acl
|
56
|
-
%w
|
55
|
+
%w{create read update delete grant}.each do |action|
|
57
56
|
expect(acl[action]["actors"]).to include([:global, pivotal.name])
|
58
57
|
end
|
59
58
|
end
|
60
|
-
|
59
|
+
|
61
60
|
end
|
62
61
|
|
63
62
|
context "ACE Membership" do
|
64
|
-
|
65
|
-
let (:admingroup) { test_org.groups[
|
66
|
-
let (:testobject) { test_org.groups[
|
67
|
-
let (:notadmingroup) { test_org.groups[
|
68
|
-
let (:adminuser) { users[
|
69
|
-
let (:notadminuser) { users[
|
70
|
-
let (:pivotal) { users[
|
71
|
-
|
63
|
+
|
64
|
+
let (:admingroup) { test_org.groups["admins"] }
|
65
|
+
let (:testobject) { test_org.groups["admins"] }
|
66
|
+
let (:notadmingroup) { test_org.groups["clients"] }
|
67
|
+
let (:adminuser) { users["rainbowdash"] }
|
68
|
+
let (:notadminuser) { users["mary"] }
|
69
|
+
let (:pivotal) { users["pivotal"] }
|
70
|
+
|
72
71
|
it "Privileged users and groups are part of the read ACE" do
|
73
72
|
expect(testobject.ace_member?(:read, admingroup)).to be true
|
74
73
|
expect(testobject.ace_member?(:read, pivotal)).to be true
|
@@ -79,5 +78,4 @@ RSpec.describe ChefFixie::Sql::Orgs, "ACL access" do
|
|
79
78
|
end
|
80
79
|
end
|
81
80
|
|
82
|
-
|
83
81
|
end
|