chef-vpc-toolkit 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/COPYING +26 -0
- data/README.rdoc +75 -0
- data/Rakefile +34 -0
- data/VERSION +1 -0
- data/bin/chef-vpc-toolkit +80 -0
- data/config/chef_installer.yml +20 -0
- data/config/databags.json.example +24 -0
- data/config/nodes.json +10 -0
- data/config/server_group.json +16 -0
- data/contrib/doc/ChefVPCToolkit.odt +0 -0
- data/contrib/doc/ChefVPCToolkit.pdf +0 -0
- data/contrib/etc/chef_vpc_toolkit.conf +10 -0
- data/contrib/rake/Rakefile +23 -0
- data/cookbook-repos/local/README +5 -0
- data/cookbook-repos/local/Rakefile +66 -0
- data/cookbook-repos/local/certificates/README +1 -0
- data/cookbook-repos/local/config/client.rb.example +21 -0
- data/cookbook-repos/local/config/knife.rb.example +10 -0
- data/cookbook-repos/local/config/rake.rb +60 -0
- data/cookbook-repos/local/config/server.rb.example +42 -0
- data/cookbook-repos/local/config/solo.rb.example +13 -0
- data/cookbook-repos/local/cookbooks/README +4 -0
- data/cookbook-repos/local/cookbooks/motd/README.rdoc +15 -0
- data/cookbook-repos/local/cookbooks/motd/attributes/motd.rb +1 -0
- data/cookbook-repos/local/cookbooks/motd/metadata.rb +6 -0
- data/cookbook-repos/local/cookbooks/motd/recipes/default.rb +13 -0
- data/cookbook-repos/local/cookbooks/motd/templates/default/motd.erb +1 -0
- data/cookbook-repos/local/roles/README +4 -0
- data/lib/chef-vpc-toolkit.rb +6 -0
- data/lib/chef-vpc-toolkit/chef-0.9.bash +232 -0
- data/lib/chef-vpc-toolkit/chef_bootstrap/centos.bash +47 -0
- data/lib/chef-vpc-toolkit/chef_bootstrap/fedora.bash +41 -0
- data/lib/chef-vpc-toolkit/chef_bootstrap/rhel.bash +38 -0
- data/lib/chef-vpc-toolkit/chef_bootstrap/ubuntu.bash +32 -0
- data/lib/chef-vpc-toolkit/chef_installer.rb +276 -0
- data/lib/chef-vpc-toolkit/cloud_files.bash +67 -0
- data/lib/chef-vpc-toolkit/cloud_servers_vpc.rb +285 -0
- data/lib/chef-vpc-toolkit/http_util.rb +117 -0
- data/lib/chef-vpc-toolkit/ssh_util.rb +22 -0
- data/lib/chef-vpc-toolkit/util.rb +56 -0
- data/lib/chef-vpc-toolkit/version.rb +8 -0
- data/rake/chef_vpc_toolkit.rake +284 -0
- data/test/cloud_servers_vpc_test.rb +174 -0
- data/test/test_helper.rb +25 -0
- metadata +153 -0
@@ -0,0 +1,10 @@
|
|
1
|
+
log_level :info
|
2
|
+
log_location STDOUT
|
3
|
+
node_name 'chef_admin'
|
4
|
+
client_key '/home/chef_admin/.chef/chef_admin.pem'
|
5
|
+
validation_client_name 'chef-validator'
|
6
|
+
validation_key '/home/chef_admin/.chef/chef-validator.pem'
|
7
|
+
chef_server_url 'http://chef.example.com:4000'
|
8
|
+
cache_type 'BasicFile'
|
9
|
+
cache_options( :path => '/home/chef_admin/.chef/checksums' )
|
10
|
+
cookbook_path [ './cookbooks', './site-cookbooks' ]
|
@@ -0,0 +1,60 @@
|
|
1
|
+
###
|
2
|
+
# Company and SSL Details
|
3
|
+
###
|
4
|
+
|
5
|
+
# The company name - used for SSL certificates, and in srvious other places
|
6
|
+
COMPANY_NAME = ""
|
7
|
+
|
8
|
+
# The Country Name to use for SSL Certificates
|
9
|
+
SSL_COUNTRY_NAME = ""
|
10
|
+
|
11
|
+
# The State Name to use for SSL Certificates
|
12
|
+
SSL_STATE_NAME = ""
|
13
|
+
|
14
|
+
# The Locality Name for SSL - typically, the city
|
15
|
+
SSL_LOCALITY_NAME = ""
|
16
|
+
|
17
|
+
# What department?
|
18
|
+
SSL_ORGANIZATIONAL_UNIT_NAME = ""
|
19
|
+
|
20
|
+
# The SSL contact email address
|
21
|
+
SSL_EMAIL_ADDRESS = ""
|
22
|
+
|
23
|
+
# License for new Cookbooks
|
24
|
+
# Can be :apachev2 or :none
|
25
|
+
NEW_COOKBOOK_LICENSE = :none
|
26
|
+
|
27
|
+
##########################
|
28
|
+
# Chef Repository Layout #
|
29
|
+
##########################
|
30
|
+
|
31
|
+
# Where to install upstream cookbooks for serving
|
32
|
+
COOKBOOK_PATH = "/srv/chef/cookbooks"
|
33
|
+
|
34
|
+
# Where to install site-local modifications to upstream cookbooks
|
35
|
+
SITE_COOKBOOK_PATH = "/srv/chef/site-cookbooks"
|
36
|
+
|
37
|
+
# Where to install roles
|
38
|
+
ROLE_PATH = "/srv/chef/roles"
|
39
|
+
|
40
|
+
# Chef Config Path
|
41
|
+
CHEF_CONFIG_PATH = "/etc/chef"
|
42
|
+
|
43
|
+
# The location of the Chef Server Config file (on the server)
|
44
|
+
CHEF_SERVER_CONFIG = File.join(CHEF_CONFIG_PATH, "server.rb")
|
45
|
+
|
46
|
+
# The location of the Chef Client Config file (on the client)
|
47
|
+
CHEF_CLIENT_CONFIG = File.join(CHEF_CONFIG_PATH, "client.rb")
|
48
|
+
|
49
|
+
###
|
50
|
+
# Useful Extras (which you probably don't need to change)
|
51
|
+
###
|
52
|
+
|
53
|
+
# The top of the repository checkout
|
54
|
+
TOPDIR = File.expand_path(File.join(File.dirname(__FILE__), ".."))
|
55
|
+
|
56
|
+
# Where to store certificates generated with ssl_cert
|
57
|
+
CADIR = File.expand_path(File.join(TOPDIR, "certificates"))
|
58
|
+
|
59
|
+
# Where to store the mtime cache for the recipe/template syntax check
|
60
|
+
TEST_CACHE = File.expand_path(File.join(TOPDIR, ".rake_test_cache"))
|
@@ -0,0 +1,42 @@
|
|
1
|
+
#
|
2
|
+
# Chef Server Config File
|
3
|
+
#
|
4
|
+
# We recommend using Opscode's chef cookbook for managing chef itself,
|
5
|
+
# instead of using this file. It is provided as an example.
|
6
|
+
|
7
|
+
log_level :info
|
8
|
+
log_location STDOUT
|
9
|
+
ssl_verify_mode :verify_none
|
10
|
+
chef_server_url "http://chef.example.com:4000"
|
11
|
+
|
12
|
+
signing_ca_path "/srv/chef/ca"
|
13
|
+
couchdb_database 'chef'
|
14
|
+
|
15
|
+
cookbook_path [ "/srv/chef/cookbooks", "/srv/chef/site-cookbooks" ]
|
16
|
+
|
17
|
+
file_cache_path "/srv/chef/cache"
|
18
|
+
node_path "/srv/chef/nodes"
|
19
|
+
openid_store_path "/srv/chef/openid/store"
|
20
|
+
openid_cstore_path "/srv/chef/openid/cstore"
|
21
|
+
search_index_path "/srv/chef/search_index"
|
22
|
+
role_path "/srv/chef/roles"
|
23
|
+
|
24
|
+
validation_client_name "chef-validator"
|
25
|
+
validation_key "/etc/chef/validation.pem"
|
26
|
+
client_key "/etc/chef/client.pem"
|
27
|
+
web_ui_client_name "chef-webui"
|
28
|
+
web_ui_key "/etc/chef/webui.pem"
|
29
|
+
|
30
|
+
# change this as required.
|
31
|
+
#web_ui_admin_user_name "admin"
|
32
|
+
#web_ui_admin_default_password "replace_with_something_secure"
|
33
|
+
|
34
|
+
supportdir = "/srv/chef/support"
|
35
|
+
solr_jetty_path File.join(supportdir, "solr", "jetty")
|
36
|
+
solr_data_path File.join(supportdir, "solr", "data")
|
37
|
+
solr_home_path File.join(supportdir, "solr", "home")
|
38
|
+
solr_heap_size "256M"
|
39
|
+
|
40
|
+
umask 0022
|
41
|
+
|
42
|
+
Mixlib::Log::Formatter.show_time = false
|
@@ -0,0 +1,13 @@
|
|
1
|
+
#
|
2
|
+
# Chef Solo Config File
|
3
|
+
#
|
4
|
+
|
5
|
+
log_level :info
|
6
|
+
log_location STDOUT
|
7
|
+
file_cache_path "/var/chef/cookbooks"
|
8
|
+
|
9
|
+
# Optionally store your JSON data file and a tarball of cookbooks remotely.
|
10
|
+
#json_attribs "http://chef.example.com/dna.json"
|
11
|
+
#recipe_url "http://chef.example.com/cookbooks.tar.gz"
|
12
|
+
|
13
|
+
Mixlib::Log::Formatter.show_time = false
|
@@ -0,0 +1 @@
|
|
1
|
+
set_unless[:motd]= "It works!"
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= @node[:motd] %>
|
@@ -0,0 +1,232 @@
|
|
1
|
+
# Installation functions for Chef 0.8 RPMs obtained from the ELFF repo.
|
2
|
+
|
3
|
+
function configure_chef_server {
|
4
|
+
|
5
|
+
echo ""
|
6
|
+
|
7
|
+
}
|
8
|
+
|
9
|
+
function print_client_validation_key {
|
10
|
+
cat /etc/chef/validation.pem
|
11
|
+
}
|
12
|
+
|
13
|
+
function configure_chef_client {
|
14
|
+
|
15
|
+
if (( $# != 2 )); then
|
16
|
+
echo "Unable to configure chef client."
|
17
|
+
echo "usage: configure_chef_client <server_name> <client_validation_key>"
|
18
|
+
exit 1
|
19
|
+
fi
|
20
|
+
|
21
|
+
local SERVER_NAME=$1
|
22
|
+
local CLIENT_VALIDATION_KEY=$2
|
23
|
+
|
24
|
+
if [ ! -f "/etc/chef/validation.pem" ]; then
|
25
|
+
cat > /etc/chef/validation.pem <<-EOF_VALIDATION_PEM
|
26
|
+
$CLIENT_VALIDATION_KEY
|
27
|
+
EOF_VALIDATION_PEM
|
28
|
+
sed -e "/^$/d" -i /etc/chef/validation.pem
|
29
|
+
fi
|
30
|
+
|
31
|
+
sed -e "s|localhost|$SERVER_NAME|g" -i /etc/chef/client.rb
|
32
|
+
sed -e "s|^chef_server_url.*|chef_server_url \"http://$SERVER_NAME:4000\"|g" -i /etc/chef/client.rb
|
33
|
+
|
34
|
+
local CHEF_CLIENT_CONF=/etc/default/chef-client
|
35
|
+
[ -d /etc/sysconfig/ ] && CHEF_CLIENT_CONF=/etc/sysconfig/chef-client
|
36
|
+
cat > $CHEF_CLIENT_CONF <<-"EOF_CAT_CHEF_CLIENT_CONF"
|
37
|
+
INTERVAL=600
|
38
|
+
SPLAY=20
|
39
|
+
CONFIG=/etc/chef/client.rb
|
40
|
+
LOGFILE=/var/log/chef/client.log
|
41
|
+
EOF_CAT_CHEF_CLIENT_CONF
|
42
|
+
|
43
|
+
}
|
44
|
+
|
45
|
+
# This function will only run on the Chef Server for initial registration
|
46
|
+
function configure_knife {
|
47
|
+
|
48
|
+
local KNIFE_EDITOR=${1:-"vim"}
|
49
|
+
|
50
|
+
[ ! -f $HOME/.chef/chef-admin.pem ] || { echo "Knife already configured."; return 0; }
|
51
|
+
|
52
|
+
local COUNT=0
|
53
|
+
until [ -f /etc/chef/webui.pem ]; do
|
54
|
+
echo "waiting for /etc/chef/webui.pem"
|
55
|
+
sleep 1
|
56
|
+
COUNT=$(( $COUNT + 1 ))
|
57
|
+
if (( $COUNT > 30 )); then
|
58
|
+
echo "timeout waiting for /etc/chef/webui.pem"
|
59
|
+
exit 1
|
60
|
+
break;
|
61
|
+
fi
|
62
|
+
done
|
63
|
+
cd /tmp
|
64
|
+
/usr/bin/knife configure -i -s "http://localhost:4000" -u "chef-admin" -r "/root/cookbook-repos/chef-repo/" -y -d \
|
65
|
+
|| { echo "Failed to configure knife."; exit 1; }
|
66
|
+
|
67
|
+
cat > /etc/profile.d/knife.sh <<-EOF_CAT_KNIFE_SH
|
68
|
+
alias knife='EDITOR=$KNIFE_EDITOR knife'
|
69
|
+
EOF_CAT_KNIFE_SH
|
70
|
+
|
71
|
+
cat > /etc/profile.d/knife.csh <<-EOF_CAT_KNIFE_CSH
|
72
|
+
alias knife '/usr/bin/env EDITOR=$KNIFE_EDITOR knife'
|
73
|
+
EOF_CAT_KNIFE_CSH
|
74
|
+
chown root:root /etc/profile.d/knife*
|
75
|
+
chmod 755 /etc/profile.d/knife*
|
76
|
+
|
77
|
+
}
|
78
|
+
|
79
|
+
function knife_add_node {
|
80
|
+
|
81
|
+
if (( $# != 3 )); then
|
82
|
+
echo "Unable to add node with knife."
|
83
|
+
echo "usage: knife_add_node <node_name> <run_list> <json_attributes>"
|
84
|
+
exit 1
|
85
|
+
fi
|
86
|
+
|
87
|
+
local NODE_NAME=$1
|
88
|
+
local RUN_LIST=$2
|
89
|
+
local ATTRIBUTES_JSON=$3
|
90
|
+
|
91
|
+
local DOMAIN_NAME=$(hostname -d)
|
92
|
+
local TMP_FILE=/tmp/node.json
|
93
|
+
|
94
|
+
cat > $TMP_FILE <<-EOF_CAT_CHEF_CLIENT_CONF
|
95
|
+
{
|
96
|
+
"overrides": {
|
97
|
+
|
98
|
+
},
|
99
|
+
"name": "$NODE_NAME.$DOMAIN_NAME",
|
100
|
+
"chef_type": "node",
|
101
|
+
"json_class": "Chef::Node",
|
102
|
+
"attributes": $ATTRIBUTES_JSON,
|
103
|
+
"run_list": $RUN_LIST,
|
104
|
+
"defaults": {
|
105
|
+
|
106
|
+
}
|
107
|
+
}
|
108
|
+
EOF_CAT_CHEF_CLIENT_CONF
|
109
|
+
|
110
|
+
knife node from file $TMP_FILE 1> /dev/null || \
|
111
|
+
{ echo "Failed to add node with knife."; exit 1; }
|
112
|
+
|
113
|
+
rm $TMP_FILE
|
114
|
+
|
115
|
+
}
|
116
|
+
|
117
|
+
function knife_delete_node {
|
118
|
+
|
119
|
+
if (( $# != 1 )); then
|
120
|
+
echo "Unable to add node with knife."
|
121
|
+
echo "usage: knife_delete_node <node_name>"
|
122
|
+
exit 1
|
123
|
+
fi
|
124
|
+
|
125
|
+
local NODE_NAME=$1
|
126
|
+
local DOMAIN_NAME=$(hostname -d)
|
127
|
+
|
128
|
+
knife node delete "$NODE_NAME.$DOMAIN_NAME" -y &> /dev/null || \
|
129
|
+
{ echo "Failed to delete node with knife. Ignoring..."; }
|
130
|
+
knife client delete "$NODE_NAME.$DOMAIN_NAME" -y &> /dev/null || \
|
131
|
+
{ echo "Failed to delete client with knife. Ignoring..."; }
|
132
|
+
|
133
|
+
}
|
134
|
+
|
135
|
+
function knife_create_databag {
|
136
|
+
|
137
|
+
if (( $# != 3 )); then
|
138
|
+
echo "Unable to create databag with knife."
|
139
|
+
echo "usage: knife_create_databag <bag_name> <item_id> <item_json>"
|
140
|
+
exit 1
|
141
|
+
fi
|
142
|
+
|
143
|
+
local BAG_NAME=$1
|
144
|
+
local ITEM_ID=$2
|
145
|
+
local ITEM_JSON=$3
|
146
|
+
|
147
|
+
local TMP_FILE=/tmp/databag.json
|
148
|
+
|
149
|
+
cat > $TMP_FILE <<-EOF_CAT_CHEF_DATA_BAG
|
150
|
+
$ITEM_JSON
|
151
|
+
EOF_CAT_CHEF_DATA_BAG
|
152
|
+
|
153
|
+
knife data bag from file $BAG_NAME $TMP_FILE 1> /dev/null || \
|
154
|
+
{ echo "Failed to create data bag with knife."; exit 1; }
|
155
|
+
|
156
|
+
rm $TMP_FILE
|
157
|
+
|
158
|
+
}
|
159
|
+
|
160
|
+
function download_cookbook_repos {
|
161
|
+
|
162
|
+
local COOKBOOK_URLS=${1:?"Please specify a list of cookbook repos to download."}
|
163
|
+
local REPOS_BASEDIR=${2:-"/root/cookbook-repos"}
|
164
|
+
|
165
|
+
# download and extract the cookbooks
|
166
|
+
for CB_REPO in $COOKBOOK_URLS; do
|
167
|
+
echo -n "Downloading $CB_REPO..."
|
168
|
+
if [ "http:" == ${CB_REPO:0:5} ] || [ "https:" == ${CB_REPO:0:6} ]; then
|
169
|
+
wget "$CB_REPO" -O "/tmp/cookbook-repo.tar.gz" &> /dev/null || { echo "Failed to download cookbook tarball."; return 1; }
|
170
|
+
else
|
171
|
+
download_cloud_file "$CB_REPO" "/tmp/cookbook-repo.tar.gz"
|
172
|
+
fi
|
173
|
+
echo "OK"
|
174
|
+
cd $REPOS_BASEDIR
|
175
|
+
echo -n "Extracting $CB_REPO..."
|
176
|
+
tar xzf /tmp/cookbook-repo.tar.gz
|
177
|
+
rm /tmp/cookbook-repo.tar.gz
|
178
|
+
echo "OK"
|
179
|
+
done
|
180
|
+
|
181
|
+
}
|
182
|
+
|
183
|
+
function knife_upload_cookbooks_and_roles {
|
184
|
+
|
185
|
+
local REPOS_BASEDIR=${1:-"/root/cookbook-repos"}
|
186
|
+
|
187
|
+
# install cookbooks
|
188
|
+
local REPOS=""
|
189
|
+
for CB_REPO in $(ls $REPOS_BASEDIR); do
|
190
|
+
[ -n "$REPOS" ] && REPOS="$REPOS,"
|
191
|
+
REPOS="$REPOS'$REPOS_BASEDIR/$CB_REPO/cookbooks', '$REPOS_BASEDIR/$CB_REPO/site-cookbooks'"
|
192
|
+
done
|
193
|
+
sed -e "s|^cookbook_path.*|cookbook_path [ $REPOS ]|" -i $HOME/.chef/knife.rb
|
194
|
+
/usr/bin/knife cookbook metadata -a &> /dev/null || { echo "Failed to generate cookbook metadata."; exit 1; }
|
195
|
+
/usr/bin/knife cookbook upload -a &> /dev/null || { echo "Failed to install cookbooks."; exit 1; }
|
196
|
+
|
197
|
+
# install roles
|
198
|
+
for CB_REPO in $(ls $REPOS_BASEDIR); do
|
199
|
+
for ROLE in $(ls $REPOS_BASEDIR/$CB_REPO/roles/); do
|
200
|
+
[[ "$ROLE" == "README" ]] || \
|
201
|
+
/usr/bin/knife role from file "$REPOS_BASEDIR/$CB_REPO/roles/$ROLE" 1> /dev/null
|
202
|
+
done
|
203
|
+
done
|
204
|
+
|
205
|
+
}
|
206
|
+
|
207
|
+
function start_chef_server {
|
208
|
+
|
209
|
+
# Ubuntu starts the Chef server automatically
|
210
|
+
if [ -f /bin/rpm ]; then
|
211
|
+
/sbin/service couchdb start 1> /dev/null
|
212
|
+
/sbin/chkconfig couchdb on
|
213
|
+
/sbin/service rabbitmq-server start </dev/null &> /dev/null
|
214
|
+
/sbin/chkconfig rabbitmq-server on
|
215
|
+
|
216
|
+
for svc in chef-solr chef-solr-indexer chef-server chef-server-webui
|
217
|
+
do
|
218
|
+
/sbin/service $svc start
|
219
|
+
/sbin/chkconfig $svc on
|
220
|
+
done
|
221
|
+
fi
|
222
|
+
|
223
|
+
}
|
224
|
+
|
225
|
+
function start_chef_client {
|
226
|
+
|
227
|
+
/etc/init.d/chef-client start
|
228
|
+
if [ -f /sbin/chkconfig ]; then
|
229
|
+
chkconfig chef-client on
|
230
|
+
fi
|
231
|
+
|
232
|
+
}
|
@@ -0,0 +1,47 @@
|
|
1
|
+
function install_chef {
|
2
|
+
|
3
|
+
local INSTALL_TYPE=${1:-"CLIENT"} # CLIENT/SERVER
|
4
|
+
|
5
|
+
# cached RPMs from ELFF
|
6
|
+
local CDN_BASE="http://c2521002.cdn.cloudfiles.rackspacecloud.com"
|
7
|
+
|
8
|
+
local RH_RELEASE=$(cat /etc/redhat-release)
|
9
|
+
local TARBALL="chef-client-0.9.8-centos5.4-x86_64.tar.gz"
|
10
|
+
|
11
|
+
if [ "$RH_RELEASE" == "CentOS release 5.5 (Final)" ]; then
|
12
|
+
TARBALL="chef-client-0.9.8-centos5.5-x86_64.tar.gz"
|
13
|
+
if [[ "$INSTALL_TYPE" == "SERVER" ]]; then
|
14
|
+
TARBALL="chef-server-0.9.8-centos5.5-x86_64.tar.gz"
|
15
|
+
fi
|
16
|
+
else
|
17
|
+
if [[ "$INSTALL_TYPE" == "SERVER" ]]; then
|
18
|
+
TARBALL="chef-server-0.9.8-centos5.4-x86_64.tar.gz"
|
19
|
+
fi
|
20
|
+
fi
|
21
|
+
|
22
|
+
rpm -q rsync &> /dev/null || yum install -y -q rsync
|
23
|
+
rpm -q wget &> /dev/null || yum install -y -q wget
|
24
|
+
|
25
|
+
if ! rpm -q rubygem-chef &> /dev/null; then
|
26
|
+
|
27
|
+
local CHEF_RPM_DIR=$(mktemp -d)
|
28
|
+
|
29
|
+
wget "$CDN_BASE/$TARBALL" -O "$CHEF_RPM_DIR/chef.tar.gz" &> /dev/null \
|
30
|
+
|| { echo "Failed to download Chef RPM tarball."; exit 1; }
|
31
|
+
cd $CHEF_RPM_DIR
|
32
|
+
|
33
|
+
tar xzf chef.tar.gz || { echo "Failed to extract Chef tarball."; exit 1; }
|
34
|
+
rm chef.tar.gz
|
35
|
+
cd chef*
|
36
|
+
yum install -q -y --nogpgcheck */*.rpm
|
37
|
+
if [[ "$INSTALL_TYPE" == "SERVER" ]]; then
|
38
|
+
rpm -q rubygem-chef-server &> /dev/null || { echo "Failed to install chef."; exit 1; }
|
39
|
+
else
|
40
|
+
rpm -q rubygem-chef &> /dev/null || { echo "Failed to install chef."; exit 1; }
|
41
|
+
fi
|
42
|
+
cd /tmp
|
43
|
+
rm -Rf "$CHEF_RPM_DIR"
|
44
|
+
|
45
|
+
fi
|
46
|
+
|
47
|
+
}
|