chef_fixie 0.1.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 +7 -0
- data/LICENSE +201 -0
- data/README.md +69 -0
- data/bin/bundler +16 -0
- data/bin/chef-apply +16 -0
- data/bin/chef-client +16 -0
- data/bin/chef-shell +16 -0
- data/bin/chef-solo +16 -0
- data/bin/chef-zero +16 -0
- data/bin/chef_fixie +5 -0
- data/bin/coderay +16 -0
- data/bin/edit_json.rb +16 -0
- data/bin/erubis +16 -0
- data/bin/ffi-yajl-bench +16 -0
- data/bin/fixie~ +231 -0
- data/bin/htmldiff +16 -0
- data/bin/knife +16 -0
- data/bin/ldiff +16 -0
- data/bin/net-dhcp +16 -0
- data/bin/ohai +16 -0
- data/bin/prettify_json.rb +16 -0
- data/bin/pry +16 -0
- data/bin/rackup +16 -0
- data/bin/rake +16 -0
- data/bin/rdoc +16 -0
- data/bin/restclient +16 -0
- data/bin/ri +16 -0
- data/bin/rspec +16 -0
- data/bin/s3sh +16 -0
- data/bin/sequel +16 -0
- data/bin/serverspec-init +16 -0
- data/doc/AccessingSQL.md +36 -0
- data/doc/AccessingSQL.md~ +32 -0
- data/doc/BulkFixup.md~ +28 -0
- data/doc/CommonTasks.md +20 -0
- data/doc/CommonTasks.md~ +0 -0
- data/doc/GETTING_STARTED.md +228 -0
- data/doc/GETTING_STARTED.md~ +6 -0
- data/fixie.conf.example +8 -0
- data/lib/chef_fixie.rb +27 -0
- data/lib/chef_fixie/authz_mapper.rb +143 -0
- data/lib/chef_fixie/authz_objects.rb +285 -0
- data/lib/chef_fixie/check_org_associations.rb +242 -0
- data/lib/chef_fixie/config.rb +139 -0
- data/lib/chef_fixie/console.rb +91 -0
- data/lib/chef_fixie/context.rb +72 -0
- data/lib/chef_fixie/sql.rb +74 -0
- data/lib/chef_fixie/sql_objects.rb +497 -0
- data/lib/chef_fixie/utility_helpers.rb +59 -0
- data/lib/chef_fixie/version.rb +3 -0
- data/spec/chef_fixie/acl_spec.rb +83 -0
- data/spec/chef_fixie/assoc_invite_spec.rb +47 -0
- data/spec/chef_fixie/assoc_invite_spec.rb~ +26 -0
- data/spec/chef_fixie/check_org_associations_spec.rb +140 -0
- data/spec/chef_fixie/check_org_associations_spec.rb~ +34 -0
- data/spec/chef_fixie/groups_spec.rb +34 -0
- data/spec/chef_fixie/org_spec.rb +26 -0
- data/spec/chef_fixie/org_spec.rb~ +53 -0
- data/spec/chef_fixie/orgs_spec.rb +53 -0
- data/spec/spec_helper.rb +41 -0
- metadata +252 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
# -*- indent-tabs-mode: nil; fill-column: 110 -*-
|
2
|
+
#
|
3
|
+
# Copyright (c) 2015 Chef Software Inc.
|
4
|
+
# License :: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
# Author: Mark Anderson <mark@chef.io>
|
19
|
+
#
|
20
|
+
|
21
|
+
require 'chef_fixie/config'
|
22
|
+
require 'chef_fixie/authz_objects'
|
23
|
+
require 'chef_fixie/authz_mapper'
|
24
|
+
|
25
|
+
module ChefFixie
|
26
|
+
module UtilityHelpers
|
27
|
+
def self.orgs
|
28
|
+
@orgs ||= ChefFixie::Sql::Orgs.new
|
29
|
+
end
|
30
|
+
def self.users
|
31
|
+
@users ||= ChefFixie::Sql::Users.new
|
32
|
+
end
|
33
|
+
def self.assocs
|
34
|
+
@assocs ||= ChefFixie::Sql::Associations.new
|
35
|
+
end
|
36
|
+
def self.invites
|
37
|
+
invites ||= ChefFixie::Sql::Invites.new
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.make_user(user)
|
41
|
+
if user.is_a?(String)
|
42
|
+
return users[user]
|
43
|
+
elsif user.is_a?(ChefFixie::Sql::User)
|
44
|
+
return user
|
45
|
+
else
|
46
|
+
raise Exception "Expected a user, got a #{user.class}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
def self.make_org(org)
|
50
|
+
if org.is_a?(String)
|
51
|
+
return orgs[org]
|
52
|
+
elsif org.is_a?(ChefFixie::Sql::Org)
|
53
|
+
return org
|
54
|
+
else
|
55
|
+
raise Exception "Expected an org, got a #{org.class}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
|
2
|
+
require 'rspec'
|
3
|
+
require "spec_helper"
|
4
|
+
require 'chef_fixie'
|
5
|
+
require 'chef_fixie/config'
|
6
|
+
|
7
|
+
RSpec.describe ChefFixie::Sql::Orgs, "ACL access" do
|
8
|
+
let (:test_org_name) { "ponyville"}
|
9
|
+
let (:orgs) { ChefFixie::Sql::Orgs.new }
|
10
|
+
let (:users) { ChefFixie::Sql::Users.new }
|
11
|
+
let (:test_org) { orgs[test_org_name] }
|
12
|
+
|
13
|
+
# TODO this should use a freshly created object and purge it afterwords.
|
14
|
+
# But we need to write the create object feature still
|
15
|
+
|
16
|
+
context "Fetch acl for actor (client)" do
|
17
|
+
let (:testclient) { test_org.clients.all.first }
|
18
|
+
let (:testuser) { users['spitfire'] }
|
19
|
+
let (:pivotal) { users['pivotal'] }
|
20
|
+
let (:client_container) { test_org.containers["clients"] }
|
21
|
+
|
22
|
+
it "We can fetch the acl" do
|
23
|
+
acl = testclient.acl
|
24
|
+
expect(acl.keys).to include(* %w(create read update delete grant))
|
25
|
+
end
|
26
|
+
|
27
|
+
it "we can add a user to an ace" do
|
28
|
+
# This requires either a temp object or good cleanup
|
29
|
+
# acl = testclient.acl
|
30
|
+
# expect(acl["read"]["actors"].not_to include("wonderbolts")
|
31
|
+
|
32
|
+
testclient.ace_add(:read, testuser)
|
33
|
+
|
34
|
+
acl = testclient.acl
|
35
|
+
expect(acl["read"]["actors"]).to include([:global, testuser.name])
|
36
|
+
end
|
37
|
+
|
38
|
+
it "we can add then delete a user from an ace" do
|
39
|
+
testclient.ace_add(:read, testuser)
|
40
|
+
acl = testclient.acl
|
41
|
+
expect(acl["read"]["actors"]).to include([:global, testuser.name])
|
42
|
+
|
43
|
+
|
44
|
+
testclient.ace_delete(:read, testuser)
|
45
|
+
|
46
|
+
acl = testclient.acl
|
47
|
+
expect(acl["read"]["actors"]).not_to include([:global, testuser.name])
|
48
|
+
end
|
49
|
+
|
50
|
+
it "we can copy users from another acl" do
|
51
|
+
testclient.ace_delete(:all, pivotal)
|
52
|
+
|
53
|
+
testclient.acl_add_from_object(client_container)
|
54
|
+
|
55
|
+
acl = testclient.acl
|
56
|
+
%w(create read update delete grant).each do |action|
|
57
|
+
expect(acl[action]["actors"]).to include([:global, pivotal.name])
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
context "ACE Membership" do
|
64
|
+
|
65
|
+
let (:admingroup) { test_org.groups['admins'] }
|
66
|
+
let (:testobject) { test_org.groups['admins'] }
|
67
|
+
let (:notadmingroup) { test_org.groups['clients'] }
|
68
|
+
let (:adminuser) { users['rainbowdash'] }
|
69
|
+
let (:notadminuser) { users['mary'] }
|
70
|
+
let (:pivotal) { users['pivotal'] }
|
71
|
+
|
72
|
+
it "Privileged users and groups are part of the read ACE" do
|
73
|
+
expect(testobject.ace_member?(:read, admingroup)).to be true
|
74
|
+
expect(testobject.ace_member?(:read, pivotal)).to be true
|
75
|
+
end
|
76
|
+
it "Unprivileged members are not part of read ACE" do
|
77
|
+
expect(testobject.member?(notadmingroup)).to be false
|
78
|
+
expect(testobject.member?(notadminuser)).to be false
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
|
2
|
+
require 'rspec'
|
3
|
+
require "spec_helper"
|
4
|
+
require 'chef_fixie'
|
5
|
+
require 'chef_fixie/config'
|
6
|
+
|
7
|
+
RSpec.describe ChefFixie::Sql::Associations, "Associations tests" do
|
8
|
+
let (:test_org_name) { "ponyville" }
|
9
|
+
let (:orgs) { ChefFixie::Sql::Orgs.new }
|
10
|
+
let (:test_org) { orgs[test_org_name]}
|
11
|
+
|
12
|
+
let (:users) { ChefFixie::Sql::Users.new }
|
13
|
+
let (:assocs) { ChefFixie::Sql::Associations.new }
|
14
|
+
|
15
|
+
|
16
|
+
context "Basic functionality of association spec" do
|
17
|
+
let ("test_user_name") { "fluttershy" }
|
18
|
+
let ("test_user") { users[test_user_name] }
|
19
|
+
it "Can fetch by user id" do
|
20
|
+
assocs_by_user = assocs.by_user_id(test_user.id).all
|
21
|
+
expect(assocs_by_user).not_to be_nil
|
22
|
+
expect(assocs_by_user.count).to eq(1)
|
23
|
+
expect(assocs_by_user.first.user_id ).to eq(test_user.id)
|
24
|
+
expect(assocs_by_user.first.org_id ).to eq(test_org.id)
|
25
|
+
end
|
26
|
+
it "Can fetch by org id" do
|
27
|
+
assocs_by_org = assocs.by_org_id(test_org.id).all
|
28
|
+
expect(assocs_by_org).not_to be_nil
|
29
|
+
expect(assocs_by_org.count).to be > 1
|
30
|
+
expect(assocs_by_org.first.org_id).to eq(test_org.id)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "Can fetch by both org/user id" do
|
34
|
+
assoc_item = assocs.by_org_id_user_id(test_org.id, test_user.id)
|
35
|
+
expect(assoc_item).not_to be_nil
|
36
|
+
expect(assoc_item.user_id).to eq(test_user.id)
|
37
|
+
expect(assoc_item.org_id).to eq(test_org.id)
|
38
|
+
|
39
|
+
# test user not in org
|
40
|
+
expect(assocs.by_org_id_user_id(test_org.id, users['mary'].id)).to be_nil
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
|
2
|
+
require 'rspec'
|
3
|
+
require "spec_helper"
|
4
|
+
require 'fixie'
|
5
|
+
require 'fixie/config'
|
6
|
+
|
7
|
+
RSpec.describe Fixie::Sql::Orgs, "Organizations access" do
|
8
|
+
let (:test_org_name) { "ponyville" }
|
9
|
+
let (:orgs) { Fixie::Sql::Orgs.new }
|
10
|
+
let (:test_org) { orgs[test_org_name]}
|
11
|
+
|
12
|
+
context "Basic functionality of org accessor" do
|
13
|
+
|
14
|
+
it "Org has a name and id" do
|
15
|
+
expect(test_org.name).to eq(test_org_name)
|
16
|
+
expect(test_org.id).not_to be_nil
|
17
|
+
end
|
18
|
+
|
19
|
+
it "Org has a global admins group" do
|
20
|
+
expect(test_org.global_admins.name).to eq(test_org_name + "_global_admins")
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# -*- indent-tabs-mode: nil; fill-column: 110 -*-
|
2
|
+
require 'rspec'
|
3
|
+
require "spec_helper"
|
4
|
+
require 'chef_fixie'
|
5
|
+
require 'chef_fixie/config'
|
6
|
+
|
7
|
+
RSpec.describe ChefFixie::CheckOrgAssociations, "Association checker" do
|
8
|
+
let (:test_org_name) { "ponyville"}
|
9
|
+
let (:orgs) { ChefFixie::Sql::Orgs.new }
|
10
|
+
let (:test_org) { orgs[test_org_name] }
|
11
|
+
|
12
|
+
let (:users) { ChefFixie::Sql::Users.new }
|
13
|
+
let (:adminuser) { users['rainbowdash'] }
|
14
|
+
let (:notorguser) { users['mary'] }
|
15
|
+
|
16
|
+
# TODO this should use a freshly created object and purge it afterwords.
|
17
|
+
# But we need to write the create object feature still
|
18
|
+
|
19
|
+
context "Individual user check" do
|
20
|
+
it "Works on expected sane org/user pair" do
|
21
|
+
expect(ChefFixie::CheckOrgAssociations.check_association(test_org, adminuser)).to be true
|
22
|
+
expect(ChefFixie::CheckOrgAssociations.check_association(test_org_name, adminuser.name)).to be true
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
context "Individual user check" do
|
27
|
+
before :each do
|
28
|
+
expect(ChefFixie::CheckOrgAssociations.check_association(test_org, adminuser)).to be true
|
29
|
+
end
|
30
|
+
|
31
|
+
after :each do
|
32
|
+
usag = test_org.groups[adminuser.id]
|
33
|
+
|
34
|
+
usag.group_add(adminuser)
|
35
|
+
test_org.groups['users'].group_add(usag)
|
36
|
+
|
37
|
+
adminuser.ace_add(:read, test_org.global_admins)
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
it "Detects user not associated" do
|
42
|
+
# break it
|
43
|
+
expect(ChefFixie::CheckOrgAssociations.check_association(test_org, notorguser)).to be :not_associated
|
44
|
+
end
|
45
|
+
|
46
|
+
# TODO: Write missing USAG test, but can't until we can restore the USAG or use disposable org
|
47
|
+
|
48
|
+
it "Detects user missing from usag" do
|
49
|
+
# break it
|
50
|
+
usag = test_org.groups[adminuser.id]
|
51
|
+
usag.group_delete(adminuser)
|
52
|
+
|
53
|
+
expect(ChefFixie::CheckOrgAssociations.check_association(test_org, adminuser)).to be :user_not_in_usag
|
54
|
+
end
|
55
|
+
|
56
|
+
it "Detects usag missing from users group" do
|
57
|
+
# break it
|
58
|
+
usag = test_org.groups[adminuser.id]
|
59
|
+
test_org.groups['users'].group_delete(usag)
|
60
|
+
|
61
|
+
expect(ChefFixie::CheckOrgAssociations.check_association(test_org, adminuser)).to be :usag_not_in_users
|
62
|
+
end
|
63
|
+
|
64
|
+
it "Detects global admins missing read" do
|
65
|
+
# break it
|
66
|
+
adminuser.ace_delete(:read, test_org.global_admins)
|
67
|
+
|
68
|
+
expect(ChefFixie::CheckOrgAssociations.check_association(test_org, adminuser)).to be :global_admins_lacks_read
|
69
|
+
end
|
70
|
+
|
71
|
+
# TODO test zombie invite; need some way to create it.
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
context "Individual user fixup" do
|
76
|
+
before :each do
|
77
|
+
expect(ChefFixie::CheckOrgAssociations.check_association(test_org, adminuser)).to be true
|
78
|
+
end
|
79
|
+
|
80
|
+
after :each do
|
81
|
+
usag = test_org.groups[adminuser.id]
|
82
|
+
|
83
|
+
usag.group_add(adminuser)
|
84
|
+
test_org.groups['users'].group_add(usag)
|
85
|
+
|
86
|
+
adminuser.ace_add(:read, test_org.global_admins)
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
it "Detects user not associated" do
|
91
|
+
# break it
|
92
|
+
expect(ChefFixie::CheckOrgAssociations.check_association(test_org, notorguser)).to be :not_associated
|
93
|
+
end
|
94
|
+
|
95
|
+
# TODO: Write missing USAG test, but can't until we can restore the USAG or use disposable org
|
96
|
+
|
97
|
+
it "Fixes user missing from usag" do
|
98
|
+
# break it
|
99
|
+
usag = test_org.groups[adminuser.id]
|
100
|
+
usag.group_delete(adminuser)
|
101
|
+
|
102
|
+
expect(ChefFixie::CheckOrgAssociations.fix_association(test_org, adminuser)).to be true
|
103
|
+
expect(ChefFixie::CheckOrgAssociations.check_association(test_org, adminuser)).to be true
|
104
|
+
end
|
105
|
+
|
106
|
+
it "Fixes usag missing from users group" do
|
107
|
+
# break it
|
108
|
+
usag = test_org.groups[adminuser.id]
|
109
|
+
test_org.groups['users'].group_delete(usag)
|
110
|
+
|
111
|
+
expect(ChefFixie::CheckOrgAssociations.fix_association(test_org, adminuser)).to be true
|
112
|
+
expect(ChefFixie::CheckOrgAssociations.check_association(test_org, adminuser)).to be true
|
113
|
+
end
|
114
|
+
|
115
|
+
it "Fixes global admins missing read" do
|
116
|
+
# break it
|
117
|
+
adminuser.ace_delete(:read, test_org.global_admins)
|
118
|
+
|
119
|
+
expect(ChefFixie::CheckOrgAssociations.fix_association(test_org, adminuser)).to be true
|
120
|
+
expect(ChefFixie::CheckOrgAssociations.check_association(test_org, adminuser)).to be true
|
121
|
+
end
|
122
|
+
|
123
|
+
# TODO test zombie invite; need some way to create it.
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
# TODO Break the org and check it!
|
129
|
+
context "Global org check" do
|
130
|
+
|
131
|
+
it "Works on expected sane org" do
|
132
|
+
expect(ChefFixie::CheckOrgAssociations.check_associations("acme")).to be true
|
133
|
+
expect(ChefFixie::CheckOrgAssociations.check_associations(orgs["acme"])).to be true
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
|
140
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
|
2
|
+
require 'rspec'
|
3
|
+
require "spec_helper"
|
4
|
+
require 'fixie'
|
5
|
+
require 'fixie/config'
|
6
|
+
|
7
|
+
RSpec.describe Fixie::Sql::Groups, "Group access" do
|
8
|
+
let (:test_org_name) { "ponyville"}
|
9
|
+
let (:orgs) { Fixie::Sql::Orgs.new }
|
10
|
+
let (:users) { Fixie::Sql::Users.new }
|
11
|
+
let (:test_org) { orgs[test_org_name] }
|
12
|
+
|
13
|
+
# TODO this should use a freshly created object and purge it afterwords.
|
14
|
+
# But we need to write the create object feature still
|
15
|
+
|
16
|
+
context "Groups" do
|
17
|
+
let (:testgroup) { test_org.groups['admins'] }
|
18
|
+
let (:adminuser) { users['rainbowdash'] }
|
19
|
+
let (:notadminuser) { users['mary'] }
|
20
|
+
|
21
|
+
it "Members are part of the group" do
|
22
|
+
expect(testgroup.member?(adminuser)).to be true
|
23
|
+
end
|
24
|
+
it "Members are not part of the group" do
|
25
|
+
expect(testgroup.member?(notadminuser)).to be false
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# -*- indent-tabs-mode: nil; fill-column: 110 -*-
|
2
|
+
require 'rspec'
|
3
|
+
require "spec_helper"
|
4
|
+
require 'chef_fixie'
|
5
|
+
require 'chef_fixie/config'
|
6
|
+
|
7
|
+
RSpec.describe ChefFixie::Sql::Groups, "Group access" do
|
8
|
+
let (:test_org_name) { "ponyville"}
|
9
|
+
let (:orgs) { ChefFixie::Sql::Orgs.new }
|
10
|
+
let (:users) { ChefFixie::Sql::Users.new }
|
11
|
+
let (:test_org) { orgs[test_org_name] }
|
12
|
+
|
13
|
+
# TODO this should use a freshly created object and purge it afterwords.
|
14
|
+
# But we need to write the create object feature still
|
15
|
+
|
16
|
+
context "Groups" do
|
17
|
+
let (:testgroup) { test_org.groups['admins'] }
|
18
|
+
let (:adminuser) { users['rainbowdash'] }
|
19
|
+
let (:notadminuser) { users['mary'] }
|
20
|
+
|
21
|
+
it "Members are part of the group" do
|
22
|
+
expect(testgroup.member?(adminuser)).to be true
|
23
|
+
end
|
24
|
+
it "Members are not part of the group" do
|
25
|
+
expect(testgroup.member?(notadminuser)).to be false
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
|
2
|
+
require 'rspec'
|
3
|
+
require "spec_helper"
|
4
|
+
require 'chef_fixie'
|
5
|
+
require 'chef_fixie/config'
|
6
|
+
|
7
|
+
RSpec.describe ChefFixie::Sql::Orgs, "Organizations access" do
|
8
|
+
let (:test_org_name) { "ponyville" }
|
9
|
+
let (:orgs) { ChefFixie::Sql::Orgs.new }
|
10
|
+
let (:test_org) { orgs[test_org_name]}
|
11
|
+
|
12
|
+
context "Basic functionality of org accessor" do
|
13
|
+
|
14
|
+
it "Org has a name and id" do
|
15
|
+
expect(test_org.name).to eq(test_org_name)
|
16
|
+
expect(test_org.id).not_to be_nil
|
17
|
+
end
|
18
|
+
|
19
|
+
it "Org has a global admins group" do
|
20
|
+
expect(test_org.global_admins.name).to eq(test_org_name + "_global_admins")
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
end
|