Merge lp:~hopem/charms/trusty/mysql/contrib-database-mysql into lp:charms/trusty/mysql

Proposed by Edward Hope-Morley
Status: Merged
Merged at revision: 137
Proposed branch: lp:~hopem/charms/trusty/mysql/contrib-database-mysql
Merge into: lp:charms/trusty/mysql
Diff against target: 1602 lines (+1001/-191)
24 files modified
charm-helpers.yaml (+2/-0)
hooks/charmhelpers/__init__.py (+16/-0)
hooks/charmhelpers/contrib/__init__.py (+15/-0)
hooks/charmhelpers/contrib/database/mysql.py (+372/-0)
hooks/charmhelpers/contrib/network/__init__.py (+15/-0)
hooks/charmhelpers/contrib/network/ip.py (+16/-0)
hooks/charmhelpers/contrib/peerstorage/__init__.py (+148/-0)
hooks/charmhelpers/core/__init__.py (+15/-0)
hooks/charmhelpers/core/decorators.py (+57/-0)
hooks/charmhelpers/core/fstab.py (+16/-0)
hooks/charmhelpers/core/hookenv.py (+32/-4)
hooks/charmhelpers/core/host.py (+59/-9)
hooks/charmhelpers/core/services/__init__.py (+16/-0)
hooks/charmhelpers/core/services/base.py (+16/-0)
hooks/charmhelpers/core/services/helpers.py (+16/-0)
hooks/charmhelpers/core/sysctl.py (+27/-5)
hooks/charmhelpers/core/templating.py (+19/-3)
hooks/charmhelpers/fetch/__init__.py (+24/-1)
hooks/charmhelpers/fetch/archiveurl.py (+16/-0)
hooks/charmhelpers/fetch/bzrurl.py (+25/-1)
hooks/charmhelpers/fetch/giturl.py (+26/-3)
hooks/common.py (+8/-59)
hooks/ha_relations.py (+10/-0)
hooks/shared_db_relations.py (+35/-106)
To merge this branch: bzr merge lp:~hopem/charms/trusty/mysql/contrib-database-mysql
Reviewer Review Type Date Requested Status
Liam Young (community) Approve
Cory Johns (community) Approve
Review Queue (community) automated testing Needs Fixing
Review via email: mp+248743@code.launchpad.net
To post a comment you must log in.
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_unit_test #1560 mysql for hopem mp248743
    UNIT FAIL: unit-test missing

UNIT Results (max last 2 lines):
INFO:root:Search string not found in makefile target commands.
ERROR:root:No make target was executed.

Full unit test output: http://paste.ubuntu.com/10072413/
Build: http://10.245.162.77:8080/job/charm_unit_test/1560/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_lint_check #1732 mysql for hopem mp248743
    LINT OK: passed

Build: http://10.245.162.77:8080/job/charm_lint_check/1732/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_amulet_test #1663 mysql for hopem mp248743
    AMULET FAIL: amulet-test missing

AMULET Results (max last 2 lines):
INFO:root:Search string not found in makefile target commands.
ERROR:root:No make target was executed.

Full amulet test output: http://paste.ubuntu.com/10072425/
Build: http://10.245.162.77:8080/job/charm_amulet_test/1663/

146. By Edward Hope-Morley

fix charm-helpers yaml

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_lint_check #1834 mysql for hopem mp248743
    LINT OK: passed

Build: http://10.245.162.77:8080/job/charm_lint_check/1834/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_unit_test #1662 mysql for hopem mp248743
    UNIT FAIL: unit-test missing

UNIT Results (max last 2 lines):
INFO:root:Search string not found in makefile target commands.
ERROR:root:No make target was executed.

Full unit test output: http://paste.ubuntu.com/10157219/
Build: http://10.245.162.77:8080/job/charm_unit_test/1662/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_amulet_test #1853 mysql for hopem mp248743
    AMULET FAIL: amulet-test missing

AMULET Results (max last 2 lines):
INFO:root:Search string not found in makefile target commands.
ERROR:root:No make target was executed.

Full amulet test output: http://paste.ubuntu.com/10157234/
Build: http://10.245.162.77:8080/job/charm_amulet_test/1853/

Revision history for this message
Review Queue (review-queue) wrote :

This items has failed automated testing! Results available here http://reports.vapour.ws/charm-tests/charm-bundle-test-11006-results

review: Needs Fixing (automated testing)
Revision history for this message
Cory Johns (johnsca) wrote :

The automated test failure is only for Azure and seems spurious. I manually tested this relation with cs:trusty/lamp and it seems fine. +1

review: Approve
Revision history for this message
Liam Young (gnuoy) wrote :

Approve

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'charm-helpers.yaml'
--- charm-helpers.yaml 2014-12-01 17:17:12 +0000
+++ charm-helpers.yaml 2015-02-10 11:19:03 +0000
@@ -5,3 +5,5 @@
5 - core5 - core
6 - fetch6 - fetch
7 - contrib.network.ip7 - contrib.network.ip
8 - contrib.database
9 - contrib.peerstorage
810
=== modified file 'hooks/charmhelpers/__init__.py'
--- hooks/charmhelpers/__init__.py 2014-12-01 17:17:12 +0000
+++ hooks/charmhelpers/__init__.py 2015-02-10 11:19:03 +0000
@@ -1,3 +1,19 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
1# Bootstrap charm-helpers, installing its dependencies if necessary using17# Bootstrap charm-helpers, installing its dependencies if necessary using
2# only standard libraries.18# only standard libraries.
3import subprocess19import subprocess
420
=== modified file 'hooks/charmhelpers/contrib/__init__.py'
--- hooks/charmhelpers/contrib/__init__.py 2014-05-08 10:22:43 +0000
+++ hooks/charmhelpers/contrib/__init__.py 2015-02-10 11:19:03 +0000
@@ -0,0 +1,15 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
016
=== added directory 'hooks/charmhelpers/contrib/database'
=== added file 'hooks/charmhelpers/contrib/database/__init__.py'
=== added file 'hooks/charmhelpers/contrib/database/mysql.py'
--- hooks/charmhelpers/contrib/database/mysql.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/database/mysql.py 2015-02-10 11:19:03 +0000
@@ -0,0 +1,372 @@
1"""Helper for working with a MySQL database"""
2import json
3import socket
4import re
5import sys
6import platform
7import os
8import glob
9
10from string import upper
11
12from charmhelpers.core.host import (
13 mkdir,
14 pwgen,
15 write_file
16)
17from charmhelpers.core.hookenv import (
18 relation_get,
19 related_units,
20 unit_get,
21 log,
22 DEBUG,
23 INFO,
24)
25from charmhelpers.core.hookenv import config as config_get
26from charmhelpers.fetch import (
27 apt_install,
28 apt_update,
29 filter_installed_packages,
30)
31from charmhelpers.contrib.peerstorage import (
32 peer_store,
33 peer_retrieve,
34)
35
36try:
37 import MySQLdb
38except ImportError:
39 apt_update(fatal=True)
40 apt_install(filter_installed_packages(['python-mysqldb']), fatal=True)
41 import MySQLdb
42
43
44class MySQLHelper(object):
45
46 def __init__(self, rpasswdf_template, upasswdf_template, host='localhost'):
47 self.host = host
48 # Password file path templates
49 self.root_passwd_file_template = rpasswdf_template
50 self.user_passwd_file_template = upasswdf_template
51
52 def connect(self, user='root', password=None):
53 self.connection = MySQLdb.connect(user=user, host=self.host,
54 passwd=password)
55
56 def database_exists(self, db_name):
57 cursor = self.connection.cursor()
58 try:
59 cursor.execute("SHOW DATABASES")
60 databases = [i[0] for i in cursor.fetchall()]
61 finally:
62 cursor.close()
63
64 return db_name in databases
65
66 def create_database(self, db_name):
67 cursor = self.connection.cursor()
68 try:
69 cursor.execute("CREATE DATABASE {} CHARACTER SET UTF8"
70 .format(db_name))
71 finally:
72 cursor.close()
73
74 def grant_exists(self, db_name, db_user, remote_ip):
75 cursor = self.connection.cursor()
76 priv_string = "GRANT ALL PRIVILEGES ON `{}`.* " \
77 "TO '{}'@'{}'".format(db_name, db_user, remote_ip)
78 try:
79 cursor.execute("SHOW GRANTS for '{}'@'{}'".format(db_user,
80 remote_ip))
81 grants = [i[0] for i in cursor.fetchall()]
82 except MySQLdb.OperationalError:
83 return False
84 finally:
85 cursor.close()
86
87 # TODO: review for different grants
88 return priv_string in grants
89
90 def create_grant(self, db_name, db_user, remote_ip, password):
91 cursor = self.connection.cursor()
92 try:
93 # TODO: review for different grants
94 cursor.execute("GRANT ALL PRIVILEGES ON {}.* TO '{}'@'{}' "
95 "IDENTIFIED BY '{}'".format(db_name,
96 db_user,
97 remote_ip,
98 password))
99 finally:
100 cursor.close()
101
102 def create_admin_grant(self, db_user, remote_ip, password):
103 cursor = self.connection.cursor()
104 try:
105 cursor.execute("GRANT ALL PRIVILEGES ON *.* TO '{}'@'{}' "
106 "IDENTIFIED BY '{}'".format(db_user,
107 remote_ip,
108 password))
109 finally:
110 cursor.close()
111
112 def cleanup_grant(self, db_user, remote_ip):
113 cursor = self.connection.cursor()
114 try:
115 cursor.execute("DROP FROM mysql.user WHERE user='{}' "
116 "AND HOST='{}'".format(db_user,
117 remote_ip))
118 finally:
119 cursor.close()
120
121 def execute(self, sql):
122 """Execute arbitary SQL against the database."""
123 cursor = self.connection.cursor()
124 try:
125 cursor.execute(sql)
126 finally:
127 cursor.close()
128
129 def migrate_passwords_to_peer_relation(self):
130 """Migrate any passwords storage on disk to cluster peer relation."""
131 dirname = os.path.dirname(self.root_passwd_file_template)
132 path = os.path.join(dirname, '*.passwd')
133 for f in glob.glob(path):
134 _key = os.path.basename(f)
135 with open(f, 'r') as passwd:
136 _value = passwd.read().strip()
137
138 try:
139 peer_store(_key, _value)
140 os.unlink(f)
141 except ValueError:
142 # NOTE cluster relation not yet ready - skip for now
143 pass
144
145 def get_mysql_password_on_disk(self, username=None, password=None):
146 """Retrieve, generate or store a mysql password for the provided
147 username on disk."""
148 if username:
149 template = self.user_passwd_file_template
150 passwd_file = template.format(username)
151 else:
152 passwd_file = self.root_passwd_file_template
153
154 _password = None
155 if os.path.exists(passwd_file):
156 with open(passwd_file, 'r') as passwd:
157 _password = passwd.read().strip()
158 else:
159 mkdir(os.path.dirname(passwd_file), owner='root', group='root',
160 perms=0o770)
161 # Force permissions - for some reason the chmod in makedirs fails
162 os.chmod(os.path.dirname(passwd_file), 0o770)
163 _password = password or pwgen(length=32)
164 write_file(passwd_file, _password, owner='root', group='root',
165 perms=0o660)
166
167 return _password
168
169 def get_mysql_password(self, username=None, password=None):
170 """Retrieve, generate or store a mysql password for the provided
171 username using peer relation cluster."""
172 self.migrate_passwords_to_peer_relation()
173 if username:
174 _key = 'mysql-{}.passwd'.format(username)
175 else:
176 _key = 'mysql.passwd'
177
178 try:
179 _password = peer_retrieve(_key)
180 if _password is None:
181 _password = password or pwgen(length=32)
182 peer_store(_key, _password)
183 except ValueError:
184 # cluster relation is not yet started; use on-disk
185 _password = self.get_mysql_password_on_disk(username, password)
186
187 return _password
188
189 def get_mysql_root_password(self, password=None):
190 """Retrieve or generate mysql root password for service units."""
191 return self.get_mysql_password(username=None, password=password)
192
193 def get_allowed_units(self, database, username, relation_id=None):
194 """Get list of units with access grants for database with username.
195
196 This is typically used to provide shared-db relations with a list of
197 which units have been granted access to the given database.
198 """
199 self.connect(password=self.get_mysql_root_password())
200 allowed_units = set()
201 for unit in related_units(relation_id):
202 settings = relation_get(rid=relation_id, unit=unit)
203 # First check for setting with prefix, then without
204 for attr in ["%s_hostname" % (database), 'hostname']:
205 hosts = settings.get(attr, None)
206 if hosts:
207 break
208
209 if hosts:
210 # hostname can be json-encoded list of hostnames
211 try:
212 hosts = json.loads(hosts)
213 except ValueError:
214 hosts = [hosts]
215 else:
216 hosts = [settings['private-address']]
217
218 if hosts:
219 for host in hosts:
220 if self.grant_exists(database, username, host):
221 log("Grant exists for host '%s' on db '%s'" %
222 (host, database), level=DEBUG)
223 if unit not in allowed_units:
224 allowed_units.add(unit)
225 else:
226 log("Grant does NOT exist for host '%s' on db '%s'" %
227 (host, database), level=DEBUG)
228 else:
229 log("No hosts found for grant check", level=INFO)
230
231 return allowed_units
232
233 def configure_db(self, hostname, database, username, admin=False):
234 """Configure access to database for username from hostname."""
235 if config_get('prefer-ipv6'):
236 remote_ip = hostname
237 elif hostname != unit_get('private-address'):
238 try:
239 remote_ip = socket.gethostbyname(hostname)
240 except Exception:
241 # socket.gethostbyname doesn't support ipv6
242 remote_ip = hostname
243 else:
244 remote_ip = '127.0.0.1'
245
246 self.connect(password=self.get_mysql_root_password())
247 if not self.database_exists(database):
248 self.create_database(database)
249
250 password = self.get_mysql_password(username)
251 if not self.grant_exists(database, username, remote_ip):
252 if not admin:
253 self.create_grant(database, username, remote_ip, password)
254 else:
255 self.create_admin_grant(username, remote_ip, password)
256
257 return password
258
259
260class PerconaClusterHelper(object):
261
262 # Going for the biggest page size to avoid wasted bytes. InnoDB page size is
263 # 16MB
264 DEFAULT_PAGE_SIZE = 16 * 1024 * 1024
265
266 def human_to_bytes(self, human):
267 """Convert human readable configuration options to bytes."""
268 num_re = re.compile('^[0-9]+$')
269 if num_re.match(human):
270 return human
271
272 factors = {
273 'K': 1024,
274 'M': 1048576,
275 'G': 1073741824,
276 'T': 1099511627776
277 }
278 modifier = human[-1]
279 if modifier in factors:
280 return int(human[:-1]) * factors[modifier]
281
282 if modifier == '%':
283 total_ram = self.human_to_bytes(self.get_mem_total())
284 if self.is_32bit_system() and total_ram > self.sys_mem_limit():
285 total_ram = self.sys_mem_limit()
286 factor = int(human[:-1]) * 0.01
287 pctram = total_ram * factor
288 return int(pctram - (pctram % self.DEFAULT_PAGE_SIZE))
289
290 raise ValueError("Can only convert K,M,G, or T")
291
292 def is_32bit_system(self):
293 """Determine whether system is 32 or 64 bit."""
294 try:
295 return sys.maxsize < 2 ** 32
296 except OverflowError:
297 return False
298
299 def sys_mem_limit(self):
300 """Determine the default memory limit for the current service unit."""
301 if platform.machine() in ['armv7l']:
302 _mem_limit = self.human_to_bytes('2700M') # experimentally determined
303 else:
304 # Limit for x86 based 32bit systems
305 _mem_limit = self.human_to_bytes('4G')
306
307 return _mem_limit
308
309 def get_mem_total(self):
310 """Calculate the total memory in the current service unit."""
311 with open('/proc/meminfo') as meminfo_file:
312 for line in meminfo_file:
313 key, mem = line.split(':', 2)
314 if key == 'MemTotal':
315 mtot, modifier = mem.strip().split(' ')
316 return '%s%s' % (mtot, upper(modifier[0]))
317
318 def parse_config(self):
319 """Parse charm configuration and calculate values for config files."""
320 config = config_get()
321 mysql_config = {}
322 if 'max-connections' in config:
323 mysql_config['max_connections'] = config['max-connections']
324
325 # Total memory available for dataset
326 dataset_bytes = self.human_to_bytes(config['dataset-size'])
327 mysql_config['dataset_bytes'] = dataset_bytes
328
329 if 'query-cache-type' in config:
330 # Query Cache Configuration
331 mysql_config['query_cache_size'] = config['query-cache-size']
332 if (config['query-cache-size'] == -1 and
333 config['query-cache-type'] in ['ON', 'DEMAND']):
334 # Calculate the query cache size automatically
335 qcache_bytes = (dataset_bytes * 0.20)
336 qcache_bytes = int(qcache_bytes -
337 (qcache_bytes % self.DEFAULT_PAGE_SIZE))
338 mysql_config['query_cache_size'] = qcache_bytes
339 dataset_bytes -= qcache_bytes
340
341 # 5.5 allows the words, but not 5.1
342 if config['query-cache-type'] == 'ON':
343 mysql_config['query_cache_type'] = 1
344 elif config['query-cache-type'] == 'DEMAND':
345 mysql_config['query_cache_type'] = 2
346 else:
347 mysql_config['query_cache_type'] = 0
348
349 # Set a sane default key_buffer size
350 mysql_config['key_buffer'] = self.human_to_bytes('32M')
351
352 if 'preferred-storage-engine' in config:
353 # Storage engine configuration
354 preferred_engines = config['preferred-storage-engine'].split(',')
355 chunk_size = int(dataset_bytes / len(preferred_engines))
356 mysql_config['innodb_flush_log_at_trx_commit'] = 1
357 mysql_config['sync_binlog'] = 1
358 if 'InnoDB' in preferred_engines:
359 mysql_config['innodb_buffer_pool_size'] = chunk_size
360 if config['tuning-level'] == 'fast':
361 mysql_config['innodb_flush_log_at_trx_commit'] = 2
362 else:
363 mysql_config['innodb_buffer_pool_size'] = 0
364
365 mysql_config['default_storage_engine'] = preferred_engines[0]
366 if 'MyISAM' in preferred_engines:
367 mysql_config['key_buffer'] = chunk_size
368
369 if config['tuning-level'] == 'fast':
370 mysql_config['sync_binlog'] = 0
371
372 return mysql_config
0373
=== modified file 'hooks/charmhelpers/contrib/network/__init__.py'
--- hooks/charmhelpers/contrib/network/__init__.py 2014-09-21 21:48:22 +0000
+++ hooks/charmhelpers/contrib/network/__init__.py 2015-02-10 11:19:03 +0000
@@ -0,0 +1,15 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
016
=== modified file 'hooks/charmhelpers/contrib/network/ip.py'
--- hooks/charmhelpers/contrib/network/ip.py 2014-11-26 12:31:33 +0000
+++ hooks/charmhelpers/contrib/network/ip.py 2015-02-10 11:19:03 +0000
@@ -1,3 +1,19 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
1import glob17import glob
2import re18import re
3import subprocess19import subprocess
420
=== added directory 'hooks/charmhelpers/contrib/peerstorage'
=== added file 'hooks/charmhelpers/contrib/peerstorage/__init__.py'
--- hooks/charmhelpers/contrib/peerstorage/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/peerstorage/__init__.py 2015-02-10 11:19:03 +0000
@@ -0,0 +1,148 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import six
18from charmhelpers.core.hookenv import relation_id as current_relation_id
19from charmhelpers.core.hookenv import (
20 is_relation_made,
21 relation_ids,
22 relation_get,
23 local_unit,
24 relation_set,
25)
26
27
28"""
29This helper provides functions to support use of a peer relation
30for basic key/value storage, with the added benefit that all storage
31can be replicated across peer units.
32
33Requirement to use:
34
35To use this, the "peer_echo()" method has to be called form the peer
36relation's relation-changed hook:
37
38@hooks.hook("cluster-relation-changed") # Adapt the to your peer relation name
39def cluster_relation_changed():
40 peer_echo()
41
42Once this is done, you can use peer storage from anywhere:
43
44@hooks.hook("some-hook")
45def some_hook():
46 # You can store and retrieve key/values this way:
47 if is_relation_made("cluster"): # from charmhelpers.core.hookenv
48 # There are peers available so we can work with peer storage
49 peer_store("mykey", "myvalue")
50 value = peer_retrieve("mykey")
51 print value
52 else:
53 print "No peers joind the relation, cannot share key/values :("
54"""
55
56
57def peer_retrieve(key, relation_name='cluster'):
58 """Retrieve a named key from peer relation `relation_name`."""
59 cluster_rels = relation_ids(relation_name)
60 if len(cluster_rels) > 0:
61 cluster_rid = cluster_rels[0]
62 return relation_get(attribute=key, rid=cluster_rid,
63 unit=local_unit())
64 else:
65 raise ValueError('Unable to detect'
66 'peer relation {}'.format(relation_name))
67
68
69def peer_retrieve_by_prefix(prefix, relation_name='cluster', delimiter='_',
70 inc_list=None, exc_list=None):
71 """ Retrieve k/v pairs given a prefix and filter using {inc,exc}_list """
72 inc_list = inc_list if inc_list else []
73 exc_list = exc_list if exc_list else []
74 peerdb_settings = peer_retrieve('-', relation_name=relation_name)
75 matched = {}
76 for k, v in peerdb_settings.items():
77 full_prefix = prefix + delimiter
78 if k.startswith(full_prefix):
79 new_key = k.replace(full_prefix, '')
80 if new_key in exc_list:
81 continue
82 if new_key in inc_list or len(inc_list) == 0:
83 matched[new_key] = v
84 return matched
85
86
87def peer_store(key, value, relation_name='cluster'):
88 """Store the key/value pair on the named peer relation `relation_name`."""
89 cluster_rels = relation_ids(relation_name)
90 if len(cluster_rels) > 0:
91 cluster_rid = cluster_rels[0]
92 relation_set(relation_id=cluster_rid,
93 relation_settings={key: value})
94 else:
95 raise ValueError('Unable to detect '
96 'peer relation {}'.format(relation_name))
97
98
99def peer_echo(includes=None):
100 """Echo filtered attributes back onto the same relation for storage.
101
102 This is a requirement to use the peerstorage module - it needs to be called
103 from the peer relation's changed hook.
104 """
105 rdata = relation_get()
106 echo_data = {}
107 if includes is None:
108 echo_data = rdata.copy()
109 for ex in ['private-address', 'public-address']:
110 if ex in echo_data:
111 echo_data.pop(ex)
112 else:
113 for attribute, value in six.iteritems(rdata):
114 for include in includes:
115 if include in attribute:
116 echo_data[attribute] = value
117 if len(echo_data) > 0:
118 relation_set(relation_settings=echo_data)
119
120
121def peer_store_and_set(relation_id=None, peer_relation_name='cluster',
122 peer_store_fatal=False, relation_settings=None,
123 delimiter='_', **kwargs):
124 """Store passed-in arguments both in argument relation and in peer storage.
125
126 It functions like doing relation_set() and peer_store() at the same time,
127 with the same data.
128
129 @param relation_id: the id of the relation to store the data on. Defaults
130 to the current relation.
131 @param peer_store_fatal: Set to True, the function will raise an exception
132 should the peer sotrage not be avialable."""
133
134 relation_settings = relation_settings if relation_settings else {}
135 relation_set(relation_id=relation_id,
136 relation_settings=relation_settings,
137 **kwargs)
138 if is_relation_made(peer_relation_name):
139 for key, value in six.iteritems(dict(list(kwargs.items()) +
140 list(relation_settings.items()))):
141 key_prefix = relation_id or current_relation_id()
142 peer_store(key_prefix + delimiter + key,
143 value,
144 relation_name=peer_relation_name)
145 else:
146 if peer_store_fatal:
147 raise ValueError('Unable to detect '
148 'peer relation {}'.format(peer_relation_name))
0149
=== modified file 'hooks/charmhelpers/core/__init__.py'
--- hooks/charmhelpers/core/__init__.py 2014-02-19 14:49:31 +0000
+++ hooks/charmhelpers/core/__init__.py 2015-02-10 11:19:03 +0000
@@ -0,0 +1,15 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
016
=== added file 'hooks/charmhelpers/core/decorators.py'
--- hooks/charmhelpers/core/decorators.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/decorators.py 2015-02-10 11:19:03 +0000
@@ -0,0 +1,57 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17#
18# Copyright 2014 Canonical Ltd.
19#
20# Authors:
21# Edward Hope-Morley <opentastic@gmail.com>
22#
23
24import time
25
26from charmhelpers.core.hookenv import (
27 log,
28 INFO,
29)
30
31
32def retry_on_exception(num_retries, base_delay=0, exc_type=Exception):
33 """If the decorated function raises exception exc_type, allow num_retries
34 retry attempts before raise the exception.
35 """
36 def _retry_on_exception_inner_1(f):
37 def _retry_on_exception_inner_2(*args, **kwargs):
38 retries = num_retries
39 multiplier = 1
40 while True:
41 try:
42 return f(*args, **kwargs)
43 except exc_type:
44 if not retries:
45 raise
46
47 delay = base_delay * multiplier
48 multiplier += 1
49 log("Retrying '%s' %d more times (delay=%s)" %
50 (f.__name__, retries, delay), level=INFO)
51 retries -= 1
52 if delay:
53 time.sleep(delay)
54
55 return _retry_on_exception_inner_2
56
57 return _retry_on_exception_inner_1
058
=== modified file 'hooks/charmhelpers/core/fstab.py'
--- hooks/charmhelpers/core/fstab.py 2014-11-26 12:31:33 +0000
+++ hooks/charmhelpers/core/fstab.py 2015-02-10 11:19:03 +0000
@@ -1,6 +1,22 @@
1#!/usr/bin/env python1#!/usr/bin/env python
2# -*- coding: utf-8 -*-2# -*- coding: utf-8 -*-
33
4# Copyright 2014-2015 Canonical Limited.
5#
6# This file is part of charm-helpers.
7#
8# charm-helpers is free software: you can redistribute it and/or modify
9# it under the terms of the GNU Lesser General Public License version 3 as
10# published by the Free Software Foundation.
11#
12# charm-helpers is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Lesser General Public License for more details.
16#
17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
19
4__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'20__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
521
6import io22import io
723
=== modified file 'hooks/charmhelpers/core/hookenv.py'
--- hooks/charmhelpers/core/hookenv.py 2014-11-26 12:31:33 +0000
+++ hooks/charmhelpers/core/hookenv.py 2015-02-10 11:19:03 +0000
@@ -1,3 +1,19 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
1"Interactions with the Juju environment"17"Interactions with the Juju environment"
2# Copyright 2013 Canonical Ltd.18# Copyright 2013 Canonical Ltd.
3#19#
@@ -68,6 +84,8 @@
68 command = ['juju-log']84 command = ['juju-log']
69 if level:85 if level:
70 command += ['-l', level]86 command += ['-l', level]
87 if not isinstance(message, six.string_types):
88 message = repr(message)
71 command += [message]89 command += [message]
72 subprocess.call(command)90 subprocess.call(command)
7391
@@ -394,21 +412,31 @@
394412
395413
396@cached414@cached
415def metadata():
416 """Get the current charm metadata.yaml contents as a python object"""
417 with open(os.path.join(charm_dir(), 'metadata.yaml')) as md:
418 return yaml.safe_load(md)
419
420
421@cached
397def relation_types():422def relation_types():
398 """Get a list of relation types supported by this charm"""423 """Get a list of relation types supported by this charm"""
399 charmdir = os.environ.get('CHARM_DIR', '')
400 mdf = open(os.path.join(charmdir, 'metadata.yaml'))
401 md = yaml.safe_load(mdf)
402 rel_types = []424 rel_types = []
425 md = metadata()
403 for key in ('provides', 'requires', 'peers'):426 for key in ('provides', 'requires', 'peers'):
404 section = md.get(key)427 section = md.get(key)
405 if section:428 if section:
406 rel_types.extend(section.keys())429 rel_types.extend(section.keys())
407 mdf.close()
408 return rel_types430 return rel_types
409431
410432
411@cached433@cached
434def charm_name():
435 """Get the name of the current charm as is specified on metadata.yaml"""
436 return metadata().get('name')
437
438
439@cached
412def relations():440def relations():
413 """Get a nested dictionary of relation data for all related units"""441 """Get a nested dictionary of relation data for all related units"""
414 rels = {}442 rels = {}
415443
=== modified file 'hooks/charmhelpers/core/host.py'
--- hooks/charmhelpers/core/host.py 2014-11-26 12:31:33 +0000
+++ hooks/charmhelpers/core/host.py 2015-02-10 11:19:03 +0000
@@ -1,3 +1,19 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
1"""Tools for working with the host system"""17"""Tools for working with the host system"""
2# Copyright 2012 Canonical Ltd.18# Copyright 2012 Canonical Ltd.
3#19#
@@ -101,6 +117,26 @@
101 return user_info117 return user_info
102118
103119
120def add_group(group_name, system_group=False):
121 """Add a group to the system"""
122 try:
123 group_info = grp.getgrnam(group_name)
124 log('group {0} already exists!'.format(group_name))
125 except KeyError:
126 log('creating group {0}'.format(group_name))
127 cmd = ['addgroup']
128 if system_group:
129 cmd.append('--system')
130 else:
131 cmd.extend([
132 '--group',
133 ])
134 cmd.append(group_name)
135 subprocess.check_call(cmd)
136 group_info = grp.getgrnam(group_name)
137 return group_info
138
139
104def add_user_to_group(username, group):140def add_user_to_group(username, group):
105 """Add a user to a group"""141 """Add a user to a group"""
106 cmd = [142 cmd = [
@@ -142,21 +178,24 @@
142 uid = pwd.getpwnam(owner).pw_uid178 uid = pwd.getpwnam(owner).pw_uid
143 gid = grp.getgrnam(group).gr_gid179 gid = grp.getgrnam(group).gr_gid
144 realpath = os.path.abspath(path)180 realpath = os.path.abspath(path)
145 if os.path.exists(realpath):181 path_exists = os.path.exists(realpath)
146 if force and not os.path.isdir(realpath):182 if path_exists and force:
183 if not os.path.isdir(realpath):
147 log("Removing non-directory file {} prior to mkdir()".format(path))184 log("Removing non-directory file {} prior to mkdir()".format(path))
148 os.unlink(realpath)185 os.unlink(realpath)
149 else:186 os.makedirs(realpath, perms)
187 elif not path_exists:
150 os.makedirs(realpath, perms)188 os.makedirs(realpath, perms)
151 os.chown(realpath, uid, gid)189 os.chown(realpath, uid, gid)
190 os.chmod(realpath, perms)
152191
153192
154def write_file(path, content, owner='root', group='root', perms=0o444):193def write_file(path, content, owner='root', group='root', perms=0o444):
155 """Create or overwrite a file with the contents of a string"""194 """Create or overwrite a file with the contents of a byte string."""
156 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))195 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
157 uid = pwd.getpwnam(owner).pw_uid196 uid = pwd.getpwnam(owner).pw_uid
158 gid = grp.getgrnam(group).gr_gid197 gid = grp.getgrnam(group).gr_gid
159 with open(path, 'w') as target:198 with open(path, 'wb') as target:
160 os.fchown(target.fileno(), uid, gid)199 os.fchown(target.fileno(), uid, gid)
161 os.fchmod(target.fileno(), perms)200 os.fchmod(target.fileno(), perms)
162 target.write(content)201 target.write(content)
@@ -322,7 +361,7 @@
322 ip_output = (line for line in ip_output if line)361 ip_output = (line for line in ip_output if line)
323 for line in ip_output:362 for line in ip_output:
324 if line.split()[1].startswith(int_type):363 if line.split()[1].startswith(int_type):
325 matched = re.search('.*: (bond[0-9]+\.[0-9]+)@.*', line)364 matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line)
326 if matched:365 if matched:
327 interface = matched.groups()[0]366 interface = matched.groups()[0]
328 else:367 else:
@@ -366,10 +405,13 @@
366 * 0 => Installed revno is the same as supplied arg405 * 0 => Installed revno is the same as supplied arg
367 * -1 => Installed revno is less than supplied arg406 * -1 => Installed revno is less than supplied arg
368407
408 This function imports apt_cache function from charmhelpers.fetch if
409 the pkgcache argument is None. Be sure to add charmhelpers.fetch if
410 you call this function, or pass an apt_pkg.Cache() instance.
369 '''411 '''
370 import apt_pkg412 import apt_pkg
371 from charmhelpers.fetch import apt_cache
372 if not pkgcache:413 if not pkgcache:
414 from charmhelpers.fetch import apt_cache
373 pkgcache = apt_cache()415 pkgcache = apt_cache()
374 pkg = pkgcache[package]416 pkg = pkgcache[package]
375 return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)417 return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
@@ -384,13 +426,21 @@
384 os.chdir(cur)426 os.chdir(cur)
385427
386428
387def chownr(path, owner, group):429def chownr(path, owner, group, follow_links=True):
388 uid = pwd.getpwnam(owner).pw_uid430 uid = pwd.getpwnam(owner).pw_uid
389 gid = grp.getgrnam(group).gr_gid431 gid = grp.getgrnam(group).gr_gid
432 if follow_links:
433 chown = os.chown
434 else:
435 chown = os.lchown
390436
391 for root, dirs, files in os.walk(path):437 for root, dirs, files in os.walk(path):
392 for name in dirs + files:438 for name in dirs + files:
393 full = os.path.join(root, name)439 full = os.path.join(root, name)
394 broken_symlink = os.path.lexists(full) and not os.path.exists(full)440 broken_symlink = os.path.lexists(full) and not os.path.exists(full)
395 if not broken_symlink:441 if not broken_symlink:
396 os.chown(full, uid, gid)442 chown(full, uid, gid)
443
444
445def lchownr(path, owner, group):
446 chownr(path, owner, group, follow_links=False)
397447
=== modified file 'hooks/charmhelpers/core/services/__init__.py'
--- hooks/charmhelpers/core/services/__init__.py 2014-10-22 14:38:57 +0000
+++ hooks/charmhelpers/core/services/__init__.py 2015-02-10 11:19:03 +0000
@@ -1,2 +1,18 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
1from .base import * # NOQA17from .base import * # NOQA
2from .helpers import * # NOQA18from .helpers import * # NOQA
319
=== modified file 'hooks/charmhelpers/core/services/base.py'
--- hooks/charmhelpers/core/services/base.py 2014-09-26 07:17:08 +0000
+++ hooks/charmhelpers/core/services/base.py 2015-02-10 11:19:03 +0000
@@ -1,3 +1,19 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
1import os17import os
2import re18import re
3import json19import json
420
=== modified file 'hooks/charmhelpers/core/services/helpers.py'
--- hooks/charmhelpers/core/services/helpers.py 2014-11-26 12:31:33 +0000
+++ hooks/charmhelpers/core/services/helpers.py 2015-02-10 11:19:03 +0000
@@ -1,3 +1,19 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
1import os17import os
2import yaml18import yaml
3from charmhelpers.core import hookenv19from charmhelpers.core import hookenv
420
=== modified file 'hooks/charmhelpers/core/sysctl.py'
--- hooks/charmhelpers/core/sysctl.py 2014-10-09 10:30:08 +0000
+++ hooks/charmhelpers/core/sysctl.py 2015-02-10 11:19:03 +0000
@@ -1,6 +1,22 @@
1#!/usr/bin/env python1#!/usr/bin/env python
2# -*- coding: utf-8 -*-2# -*- coding: utf-8 -*-
33
4# Copyright 2014-2015 Canonical Limited.
5#
6# This file is part of charm-helpers.
7#
8# charm-helpers is free software: you can redistribute it and/or modify
9# it under the terms of the GNU Lesser General Public License version 3 as
10# published by the Free Software Foundation.
11#
12# charm-helpers is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Lesser General Public License for more details.
16#
17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
19
4__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'20__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
521
6import yaml22import yaml
@@ -10,25 +26,31 @@
10from charmhelpers.core.hookenv import (26from charmhelpers.core.hookenv import (
11 log,27 log,
12 DEBUG,28 DEBUG,
29 ERROR,
13)30)
1431
1532
16def create(sysctl_dict, sysctl_file):33def create(sysctl_dict, sysctl_file):
17 """Creates a sysctl.conf file from a YAML associative array34 """Creates a sysctl.conf file from a YAML associative array
1835
19 :param sysctl_dict: a dict of sysctl options eg { 'kernel.max_pid': 1337 }36 :param sysctl_dict: a YAML-formatted string of sysctl options eg "{ 'kernel.max_pid': 1337 }"
20 :type sysctl_dict: dict37 :type sysctl_dict: str
21 :param sysctl_file: path to the sysctl file to be saved38 :param sysctl_file: path to the sysctl file to be saved
22 :type sysctl_file: str or unicode39 :type sysctl_file: str or unicode
23 :returns: None40 :returns: None
24 """41 """
25 sysctl_dict = yaml.load(sysctl_dict)42 try:
43 sysctl_dict_parsed = yaml.safe_load(sysctl_dict)
44 except yaml.YAMLError:
45 log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict),
46 level=ERROR)
47 return
2648
27 with open(sysctl_file, "w") as fd:49 with open(sysctl_file, "w") as fd:
28 for key, value in sysctl_dict.items():50 for key, value in sysctl_dict_parsed.items():
29 fd.write("{}={}\n".format(key, value))51 fd.write("{}={}\n".format(key, value))
3052
31 log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict),53 log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict_parsed),
32 level=DEBUG)54 level=DEBUG)
3355
34 check_call(["sysctl", "-p", sysctl_file])56 check_call(["sysctl", "-p", sysctl_file])
3557
=== modified file 'hooks/charmhelpers/core/templating.py'
--- hooks/charmhelpers/core/templating.py 2014-11-26 12:31:33 +0000
+++ hooks/charmhelpers/core/templating.py 2015-02-10 11:19:03 +0000
@@ -1,3 +1,19 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
1import os17import os
218
3from charmhelpers.core import host19from charmhelpers.core import host
@@ -5,7 +21,7 @@
521
622
7def render(source, target, context, owner='root', group='root',23def render(source, target, context, owner='root', group='root',
8 perms=0o444, templates_dir=None):24 perms=0o444, templates_dir=None, encoding='UTF-8'):
9 """25 """
10 Render a template.26 Render a template.
1127
@@ -48,5 +64,5 @@
48 level=hookenv.ERROR)64 level=hookenv.ERROR)
49 raise e65 raise e
50 content = template.render(context)66 content = template.render(context)
51 host.mkdir(os.path.dirname(target))67 host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
52 host.write_file(target, content, owner, group, perms)68 host.write_file(target, content.encode(encoding), owner, group, perms)
5369
=== modified file 'hooks/charmhelpers/fetch/__init__.py'
--- hooks/charmhelpers/fetch/__init__.py 2014-11-26 12:31:33 +0000
+++ hooks/charmhelpers/fetch/__init__.py 2015-02-10 11:19:03 +0000
@@ -1,3 +1,19 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
1import importlib17import importlib
2from tempfile import NamedTemporaryFile18from tempfile import NamedTemporaryFile
3import time19import time
@@ -64,9 +80,16 @@
64 'trusty-juno/updates': 'trusty-updates/juno',80 'trusty-juno/updates': 'trusty-updates/juno',
65 'trusty-updates/juno': 'trusty-updates/juno',81 'trusty-updates/juno': 'trusty-updates/juno',
66 'juno/proposed': 'trusty-proposed/juno',82 'juno/proposed': 'trusty-proposed/juno',
67 'juno/proposed': 'trusty-proposed/juno',
68 'trusty-juno/proposed': 'trusty-proposed/juno',83 'trusty-juno/proposed': 'trusty-proposed/juno',
69 'trusty-proposed/juno': 'trusty-proposed/juno',84 'trusty-proposed/juno': 'trusty-proposed/juno',
85 # Kilo
86 'kilo': 'trusty-updates/kilo',
87 'trusty-kilo': 'trusty-updates/kilo',
88 'trusty-kilo/updates': 'trusty-updates/kilo',
89 'trusty-updates/kilo': 'trusty-updates/kilo',
90 'kilo/proposed': 'trusty-proposed/kilo',
91 'trusty-kilo/proposed': 'trusty-proposed/kilo',
92 'trusty-proposed/kilo': 'trusty-proposed/kilo',
70}93}
7194
72# The order of this list is very important. Handlers should be listed in from95# The order of this list is very important. Handlers should be listed in from
7396
=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
--- hooks/charmhelpers/fetch/archiveurl.py 2014-11-26 12:31:33 +0000
+++ hooks/charmhelpers/fetch/archiveurl.py 2015-02-10 11:19:03 +0000
@@ -1,3 +1,19 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
1import os17import os
2import hashlib18import hashlib
3import re19import re
420
=== modified file 'hooks/charmhelpers/fetch/bzrurl.py'
--- hooks/charmhelpers/fetch/bzrurl.py 2014-11-26 12:31:33 +0000
+++ hooks/charmhelpers/fetch/bzrurl.py 2015-02-10 11:19:03 +0000
@@ -1,3 +1,19 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
1import os17import os
2from charmhelpers.fetch import (18from charmhelpers.fetch import (
3 BaseFetchHandler,19 BaseFetchHandler,
@@ -11,10 +27,12 @@
1127
12try:28try:
13 from bzrlib.branch import Branch29 from bzrlib.branch import Branch
30 from bzrlib import bzrdir, workingtree, errors
14except ImportError:31except ImportError:
15 from charmhelpers.fetch import apt_install32 from charmhelpers.fetch import apt_install
16 apt_install("python-bzrlib")33 apt_install("python-bzrlib")
17 from bzrlib.branch import Branch34 from bzrlib.branch import Branch
35 from bzrlib import bzrdir, workingtree, errors
1836
1937
20class BzrUrlFetchHandler(BaseFetchHandler):38class BzrUrlFetchHandler(BaseFetchHandler):
@@ -35,8 +53,14 @@
35 from bzrlib.plugin import load_plugins53 from bzrlib.plugin import load_plugins
36 load_plugins()54 load_plugins()
37 try:55 try:
56 local_branch = bzrdir.BzrDir.create_branch_convenience(dest)
57 except errors.AlreadyControlDirError:
58 local_branch = Branch.open(dest)
59 try:
38 remote_branch = Branch.open(source)60 remote_branch = Branch.open(source)
39 remote_branch.bzrdir.sprout(dest).open_branch()61 remote_branch.push(local_branch)
62 tree = workingtree.WorkingTree.open(dest)
63 tree.update()
40 except Exception as e:64 except Exception as e:
41 raise e65 raise e
4266
4367
=== modified file 'hooks/charmhelpers/fetch/giturl.py'
--- hooks/charmhelpers/fetch/giturl.py 2014-11-26 12:31:33 +0000
+++ hooks/charmhelpers/fetch/giturl.py 2015-02-10 11:19:03 +0000
@@ -1,3 +1,19 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
1import os17import os
2from charmhelpers.fetch import (18from charmhelpers.fetch import (
3 BaseFetchHandler,19 BaseFetchHandler,
@@ -16,6 +32,8 @@
16 apt_install("python-git")32 apt_install("python-git")
17 from git import Repo33 from git import Repo
1834
35from git.exc import GitCommandError
36
1937
20class GitUrlFetchHandler(BaseFetchHandler):38class GitUrlFetchHandler(BaseFetchHandler):
21 """Handler for git branches via generic and github URLs"""39 """Handler for git branches via generic and github URLs"""
@@ -34,15 +52,20 @@
34 repo = Repo.clone_from(source, dest)52 repo = Repo.clone_from(source, dest)
35 repo.git.checkout(branch)53 repo.git.checkout(branch)
3654
37 def install(self, source, branch="master"):55 def install(self, source, branch="master", dest=None):
38 url_parts = self.parse_url(source)56 url_parts = self.parse_url(source)
39 branch_name = url_parts.path.strip("/").split("/")[-1]57 branch_name = url_parts.path.strip("/").split("/")[-1]
40 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",58 if dest:
41 branch_name)59 dest_dir = os.path.join(dest, branch_name)
60 else:
61 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
62 branch_name)
42 if not os.path.exists(dest_dir):63 if not os.path.exists(dest_dir):
43 mkdir(dest_dir, perms=0o755)64 mkdir(dest_dir, perms=0o755)
44 try:65 try:
45 self.clone(source, dest_dir, branch)66 self.clone(source, dest_dir, branch)
67 except GitCommandError as e:
68 raise UnhandledSource(e.message)
46 except OSError as e:69 except OSError as e:
47 raise UnhandledSource(e.strerror)70 raise UnhandledSource(e.strerror)
48 return dest_dir71 return dest_dir
4972
=== modified file 'hooks/common.py'
--- hooks/common.py 2015-01-07 14:18:57 +0000
+++ hooks/common.py 2015-02-10 11:19:03 +0000
@@ -6,6 +6,7 @@
6import shutil6import shutil
7from charmhelpers.core import hookenv, host7from charmhelpers.core import hookenv, host
8from charmhelpers.core.templating import render8from charmhelpers.core.templating import render
9from charmhelpers.contrib.database.mysql import MySQLHelper
910
1011
11def get_service_user_file(service):12def get_service_user_file(service):
@@ -60,71 +61,19 @@
60broken = os.path.exists(broken_path)61broken = os.path.exists(broken_path)
6162
6263
64def get_db_helper():
65 return MySQLHelper(rpasswdf_template='/var/lib/mysql/mysql.passwd',
66 upasswdf_template='/var/lib/mysql/mysql-{}.passwd')
67
68
63def get_db_cursor():69def get_db_cursor():
64 # Connect to mysql70 # Connect to mysql
65 passwd = open("/var/lib/mysql/mysql.passwd").read().strip()71 db_helper = get_db_helper()
72 passwd = db_helper.get_mysql_root_password()
66 connection = MySQLdb.connect(user="root", host="localhost", passwd=passwd)73 connection = MySQLdb.connect(user="root", host="localhost", passwd=passwd)
67 return connection.cursor()74 return connection.cursor()
6875
6976
70def database_exists(db_name):
71 cursor = get_db_cursor()
72 try:
73 cursor.execute("SHOW DATABASES")
74 databases = [i[0] for i in cursor.fetchall()]
75 finally:
76 cursor.close()
77 return db_name in databases
78
79
80def create_database(db_name):
81 cursor = get_db_cursor()
82 try:
83 cursor.execute("CREATE DATABASE {}".format(db_name))
84 finally:
85 cursor.close()
86
87
88def grant_exists(db_name, db_user, remote_ip):
89 cursor = get_db_cursor()
90 priv_string = "GRANT ALL PRIVILEGES ON `{}`.* " \
91 "TO '{}'@'{}'".format(db_name, db_user, remote_ip)
92 try:
93 cursor.execute("SHOW GRANTS for '{}'@'{}'".format(db_user,
94 remote_ip))
95 grants = [i[0] for i in cursor.fetchall()]
96 except MySQLdb.OperationalError:
97 print "No grants found"
98 return False
99 finally:
100 cursor.close()
101 return priv_string in grants
102
103
104def create_grant(db_name, db_user,
105 remote_ip, password):
106 cursor = get_db_cursor()
107 try:
108 cursor.execute("GRANT ALL PRIVILEGES ON {}.* TO '{}'@'{}' "
109 "IDENTIFIED BY '{}'".format(db_name,
110 db_user,
111 remote_ip,
112 password))
113 finally:
114 cursor.close()
115
116
117def cleanup_grant(db_user,
118 remote_ip):
119 cursor = get_db_cursor()
120 try:
121 cursor.execute("DROP FROM mysql.user WHERE user='{}' "
122 "AND HOST='{}'".format(db_user,
123 remote_ip))
124 finally:
125 cursor.close()
126
127
128def migrate_to_mount(new_path):77def migrate_to_mount(new_path):
129 """Invoked when new mountpoint appears. This function safely migrates78 """Invoked when new mountpoint appears. This function safely migrates
130 MySQL data from local disk to persistent storage (only if needed)79 MySQL data from local disk to persistent storage (only if needed)
13180
=== modified file 'hooks/ha_relations.py'
--- hooks/ha_relations.py 2014-09-21 22:03:41 +0000
+++ hooks/ha_relations.py 2015-02-10 11:19:03 +0000
@@ -7,6 +7,10 @@
7import lib.ceph_utils as ceph7import lib.ceph_utils as ceph
8import lib.cluster_utils as cluster8import lib.cluster_utils as cluster
99
10from charmhelpers.contrib.peerstorage import (
11 peer_echo,
12)
13
10# CEPH14# CEPH
11DATA_SRC_DST = '/var/lib/mysql'15DATA_SRC_DST = '/var/lib/mysql'
12SERVICE_NAME = os.getenv('JUJU_UNIT_NAME').split('/')[0]16SERVICE_NAME = os.getenv('JUJU_UNIT_NAME').split('/')[0]
@@ -14,6 +18,11 @@
14LEADER_RES = 'res_mysql_vip'18LEADER_RES = 'res_mysql_vip'
1519
1620
21def cluster_changed():
22 # Echo any passwords placed on peer relation
23 peer_echo(includes=['.passwd'])
24
25
17def ha_relation_joined():26def ha_relation_joined():
18 vip = utils.config_get('vip')27 vip = utils.config_get('vip')
19 vip_iface = utils.config_get('vip_iface')28 vip_iface = utils.config_get('vip_iface')
@@ -148,6 +157,7 @@
148 "ha-relation-changed": ha_relation_changed,157 "ha-relation-changed": ha_relation_changed,
149 "ceph-relation-joined": ceph_joined,158 "ceph-relation-joined": ceph_joined,
150 "ceph-relation-changed": ceph_changed,159 "ceph-relation-changed": ceph_changed,
160 "cluster-relation-changed": cluster_changed,
151}161}
152162
153utils.do_hooks(hooks)163utils.do_hooks(hooks)
154164
=== modified file 'hooks/shared_db_relations.py'
--- hooks/shared_db_relations.py 2014-11-26 16:35:19 +0000
+++ hooks/shared_db_relations.py 2015-02-10 11:19:03 +0000
@@ -7,18 +7,12 @@
7#7#
8# Author: Adam Gandelman <adam.gandelman@canonical.com>8# Author: Adam Gandelman <adam.gandelman@canonical.com>
99
10
11from common import (
12 database_exists,
13 create_database,
14 grant_exists,
15 create_grant)
16import subprocess10import subprocess
17import json11import json
18import socket
19import os
20import lib.utils as utils12import lib.utils as utils
21import lib.cluster_utils as cluster13import lib.cluster_utils as cluster
14
15from common import get_db_helper
22from charmhelpers.core import hookenv16from charmhelpers.core import hookenv
23from charmhelpers.contrib.network.ip import (17from charmhelpers.contrib.network.ip import (
24 get_ipv6_addr18 get_ipv6_addr
@@ -50,71 +44,6 @@
5044
5145
52def shared_db_changed():46def shared_db_changed():
53
54 def get_allowed_units(database, username):
55 allowed_units = set()
56 for relid in hookenv.relation_ids('shared-db'):
57 for unit in hookenv.related_units(relid):
58 attr = "%s_%s" % (database, 'hostname')
59 hosts = hookenv.relation_get(attribute=attr, unit=unit,
60 rid=relid)
61 if not hosts:
62 hosts = [hookenv.relation_get(attribute='private-address',
63 unit=unit, rid=relid)]
64 else:
65 # hostname can be json-encoded list of hostnames
66 try:
67 hosts = json.loads(hosts)
68 except ValueError:
69 pass
70
71 if not isinstance(hosts, list):
72 hosts = [hosts]
73
74 if hosts:
75 for host in hosts:
76 utils.juju_log('INFO', "Checking host '%s' grant" %
77 (host))
78 if grant_exists(database, username, host):
79 if unit not in allowed_units:
80 allowed_units.add(unit)
81 else:
82 utils.juju_log('INFO', "No hosts found for grant check")
83
84 return allowed_units
85
86 def configure_db(hostname,
87 database,
88 username):
89 passwd_file = "/var/lib/mysql/mysql-{}.passwd".format(username)
90 if hostname != local_hostname:
91 try:
92 remote_ip = socket.gethostbyname(hostname)
93 except Exception:
94 # socket.gethostbyname doesn't support ipv6
95 remote_ip = hostname
96 else:
97 remote_ip = '127.0.0.1'
98
99 if not os.path.exists(passwd_file):
100 password = pwgen()
101 with open(passwd_file, 'w') as pfile:
102 pfile.write(password)
103 os.chmod(pfile.name, 0600)
104 else:
105 with open(passwd_file) as pfile:
106 password = pfile.read().strip()
107
108 if not database_exists(database):
109 create_database(database)
110 if not grant_exists(database,
111 username,
112 remote_ip):
113 create_grant(database,
114 username,
115 remote_ip, password)
116 return password
117
118 if not cluster.eligible_leader(LEADER_RES):47 if not cluster.eligible_leader(LEADER_RES):
119 utils.juju_log('INFO',48 utils.juju_log('INFO',
120 'MySQL service is peered, bailing shared-db relation'49 'MySQL service is peered, bailing shared-db relation'
@@ -132,6 +61,8 @@
132 'username',61 'username',
133 'hostname'])62 'hostname'])
13463
64 db_helper = get_db_helper()
65
135 if singleset.issubset(settings):66 if singleset.issubset(settings):
136 # Process a single database configuration67 # Process a single database configuration
137 hostname = settings['hostname']68 hostname = settings['hostname']
@@ -142,26 +73,23 @@
142 try:73 try:
143 hostname = json.loads(hostname)74 hostname = json.loads(hostname)
144 except ValueError:75 except ValueError:
145 pass76 hostname = [hostname]
14677
147 if isinstance(hostname, list):78 for host in hostname:
148 for host in hostname:79 password = db_helper.configure_db(host, database, username)
149 password = configure_db(host, database, username)80
150 else:81 allowed_units = db_helper.get_allowed_units(database, username)
151 password = configure_db(hostname, database, username)82 allowed_units = unit_sorted(allowed_units)
15283 allowed_units = ' '.join(allowed_units)
153 allowed_units = " ".join(unit_sorted(get_allowed_units(database,84
154 username)))85 if cluster.is_clustered():
15586 db_host = utils.config_get("vip")
156 if not cluster.is_clustered():87 else:
157 utils.relation_set(db_host=local_hostname,88 db_host = local_hostname
158 password=password,89
159 allowed_units=allowed_units)90 utils.relation_set(db_host=db_host,
160 else:91 password=password,
161 utils.relation_set(db_host=utils.config_get("vip"),92 allowed_units=allowed_units)
162 password=password,
163 allowed_units=allowed_units)
164
165 else:93 else:
166 # Process multiple database setup requests.94 # Process multiple database setup requests.
167 # from incoming relation data:95 # from incoming relation data:
@@ -195,22 +123,23 @@
195 database = databases[db]['database']123 database = databases[db]['database']
196 hostname = databases[db]['hostname']124 hostname = databases[db]['hostname']
197 username = databases[db]['username']125 username = databases[db]['username']
126
198 try:127 try:
128 # Can be json-encoded list of hostnames
199 hostname = json.loads(hostname)129 hostname = json.loads(hostname)
200 except ValueError:130 except ValueError:
201 hostname = hostname131 # Otherwise expected to be single hostname
202132 hostname = [hostname]
203 if isinstance(hostname, list):133
204 for host in hostname:134 for host in hostname:
205 password = configure_db(host, database, username)135 password = db_helper.configure_db(host, database, username)
206 else:136
207 password = configure_db(hostname, database, username)137 a_units = db_helper.get_allowed_units(database, username)
208138 a_units = ' '.join(unit_sorted(a_units))
209 return_data['_'.join([db, 'password'])] = password139 return_data['%s_allowed_units' % (db)] = a_units
210 allowed_units = unit_sorted(get_allowed_units(database,140
211 username))141 return_data['%s_password' % (db)] = password
212 return_data['_'.join([db, 'allowed_units'])] = \142
213 " ".join(allowed_units)
214 if len(return_data) > 0:143 if len(return_data) > 0:
215 utils.relation_set(**return_data)144 utils.relation_set(**return_data)
216 if not cluster.is_clustered():145 if not cluster.is_clustered():

Subscribers

People subscribed via source and target branches

to all changes: