Merge lp:~daniel-thewatkins/charms/trusty/ubuntu-repository-cache/update_charm-helpers into lp:charms/trusty/ubuntu-repository-cache

Proposed by Dan Watkins on 2015-06-03
Status: Merged
Merged at revision: 195
Proposed branch: lp:~daniel-thewatkins/charms/trusty/ubuntu-repository-cache/update_charm-helpers
Merge into: lp:charms/trusty/ubuntu-repository-cache
Diff against target: 3246 lines (+1468/-469)
28 files modified
lib/charmhelpers/contrib/charmsupport/__init__.py (+15/-0)
lib/charmhelpers/contrib/charmsupport/nrpe.py (+147/-6)
lib/charmhelpers/contrib/hahelpers/__init__.py (+15/-0)
lib/charmhelpers/contrib/hahelpers/apache.py (+26/-3)
lib/charmhelpers/contrib/hahelpers/cluster.py (+92/-21)
lib/charmhelpers/contrib/storage/__init__.py (+15/-0)
lib/charmhelpers/contrib/storage/linux/__init__.py (+15/-0)
lib/charmhelpers/contrib/storage/linux/ceph.py (+159/-102)
lib/charmhelpers/contrib/storage/linux/loopback.py (+19/-3)
lib/charmhelpers/contrib/storage/linux/lvm.py (+17/-0)
lib/charmhelpers/contrib/storage/linux/utils.py (+20/-3)
lib/charmhelpers/contrib/unison/__init__.py (+69/-29)
lib/charmhelpers/core/__init__.py (+15/-0)
lib/charmhelpers/core/fstab.py (+30/-12)
lib/charmhelpers/core/hookenv.py (+251/-31)
lib/charmhelpers/core/host.py (+117/-35)
lib/charmhelpers/core/services/__init__.py (+18/-2)
lib/charmhelpers/core/services/base.py (+48/-11)
lib/charmhelpers/core/services/helpers.py (+150/-8)
lib/charmhelpers/core/templating.py (+20/-3)
lib/charmhelpers/fetch/__init__.py (+62/-19)
lib/charmhelpers/fetch/archiveurl.py (+116/-58)
lib/charmhelpers/fetch/bzrurl.py (+30/-2)
patches/hookenv-0001-fix_config_lookups.patch (+0/-30)
patches/lchownr.patch (+0/-42)
patches/mirror-0001-devel_reduce_dists.patch (+0/-16)
patches/storage-0001-is_device_mounted.patch (+0/-31)
patches/unison-0002-keyscan.patch (+2/-2)
To merge this branch: bzr merge lp:~daniel-thewatkins/charms/trusty/ubuntu-repository-cache/update_charm-helpers
Reviewer Review Type Date Requested Status
José Antonio Rey 2015-06-03 Approve on 2015-06-04
Adam Israel Approve on 2015-06-04
Robert C Jennings 2015-06-03 Pending
Review via email: mp+260956@code.launchpad.net

Description of the Change

We need new charm-helpers for Nagios stuff and some other minor fixes; we were carrying some of those fixes as patches, which have been dropped.

To post a comment you must log in.
José Antonio Rey (jose) wrote :

Hey Daniel!

Unfortunately there are several merge conflicts in this branch. Would you mind checking and fixing them so we can do a clean merge when approved?

Thanks for your contributions to the Charm Store!

review: Needs Fixing
Dan Watkins (daniel-thewatkins) wrote :

Thanks, bzr. Thzr.[0] I'll look at these today.

[0] https://www.youtube.com/watch?v=9jtU9BbReQk

Dan Watkins (daniel-thewatkins) wrote :

Thanks, bzr. Thzr.[0] I'll look at these today.

[0] https://www.youtube.com/watch?v=9jtU9BbReQk

Dan Watkins (daniel-thewatkins) wrote :

Thanks, bzr. Thzr.[0] I'll look at these today.

[0] https://www.youtube.com/watch?v=9jtU9BbReQk

Dan Watkins (daniel-thewatkins) wrote :

Thanks, bzr. Thzr.[0] I'll look at these today.

[0] https://www.youtube.com/watch?v=9jtU9BbReQk

Dan Watkins (daniel-thewatkins) wrote :

Thanks, bzr. Thzr.[0] I'll look at these today.

[0] https://www.youtube.com/watch?v=9jtU9BbReQk

Dan Watkins (daniel-thewatkins) wrote :

Right, this is fixed and ready for review now.

Adam Israel (aisrael) wrote :

Hi Daniel,

This merges cleanly for me. +1

review: Approve
194. By José Antonio Rey on 2015-06-04

Daniel Watkins 2015-06-01 Set PATH in templates/cron/ubuntu-repository-cache_rsync.cron.

José Antonio Rey (jose) wrote :

Daniel,

Thanks for fixing the merge conflicts. This now merges clean, so I'm going ahead and pushing.

+1 LGTM!

Thanks again for your contributions to the Charm Store!

review: Approve
195. By José Antonio Rey on 2015-06-04

Daniel Watkins 2015-06-02 Revert "storage.linux.util: Fix swapped is_device_mounted regex logic (lp:1370053)"
Daniel Watkins 2015-06-03 Update charm-helpers.
Daniel Watkins 2015-06-03 Remove superseded patches.
Daniel Watkins 2015-06-03 Refresh and apply remaining patches.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/charmhelpers/contrib/charmsupport/__init__.py'
2--- lib/charmhelpers/contrib/charmsupport/__init__.py 2014-09-18 21:30:22 +0000
3+++ lib/charmhelpers/contrib/charmsupport/__init__.py 2015-06-04 13:22:19 +0000
4@@ -0,0 +1,15 @@
5+# Copyright 2014-2015 Canonical Limited.
6+#
7+# This file is part of charm-helpers.
8+#
9+# charm-helpers is free software: you can redistribute it and/or modify
10+# it under the terms of the GNU Lesser General Public License version 3 as
11+# published by the Free Software Foundation.
12+#
13+# charm-helpers is distributed in the hope that it will be useful,
14+# but WITHOUT ANY WARRANTY; without even the implied warranty of
15+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+# GNU Lesser General Public License for more details.
17+#
18+# You should have received a copy of the GNU Lesser General Public License
19+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
20
21=== modified file 'lib/charmhelpers/contrib/charmsupport/nrpe.py'
22--- lib/charmhelpers/contrib/charmsupport/nrpe.py 2014-09-18 21:30:22 +0000
23+++ lib/charmhelpers/contrib/charmsupport/nrpe.py 2015-06-04 13:22:19 +0000
24@@ -1,3 +1,19 @@
25+# Copyright 2014-2015 Canonical Limited.
26+#
27+# This file is part of charm-helpers.
28+#
29+# charm-helpers is free software: you can redistribute it and/or modify
30+# it under the terms of the GNU Lesser General Public License version 3 as
31+# published by the Free Software Foundation.
32+#
33+# charm-helpers is distributed in the hope that it will be useful,
34+# but WITHOUT ANY WARRANTY; without even the implied warranty of
35+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
36+# GNU Lesser General Public License for more details.
37+#
38+# You should have received a copy of the GNU Lesser General Public License
39+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
40+
41 """Compatibility with the nrpe-external-master charm"""
42 # Copyright 2012 Canonical Ltd.
43 #
44@@ -8,6 +24,8 @@
45 import pwd
46 import grp
47 import os
48+import glob
49+import shutil
50 import re
51 import shlex
52 import yaml
53@@ -18,6 +36,7 @@
54 log,
55 relation_ids,
56 relation_set,
57+ relations_of_type,
58 )
59
60 from charmhelpers.core.host import service
61@@ -54,6 +73,12 @@
62 # juju-myservice-0
63 # If you're running multiple environments with the same services in them
64 # this allows you to differentiate between them.
65+# nagios_servicegroups:
66+# default: ""
67+# type: string
68+# description: |
69+# A comma-separated list of nagios servicegroups.
70+# If left empty, the nagios_context will be used as the servicegroup
71 #
72 # 3. Add custom checks (Nagios plugins) to files/nrpe-external-master
73 #
74@@ -138,7 +163,7 @@
75 log('Check command not found: {}'.format(parts[0]))
76 return ''
77
78- def write(self, nagios_context, hostname):
79+ def write(self, nagios_context, hostname, nagios_servicegroups):
80 nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format(
81 self.command)
82 with open(nrpe_check_file, 'w') as nrpe_check_config:
83@@ -150,16 +175,18 @@
84 log('Not writing service config as {} is not accessible'.format(
85 NRPE.nagios_exportdir))
86 else:
87- self.write_service_config(nagios_context, hostname)
88+ self.write_service_config(nagios_context, hostname,
89+ nagios_servicegroups)
90
91- def write_service_config(self, nagios_context, hostname):
92+ def write_service_config(self, nagios_context, hostname,
93+ nagios_servicegroups):
94 for f in os.listdir(NRPE.nagios_exportdir):
95 if re.search('.*{}.cfg'.format(self.command), f):
96 os.remove(os.path.join(NRPE.nagios_exportdir, f))
97
98 templ_vars = {
99 'nagios_hostname': hostname,
100- 'nagios_servicegroup': nagios_context,
101+ 'nagios_servicegroup': nagios_servicegroups,
102 'description': self.description,
103 'shortname': self.shortname,
104 'command': self.command,
105@@ -183,6 +210,10 @@
106 super(NRPE, self).__init__()
107 self.config = config()
108 self.nagios_context = self.config['nagios_context']
109+ if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']:
110+ self.nagios_servicegroups = self.config['nagios_servicegroups']
111+ else:
112+ self.nagios_servicegroups = self.nagios_context
113 self.unit_name = local_unit().replace('/', '-')
114 if hostname:
115 self.hostname = hostname
116@@ -208,12 +239,122 @@
117 nrpe_monitors = {}
118 monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}}
119 for nrpecheck in self.checks:
120- nrpecheck.write(self.nagios_context, self.hostname)
121+ nrpecheck.write(self.nagios_context, self.hostname,
122+ self.nagios_servicegroups)
123 nrpe_monitors[nrpecheck.shortname] = {
124 "command": nrpecheck.command,
125 }
126
127 service('restart', 'nagios-nrpe-server')
128
129- for rid in relation_ids("local-monitors"):
130+ monitor_ids = relation_ids("local-monitors") + \
131+ relation_ids("nrpe-external-master")
132+ for rid in monitor_ids:
133 relation_set(relation_id=rid, monitors=yaml.dump(monitors))
134+
135+
136+def get_nagios_hostcontext(relation_name='nrpe-external-master'):
137+ """
138+ Query relation with nrpe subordinate, return the nagios_host_context
139+
140+ :param str relation_name: Name of relation nrpe sub joined to
141+ """
142+ for rel in relations_of_type(relation_name):
143+ if 'nagios_hostname' in rel:
144+ return rel['nagios_host_context']
145+
146+
147+def get_nagios_hostname(relation_name='nrpe-external-master'):
148+ """
149+ Query relation with nrpe subordinate, return the nagios_hostname
150+
151+ :param str relation_name: Name of relation nrpe sub joined to
152+ """
153+ for rel in relations_of_type(relation_name):
154+ if 'nagios_hostname' in rel:
155+ return rel['nagios_hostname']
156+
157+
158+def get_nagios_unit_name(relation_name='nrpe-external-master'):
159+ """
160+ Return the nagios unit name prepended with host_context if needed
161+
162+ :param str relation_name: Name of relation nrpe sub joined to
163+ """
164+ host_context = get_nagios_hostcontext(relation_name)
165+ if host_context:
166+ unit = "%s:%s" % (host_context, local_unit())
167+ else:
168+ unit = local_unit()
169+ return unit
170+
171+
172+def add_init_service_checks(nrpe, services, unit_name):
173+ """
174+ Add checks for each service in list
175+
176+ :param NRPE nrpe: NRPE object to add check to
177+ :param list services: List of services to check
178+ :param str unit_name: Unit name to use in check description
179+ """
180+ for svc in services:
181+ upstart_init = '/etc/init/%s.conf' % svc
182+ sysv_init = '/etc/init.d/%s' % svc
183+ if os.path.exists(upstart_init):
184+ nrpe.add_check(
185+ shortname=svc,
186+ description='process check {%s}' % unit_name,
187+ check_cmd='check_upstart_job %s' % svc
188+ )
189+ elif os.path.exists(sysv_init):
190+ cronpath = '/etc/cron.d/nagios-service-check-%s' % svc
191+ cron_file = ('*/5 * * * * root '
192+ '/usr/local/lib/nagios/plugins/check_exit_status.pl '
193+ '-s /etc/init.d/%s status > '
194+ '/var/lib/nagios/service-check-%s.txt\n' % (svc,
195+ svc)
196+ )
197+ f = open(cronpath, 'w')
198+ f.write(cron_file)
199+ f.close()
200+ nrpe.add_check(
201+ shortname=svc,
202+ description='process check {%s}' % unit_name,
203+ check_cmd='check_status_file.py -f '
204+ '/var/lib/nagios/service-check-%s.txt' % svc,
205+ )
206+
207+
208+def copy_nrpe_checks():
209+ """
210+ Copy the nrpe checks into place
211+
212+ """
213+ NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins'
214+ nrpe_files_dir = os.path.join(os.getenv('CHARM_DIR'), 'hooks',
215+ 'charmhelpers', 'contrib', 'openstack',
216+ 'files')
217+
218+ if not os.path.exists(NAGIOS_PLUGINS):
219+ os.makedirs(NAGIOS_PLUGINS)
220+ for fname in glob.glob(os.path.join(nrpe_files_dir, "check_*")):
221+ if os.path.isfile(fname):
222+ shutil.copy2(fname,
223+ os.path.join(NAGIOS_PLUGINS, os.path.basename(fname)))
224+
225+
226+def add_haproxy_checks(nrpe, unit_name):
227+ """
228+ Add checks for each service in list
229+
230+ :param NRPE nrpe: NRPE object to add check to
231+ :param str unit_name: Unit name to use in check description
232+ """
233+ nrpe.add_check(
234+ shortname='haproxy_servers',
235+ description='Check HAProxy {%s}' % unit_name,
236+ check_cmd='check_haproxy.sh')
237+ nrpe.add_check(
238+ shortname='haproxy_queue',
239+ description='Check HAProxy queue depth {%s}' % unit_name,
240+ check_cmd='check_haproxy_queue_depth.sh')
241
242=== modified file 'lib/charmhelpers/contrib/hahelpers/__init__.py'
243--- lib/charmhelpers/contrib/hahelpers/__init__.py 2014-08-26 13:59:01 +0000
244+++ lib/charmhelpers/contrib/hahelpers/__init__.py 2015-06-04 13:22:19 +0000
245@@ -0,0 +1,15 @@
246+# Copyright 2014-2015 Canonical Limited.
247+#
248+# This file is part of charm-helpers.
249+#
250+# charm-helpers is free software: you can redistribute it and/or modify
251+# it under the terms of the GNU Lesser General Public License version 3 as
252+# published by the Free Software Foundation.
253+#
254+# charm-helpers is distributed in the hope that it will be useful,
255+# but WITHOUT ANY WARRANTY; without even the implied warranty of
256+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
257+# GNU Lesser General Public License for more details.
258+#
259+# You should have received a copy of the GNU Lesser General Public License
260+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
261
262=== modified file 'lib/charmhelpers/contrib/hahelpers/apache.py'
263--- lib/charmhelpers/contrib/hahelpers/apache.py 2014-08-26 13:59:01 +0000
264+++ lib/charmhelpers/contrib/hahelpers/apache.py 2015-06-04 13:22:19 +0000
265@@ -1,3 +1,19 @@
266+# Copyright 2014-2015 Canonical Limited.
267+#
268+# This file is part of charm-helpers.
269+#
270+# charm-helpers is free software: you can redistribute it and/or modify
271+# it under the terms of the GNU Lesser General Public License version 3 as
272+# published by the Free Software Foundation.
273+#
274+# charm-helpers is distributed in the hope that it will be useful,
275+# but WITHOUT ANY WARRANTY; without even the implied warranty of
276+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
277+# GNU Lesser General Public License for more details.
278+#
279+# You should have received a copy of the GNU Lesser General Public License
280+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
281+
282 #
283 # Copyright 2012 Canonical Ltd.
284 #
285@@ -20,20 +36,27 @@
286 )
287
288
289-def get_cert():
290+def get_cert(cn=None):
291+ # TODO: deal with multiple https endpoints via charm config
292 cert = config_get('ssl_cert')
293 key = config_get('ssl_key')
294 if not (cert and key):
295 log("Inspecting identity-service relations for SSL certificate.",
296 level=INFO)
297 cert = key = None
298+ if cn:
299+ ssl_cert_attr = 'ssl_cert_{}'.format(cn)
300+ ssl_key_attr = 'ssl_key_{}'.format(cn)
301+ else:
302+ ssl_cert_attr = 'ssl_cert'
303+ ssl_key_attr = 'ssl_key'
304 for r_id in relation_ids('identity-service'):
305 for unit in relation_list(r_id):
306 if not cert:
307- cert = relation_get('ssl_cert',
308+ cert = relation_get(ssl_cert_attr,
309 rid=r_id, unit=unit)
310 if not key:
311- key = relation_get('ssl_key',
312+ key = relation_get(ssl_key_attr,
313 rid=r_id, unit=unit)
314 return (cert, key)
315
316
317=== modified file 'lib/charmhelpers/contrib/hahelpers/cluster.py'
318--- lib/charmhelpers/contrib/hahelpers/cluster.py 2014-08-26 13:59:01 +0000
319+++ lib/charmhelpers/contrib/hahelpers/cluster.py 2015-06-04 13:22:19 +0000
320@@ -1,3 +1,19 @@
321+# Copyright 2014-2015 Canonical Limited.
322+#
323+# This file is part of charm-helpers.
324+#
325+# charm-helpers is free software: you can redistribute it and/or modify
326+# it under the terms of the GNU Lesser General Public License version 3 as
327+# published by the Free Software Foundation.
328+#
329+# charm-helpers is distributed in the hope that it will be useful,
330+# but WITHOUT ANY WARRANTY; without even the implied warranty of
331+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
332+# GNU Lesser General Public License for more details.
333+#
334+# You should have received a copy of the GNU Lesser General Public License
335+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
336+
337 #
338 # Copyright 2012 Canonical Ltd.
339 #
340@@ -16,6 +32,8 @@
341
342 from socket import gethostname as get_unit_hostname
343
344+import six
345+
346 from charmhelpers.core.hookenv import (
347 log,
348 relation_ids,
349@@ -27,12 +45,24 @@
350 WARNING,
351 unit_get,
352 )
353+from charmhelpers.core.decorators import (
354+ retry_on_exception,
355+)
356+from charmhelpers.core.strutils import (
357+ bool_from_string,
358+)
359+
360+DC_RESOURCE_NAME = 'DC'
361
362
363 class HAIncompleteConfig(Exception):
364 pass
365
366
367+class CRMResourceNotFound(Exception):
368+ pass
369+
370+
371 def is_elected_leader(resource):
372 """
373 Returns True if the charm executing this is the elected cluster leader.
374@@ -67,24 +97,53 @@
375 return False
376
377
378-def is_crm_leader(resource):
379+def is_crm_dc():
380+ """
381+ Determine leadership by querying the pacemaker Designated Controller
382+ """
383+ cmd = ['crm', 'status']
384+ try:
385+ status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
386+ if not isinstance(status, six.text_type):
387+ status = six.text_type(status, "utf-8")
388+ except subprocess.CalledProcessError:
389+ return False
390+ current_dc = ''
391+ for line in status.split('\n'):
392+ if line.startswith('Current DC'):
393+ # Current DC: juju-lytrusty-machine-2 (168108163) - partition with quorum
394+ current_dc = line.split(':')[1].split()[0]
395+ if current_dc == get_unit_hostname():
396+ return True
397+ return False
398+
399+
400+@retry_on_exception(5, base_delay=2, exc_type=CRMResourceNotFound)
401+def is_crm_leader(resource, retry=False):
402 """
403 Returns True if the charm calling this is the elected corosync leader,
404 as returned by calling the external "crm" command.
405+
406+ We allow this operation to be retried to avoid the possibility of getting a
407+ false negative. See LP #1396246 for more info.
408 """
409- cmd = [
410- "crm", "resource",
411- "show", resource
412- ]
413+ if resource == DC_RESOURCE_NAME:
414+ return is_crm_dc()
415+ cmd = ['crm', 'resource', 'show', resource]
416 try:
417- status = subprocess.check_output(cmd)
418+ status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
419+ if not isinstance(status, six.text_type):
420+ status = six.text_type(status, "utf-8")
421 except subprocess.CalledProcessError:
422- return False
423- else:
424- if get_unit_hostname() in status:
425- return True
426- else:
427- return False
428+ status = None
429+
430+ if status and get_unit_hostname() in status:
431+ return True
432+
433+ if status and "resource %s is NOT running" % (resource) in status:
434+ raise CRMResourceNotFound("CRM resource %s not found" % (resource))
435+
436+ return False
437
438
439 def is_leader(resource):
440@@ -133,16 +192,16 @@
441 .
442 returns: boolean
443 '''
444- if config_get('use-https') == "yes":
445+ use_https = config_get('use-https')
446+ if use_https and bool_from_string(use_https):
447 return True
448 if config_get('ssl_cert') and config_get('ssl_key'):
449 return True
450 for r_id in relation_ids('identity-service'):
451 for unit in relation_list(r_id):
452+ # TODO - needs fixing for new helper as ssl_cert/key suffixes with CN
453 rel_state = [
454 relation_get('https_keystone', rid=r_id, unit=unit),
455- relation_get('ssl_cert', rid=r_id, unit=unit),
456- relation_get('ssl_key', rid=r_id, unit=unit),
457 relation_get('ca_cert', rid=r_id, unit=unit),
458 ]
459 # NOTE: works around (LP: #1203241)
460@@ -151,54 +210,66 @@
461 return False
462
463
464-def determine_api_port(public_port):
465+def determine_api_port(public_port, singlenode_mode=False):
466 '''
467 Determine correct API server listening port based on
468 existence of HTTPS reverse proxy and/or haproxy.
469
470 public_port: int: standard public port for given service
471
472+ singlenode_mode: boolean: Shuffle ports when only a single unit is present
473+
474 returns: int: the correct listening port for the API service
475 '''
476 i = 0
477- if len(peer_units()) > 0 or is_clustered():
478+ if singlenode_mode:
479+ i += 1
480+ elif len(peer_units()) > 0 or is_clustered():
481 i += 1
482 if https():
483 i += 1
484 return public_port - (i * 10)
485
486
487-def determine_apache_port(public_port):
488+def determine_apache_port(public_port, singlenode_mode=False):
489 '''
490 Description: Determine correct apache listening port based on public IP +
491 state of the cluster.
492
493 public_port: int: standard public port for given service
494
495+ singlenode_mode: boolean: Shuffle ports when only a single unit is present
496+
497 returns: int: the correct listening port for the HAProxy service
498 '''
499 i = 0
500- if len(peer_units()) > 0 or is_clustered():
501+ if singlenode_mode:
502+ i += 1
503+ elif len(peer_units()) > 0 or is_clustered():
504 i += 1
505 return public_port - (i * 10)
506
507
508-def get_hacluster_config():
509+def get_hacluster_config(exclude_keys=None):
510 '''
511 Obtains all relevant configuration from charm configuration required
512 for initiating a relation to hacluster:
513
514 ha-bindiface, ha-mcastport, vip
515
516+ param: exclude_keys: list of setting key(s) to be excluded.
517 returns: dict: A dict containing settings keyed by setting name.
518 raises: HAIncompleteConfig if settings are missing.
519 '''
520 settings = ['ha-bindiface', 'ha-mcastport', 'vip']
521 conf = {}
522 for setting in settings:
523+ if exclude_keys and setting in exclude_keys:
524+ continue
525+
526 conf[setting] = config_get(setting)
527 missing = []
528- [missing.append(s) for s, v in conf.iteritems() if v is None]
529+ [missing.append(s) for s, v in six.iteritems(conf) if v is None]
530 if missing:
531 log('Insufficient config data to configure hacluster.', level=ERROR)
532 raise HAIncompleteConfig
533
534=== modified file 'lib/charmhelpers/contrib/storage/__init__.py'
535--- lib/charmhelpers/contrib/storage/__init__.py 2014-09-15 14:42:27 +0000
536+++ lib/charmhelpers/contrib/storage/__init__.py 2015-06-04 13:22:19 +0000
537@@ -0,0 +1,15 @@
538+# Copyright 2014-2015 Canonical Limited.
539+#
540+# This file is part of charm-helpers.
541+#
542+# charm-helpers is free software: you can redistribute it and/or modify
543+# it under the terms of the GNU Lesser General Public License version 3 as
544+# published by the Free Software Foundation.
545+#
546+# charm-helpers is distributed in the hope that it will be useful,
547+# but WITHOUT ANY WARRANTY; without even the implied warranty of
548+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
549+# GNU Lesser General Public License for more details.
550+#
551+# You should have received a copy of the GNU Lesser General Public License
552+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
553
554=== modified file 'lib/charmhelpers/contrib/storage/linux/__init__.py'
555--- lib/charmhelpers/contrib/storage/linux/__init__.py 2014-09-15 14:42:27 +0000
556+++ lib/charmhelpers/contrib/storage/linux/__init__.py 2015-06-04 13:22:19 +0000
557@@ -0,0 +1,15 @@
558+# Copyright 2014-2015 Canonical Limited.
559+#
560+# This file is part of charm-helpers.
561+#
562+# charm-helpers is free software: you can redistribute it and/or modify
563+# it under the terms of the GNU Lesser General Public License version 3 as
564+# published by the Free Software Foundation.
565+#
566+# charm-helpers is distributed in the hope that it will be useful,
567+# but WITHOUT ANY WARRANTY; without even the implied warranty of
568+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
569+# GNU Lesser General Public License for more details.
570+#
571+# You should have received a copy of the GNU Lesser General Public License
572+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
573
574=== modified file 'lib/charmhelpers/contrib/storage/linux/ceph.py'
575--- lib/charmhelpers/contrib/storage/linux/ceph.py 2014-09-15 14:42:27 +0000
576+++ lib/charmhelpers/contrib/storage/linux/ceph.py 2015-06-04 13:22:19 +0000
577@@ -1,3 +1,19 @@
578+# Copyright 2014-2015 Canonical Limited.
579+#
580+# This file is part of charm-helpers.
581+#
582+# charm-helpers is free software: you can redistribute it and/or modify
583+# it under the terms of the GNU Lesser General Public License version 3 as
584+# published by the Free Software Foundation.
585+#
586+# charm-helpers is distributed in the hope that it will be useful,
587+# but WITHOUT ANY WARRANTY; without even the implied warranty of
588+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
589+# GNU Lesser General Public License for more details.
590+#
591+# You should have received a copy of the GNU Lesser General Public License
592+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
593+
594 #
595 # Copyright 2012 Canonical Ltd.
596 #
597@@ -16,19 +32,18 @@
598 from subprocess import (
599 check_call,
600 check_output,
601- CalledProcessError
602+ CalledProcessError,
603 )
604-
605 from charmhelpers.core.hookenv import (
606 relation_get,
607 relation_ids,
608 related_units,
609 log,
610+ DEBUG,
611 INFO,
612 WARNING,
613- ERROR
614+ ERROR,
615 )
616-
617 from charmhelpers.core.host import (
618 mount,
619 mounts,
620@@ -37,7 +52,6 @@
621 service_running,
622 umount,
623 )
624-
625 from charmhelpers.fetch import (
626 apt_install,
627 )
628@@ -56,99 +70,85 @@
629
630
631 def install():
632- ''' Basic Ceph client installation '''
633+ """Basic Ceph client installation."""
634 ceph_dir = "/etc/ceph"
635 if not os.path.exists(ceph_dir):
636 os.mkdir(ceph_dir)
637+
638 apt_install('ceph-common', fatal=True)
639
640
641 def rbd_exists(service, pool, rbd_img):
642- ''' Check to see if a RADOS block device exists '''
643+ """Check to see if a RADOS block device exists."""
644 try:
645- out = check_output(['rbd', 'list', '--id', service,
646- '--pool', pool])
647+ out = check_output(['rbd', 'list', '--id',
648+ service, '--pool', pool]).decode('UTF-8')
649 except CalledProcessError:
650 return False
651- else:
652- return rbd_img in out
653+
654+ return rbd_img in out
655
656
657 def create_rbd_image(service, pool, image, sizemb):
658- ''' Create a new RADOS block device '''
659- cmd = [
660- 'rbd',
661- 'create',
662- image,
663- '--size',
664- str(sizemb),
665- '--id',
666- service,
667- '--pool',
668- pool
669- ]
670+ """Create a new RADOS block device."""
671+ cmd = ['rbd', 'create', image, '--size', str(sizemb), '--id', service,
672+ '--pool', pool]
673 check_call(cmd)
674
675
676 def pool_exists(service, name):
677- ''' Check to see if a RADOS pool already exists '''
678+ """Check to see if a RADOS pool already exists."""
679 try:
680- out = check_output(['rados', '--id', service, 'lspools'])
681+ out = check_output(['rados', '--id', service,
682+ 'lspools']).decode('UTF-8')
683 except CalledProcessError:
684 return False
685- else:
686- return name in out
687+
688+ return name in out
689
690
691 def get_osds(service):
692- '''
693- Return a list of all Ceph Object Storage Daemons
694- currently in the cluster
695- '''
696+ """Return a list of all Ceph Object Storage Daemons currently in the
697+ cluster.
698+ """
699 version = ceph_version()
700 if version and version >= '0.56':
701 return json.loads(check_output(['ceph', '--id', service,
702- 'osd', 'ls', '--format=json']))
703- else:
704- return None
705-
706-
707-def create_pool(service, name, replicas=2):
708- ''' Create a new RADOS pool '''
709+ 'osd', 'ls',
710+ '--format=json']).decode('UTF-8'))
711+
712+ return None
713+
714+
715+def create_pool(service, name, replicas=3):
716+ """Create a new RADOS pool."""
717 if pool_exists(service, name):
718 log("Ceph pool {} already exists, skipping creation".format(name),
719 level=WARNING)
720 return
721+
722 # Calculate the number of placement groups based
723 # on upstream recommended best practices.
724 osds = get_osds(service)
725 if osds:
726- pgnum = (len(osds) * 100 / replicas)
727+ pgnum = (len(osds) * 100 // replicas)
728 else:
729 # NOTE(james-page): Default to 200 for older ceph versions
730 # which don't support OSD query from cli
731 pgnum = 200
732- cmd = [
733- 'ceph', '--id', service,
734- 'osd', 'pool', 'create',
735- name, str(pgnum)
736- ]
737+
738+ cmd = ['ceph', '--id', service, 'osd', 'pool', 'create', name, str(pgnum)]
739 check_call(cmd)
740- cmd = [
741- 'ceph', '--id', service,
742- 'osd', 'pool', 'set', name,
743- 'size', str(replicas)
744- ]
745+
746+ cmd = ['ceph', '--id', service, 'osd', 'pool', 'set', name, 'size',
747+ str(replicas)]
748 check_call(cmd)
749
750
751 def delete_pool(service, name):
752- ''' Delete a RADOS pool from ceph '''
753- cmd = [
754- 'ceph', '--id', service,
755- 'osd', 'pool', 'delete',
756- name, '--yes-i-really-really-mean-it'
757- ]
758+ """Delete a RADOS pool from ceph."""
759+ cmd = ['ceph', '--id', service, 'osd', 'pool', 'delete', name,
760+ '--yes-i-really-really-mean-it']
761 check_call(cmd)
762
763
764@@ -161,44 +161,54 @@
765
766
767 def create_keyring(service, key):
768- ''' Create a new Ceph keyring containing key'''
769+ """Create a new Ceph keyring containing key."""
770 keyring = _keyring_path(service)
771 if os.path.exists(keyring):
772- log('ceph: Keyring exists at %s.' % keyring, level=WARNING)
773+ log('Ceph keyring exists at %s.' % keyring, level=WARNING)
774 return
775- cmd = [
776- 'ceph-authtool',
777- keyring,
778- '--create-keyring',
779- '--name=client.{}'.format(service),
780- '--add-key={}'.format(key)
781- ]
782+
783+ cmd = ['ceph-authtool', keyring, '--create-keyring',
784+ '--name=client.{}'.format(service), '--add-key={}'.format(key)]
785 check_call(cmd)
786- log('ceph: Created new ring at %s.' % keyring, level=INFO)
787+ log('Created new ceph keyring at %s.' % keyring, level=DEBUG)
788+
789+
790+def delete_keyring(service):
791+ """Delete an existing Ceph keyring."""
792+ keyring = _keyring_path(service)
793+ if not os.path.exists(keyring):
794+ log('Keyring does not exist at %s' % keyring, level=WARNING)
795+ return
796+
797+ os.remove(keyring)
798+ log('Deleted ring at %s.' % keyring, level=INFO)
799
800
801 def create_key_file(service, key):
802- ''' Create a file containing key '''
803+ """Create a file containing key."""
804 keyfile = _keyfile_path(service)
805 if os.path.exists(keyfile):
806- log('ceph: Keyfile exists at %s.' % keyfile, level=WARNING)
807+ log('Keyfile exists at %s.' % keyfile, level=WARNING)
808 return
809+
810 with open(keyfile, 'w') as fd:
811 fd.write(key)
812- log('ceph: Created new keyfile at %s.' % keyfile, level=INFO)
813+
814+ log('Created new keyfile at %s.' % keyfile, level=INFO)
815
816
817 def get_ceph_nodes():
818- ''' Query named relation 'ceph' to detemine current nodes '''
819+ """Query named relation 'ceph' to determine current nodes."""
820 hosts = []
821 for r_id in relation_ids('ceph'):
822 for unit in related_units(r_id):
823 hosts.append(relation_get('private-address', unit=unit, rid=r_id))
824+
825 return hosts
826
827
828 def configure(service, key, auth, use_syslog):
829- ''' Perform basic configuration of Ceph '''
830+ """Perform basic configuration of Ceph."""
831 create_keyring(service, key)
832 create_key_file(service, key)
833 hosts = get_ceph_nodes()
834@@ -211,17 +221,17 @@
835
836
837 def image_mapped(name):
838- ''' Determine whether a RADOS block device is mapped locally '''
839+ """Determine whether a RADOS block device is mapped locally."""
840 try:
841- out = check_output(['rbd', 'showmapped'])
842+ out = check_output(['rbd', 'showmapped']).decode('UTF-8')
843 except CalledProcessError:
844 return False
845- else:
846- return name in out
847+
848+ return name in out
849
850
851 def map_block_storage(service, pool, image):
852- ''' Map a RADOS block device for local use '''
853+ """Map a RADOS block device for local use."""
854 cmd = [
855 'rbd',
856 'map',
857@@ -235,31 +245,32 @@
858
859
860 def filesystem_mounted(fs):
861- ''' Determine whether a filesytems is already mounted '''
862+ """Determine whether a filesytems is already mounted."""
863 return fs in [f for f, m in mounts()]
864
865
866 def make_filesystem(blk_device, fstype='ext4', timeout=10):
867- ''' Make a new filesystem on the specified block device '''
868+ """Make a new filesystem on the specified block device."""
869 count = 0
870 e_noent = os.errno.ENOENT
871 while not os.path.exists(blk_device):
872 if count >= timeout:
873- log('ceph: gave up waiting on block device %s' % blk_device,
874+ log('Gave up waiting on block device %s' % blk_device,
875 level=ERROR)
876 raise IOError(e_noent, os.strerror(e_noent), blk_device)
877- log('ceph: waiting for block device %s to appear' % blk_device,
878- level=INFO)
879+
880+ log('Waiting for block device %s to appear' % blk_device,
881+ level=DEBUG)
882 count += 1
883 time.sleep(1)
884 else:
885- log('ceph: Formatting block device %s as filesystem %s.' %
886+ log('Formatting block device %s as filesystem %s.' %
887 (blk_device, fstype), level=INFO)
888 check_call(['mkfs', '-t', fstype, blk_device])
889
890
891 def place_data_on_block_device(blk_device, data_src_dst):
892- ''' Migrate data in data_src_dst to blk_device and then remount '''
893+ """Migrate data in data_src_dst to blk_device and then remount."""
894 # mount block device into /mnt
895 mount(blk_device, '/mnt')
896 # copy data to /mnt
897@@ -279,8 +290,8 @@
898
899 # TODO: re-use
900 def modprobe(module):
901- ''' Load a kernel module and configure for auto-load on reboot '''
902- log('ceph: Loading kernel module', level=INFO)
903+ """Load a kernel module and configure for auto-load on reboot."""
904+ log('Loading kernel module', level=INFO)
905 cmd = ['modprobe', module]
906 check_call(cmd)
907 with open('/etc/modules', 'r+') as modules:
908@@ -289,7 +300,7 @@
909
910
911 def copy_files(src, dst, symlinks=False, ignore=None):
912- ''' Copy files from src to dst '''
913+ """Copy files from src to dst."""
914 for item in os.listdir(src):
915 s = os.path.join(src, item)
916 d = os.path.join(dst, item)
917@@ -300,9 +311,9 @@
918
919
920 def ensure_ceph_storage(service, pool, rbd_img, sizemb, mount_point,
921- blk_device, fstype, system_services=[]):
922- """
923- NOTE: This function must only be called from a single service unit for
924+ blk_device, fstype, system_services=[],
925+ replicas=3):
926+ """NOTE: This function must only be called from a single service unit for
927 the same rbd_img otherwise data loss will occur.
928
929 Ensures given pool and RBD image exists, is mapped to a block device,
930@@ -316,15 +327,16 @@
931 """
932 # Ensure pool, RBD image, RBD mappings are in place.
933 if not pool_exists(service, pool):
934- log('ceph: Creating new pool {}.'.format(pool))
935- create_pool(service, pool)
936+ log('Creating new pool {}.'.format(pool), level=INFO)
937+ create_pool(service, pool, replicas=replicas)
938
939 if not rbd_exists(service, pool, rbd_img):
940- log('ceph: Creating RBD image ({}).'.format(rbd_img))
941+ log('Creating RBD image ({}).'.format(rbd_img), level=INFO)
942 create_rbd_image(service, pool, rbd_img, sizemb)
943
944 if not image_mapped(rbd_img):
945- log('ceph: Mapping RBD Image {} as a Block Device.'.format(rbd_img))
946+ log('Mapping RBD Image {} as a Block Device.'.format(rbd_img),
947+ level=INFO)
948 map_block_storage(service, pool, rbd_img)
949
950 # make file system
951@@ -339,45 +351,47 @@
952
953 for svc in system_services:
954 if service_running(svc):
955- log('ceph: Stopping services {} prior to migrating data.'
956- .format(svc))
957+ log('Stopping services {} prior to migrating data.'
958+ .format(svc), level=DEBUG)
959 service_stop(svc)
960
961 place_data_on_block_device(blk_device, mount_point)
962
963 for svc in system_services:
964- log('ceph: Starting service {} after migrating data.'
965- .format(svc))
966+ log('Starting service {} after migrating data.'
967+ .format(svc), level=DEBUG)
968 service_start(svc)
969
970
971 def ensure_ceph_keyring(service, user=None, group=None):
972- '''
973- Ensures a ceph keyring is created for a named service
974- and optionally ensures user and group ownership.
975+ """Ensures a ceph keyring is created for a named service and optionally
976+ ensures user and group ownership.
977
978 Returns False if no ceph key is available in relation state.
979- '''
980+ """
981 key = None
982 for rid in relation_ids('ceph'):
983 for unit in related_units(rid):
984 key = relation_get('key', rid=rid, unit=unit)
985 if key:
986 break
987+
988 if not key:
989 return False
990+
991 create_keyring(service=service, key=key)
992 keyring = _keyring_path(service)
993 if user and group:
994 check_call(['chown', '%s.%s' % (user, group), keyring])
995+
996 return True
997
998
999 def ceph_version():
1000- ''' Retrieve the local version of ceph '''
1001+ """Retrieve the local version of ceph."""
1002 if os.path.exists('/usr/bin/ceph'):
1003 cmd = ['ceph', '-v']
1004- output = check_output(cmd)
1005+ output = check_output(cmd).decode('US-ASCII')
1006 output = output.split()
1007 if len(output) > 3:
1008 return output[2]
1009@@ -385,3 +399,46 @@
1010 return None
1011 else:
1012 return None
1013+
1014+
1015+class CephBrokerRq(object):
1016+ """Ceph broker request.
1017+
1018+ Multiple operations can be added to a request and sent to the Ceph broker
1019+ to be executed.
1020+
1021+ Request is json-encoded for sending over the wire.
1022+
1023+ The API is versioned and defaults to version 1.
1024+ """
1025+ def __init__(self, api_version=1):
1026+ self.api_version = api_version
1027+ self.ops = []
1028+
1029+ def add_op_create_pool(self, name, replica_count=3):
1030+ self.ops.append({'op': 'create-pool', 'name': name,
1031+ 'replicas': replica_count})
1032+
1033+ @property
1034+ def request(self):
1035+ return json.dumps({'api-version': self.api_version, 'ops': self.ops})
1036+
1037+
1038+class CephBrokerRsp(object):
1039+ """Ceph broker response.
1040+
1041+ Response is json-decoded and contents provided as methods/properties.
1042+
1043+ The API is versioned and defaults to version 1.
1044+ """
1045+ def __init__(self, encoded_rsp):
1046+ self.api_version = None
1047+ self.rsp = json.loads(encoded_rsp)
1048+
1049+ @property
1050+ def exit_code(self):
1051+ return self.rsp.get('exit-code')
1052+
1053+ @property
1054+ def exit_msg(self):
1055+ return self.rsp.get('stderr')
1056
1057=== modified file 'lib/charmhelpers/contrib/storage/linux/loopback.py'
1058--- lib/charmhelpers/contrib/storage/linux/loopback.py 2014-09-15 14:42:27 +0000
1059+++ lib/charmhelpers/contrib/storage/linux/loopback.py 2015-06-04 13:22:19 +0000
1060@@ -1,12 +1,28 @@
1061+# Copyright 2014-2015 Canonical Limited.
1062+#
1063+# This file is part of charm-helpers.
1064+#
1065+# charm-helpers is free software: you can redistribute it and/or modify
1066+# it under the terms of the GNU Lesser General Public License version 3 as
1067+# published by the Free Software Foundation.
1068+#
1069+# charm-helpers is distributed in the hope that it will be useful,
1070+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1071+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1072+# GNU Lesser General Public License for more details.
1073+#
1074+# You should have received a copy of the GNU Lesser General Public License
1075+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1076
1077 import os
1078 import re
1079-
1080 from subprocess import (
1081 check_call,
1082 check_output,
1083 )
1084
1085+import six
1086+
1087
1088 ##################################################
1089 # loopback device helpers.
1090@@ -37,7 +53,7 @@
1091 '''
1092 file_path = os.path.abspath(file_path)
1093 check_call(['losetup', '--find', file_path])
1094- for d, f in loopback_devices().iteritems():
1095+ for d, f in six.iteritems(loopback_devices()):
1096 if f == file_path:
1097 return d
1098
1099@@ -51,7 +67,7 @@
1100
1101 :returns: str: Full path to the ensured loopback device (eg, /dev/loop0)
1102 '''
1103- for d, f in loopback_devices().iteritems():
1104+ for d, f in six.iteritems(loopback_devices()):
1105 if f == path:
1106 return d
1107
1108
1109=== modified file 'lib/charmhelpers/contrib/storage/linux/lvm.py'
1110--- lib/charmhelpers/contrib/storage/linux/lvm.py 2014-09-15 14:42:27 +0000
1111+++ lib/charmhelpers/contrib/storage/linux/lvm.py 2015-06-04 13:22:19 +0000
1112@@ -1,3 +1,19 @@
1113+# Copyright 2014-2015 Canonical Limited.
1114+#
1115+# This file is part of charm-helpers.
1116+#
1117+# charm-helpers is free software: you can redistribute it and/or modify
1118+# it under the terms of the GNU Lesser General Public License version 3 as
1119+# published by the Free Software Foundation.
1120+#
1121+# charm-helpers is distributed in the hope that it will be useful,
1122+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1123+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1124+# GNU Lesser General Public License for more details.
1125+#
1126+# You should have received a copy of the GNU Lesser General Public License
1127+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1128+
1129 from subprocess import (
1130 CalledProcessError,
1131 check_call,
1132@@ -61,6 +77,7 @@
1133 vg = None
1134 pvd = check_output(['pvdisplay', block_device]).splitlines()
1135 for l in pvd:
1136+ l = l.decode('UTF-8')
1137 if l.strip().startswith('VG Name'):
1138 vg = ' '.join(l.strip().split()[2:])
1139 return vg
1140
1141=== modified file 'lib/charmhelpers/contrib/storage/linux/utils.py'
1142--- lib/charmhelpers/contrib/storage/linux/utils.py 2014-09-16 13:52:19 +0000
1143+++ lib/charmhelpers/contrib/storage/linux/utils.py 2015-06-04 13:22:19 +0000
1144@@ -1,3 +1,19 @@
1145+# Copyright 2014-2015 Canonical Limited.
1146+#
1147+# This file is part of charm-helpers.
1148+#
1149+# charm-helpers is free software: you can redistribute it and/or modify
1150+# it under the terms of the GNU Lesser General Public License version 3 as
1151+# published by the Free Software Foundation.
1152+#
1153+# charm-helpers is distributed in the hope that it will be useful,
1154+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1155+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1156+# GNU Lesser General Public License for more details.
1157+#
1158+# You should have received a copy of the GNU Lesser General Public License
1159+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1160+
1161 import os
1162 import re
1163 from stat import S_ISBLK
1164@@ -30,7 +46,8 @@
1165 # sometimes sgdisk exits non-zero; this is OK, dd will clean up
1166 call(['sgdisk', '--zap-all', '--mbrtogpt',
1167 '--clear', block_device])
1168- dev_end = check_output(['blockdev', '--getsz', block_device])
1169+ dev_end = check_output(['blockdev', '--getsz',
1170+ block_device]).decode('UTF-8')
1171 gpt_end = int(dev_end.split()[0]) - 100
1172 check_call(['dd', 'if=/dev/zero', 'of=%s' % (block_device),
1173 'bs=1M', 'count=1'])
1174@@ -47,7 +64,7 @@
1175 it doesn't.
1176 '''
1177 is_partition = bool(re.search(r".*[0-9]+\b", device))
1178- out = check_output(['mount'])
1179- if not is_partition:
1180+ out = check_output(['mount']).decode('UTF-8')
1181+ if is_partition:
1182 return bool(re.search(device + r"\b", out))
1183 return bool(re.search(device + r"[0-9]+\b", out))
1184
1185=== modified file 'lib/charmhelpers/contrib/unison/__init__.py'
1186--- lib/charmhelpers/contrib/unison/__init__.py 2014-09-05 14:44:30 +0000
1187+++ lib/charmhelpers/contrib/unison/__init__.py 2015-06-04 13:22:19 +0000
1188@@ -1,3 +1,19 @@
1189+# Copyright 2014-2015 Canonical Limited.
1190+#
1191+# This file is part of charm-helpers.
1192+#
1193+# charm-helpers is free software: you can redistribute it and/or modify
1194+# it under the terms of the GNU Lesser General Public License version 3 as
1195+# published by the Free Software Foundation.
1196+#
1197+# charm-helpers is distributed in the hope that it will be useful,
1198+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1199+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1200+# GNU Lesser General Public License for more details.
1201+#
1202+# You should have received a copy of the GNU Lesser General Public License
1203+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1204+
1205 # Easy file synchronization among peer units using ssh + unison.
1206 #
1207 # For the -joined, -changed, and -departed peer relations, add a call to
1208@@ -54,6 +70,7 @@
1209 from charmhelpers.core.host import (
1210 adduser,
1211 add_user_to_group,
1212+ pwgen,
1213 )
1214
1215 from charmhelpers.core.hookenv import (
1216@@ -64,6 +81,7 @@
1217 relation_set,
1218 relation_get,
1219 unit_private_ip,
1220+ INFO,
1221 ERROR,
1222 )
1223
1224@@ -72,13 +90,12 @@
1225 '-prefer=newer', '-times=true']
1226
1227
1228-def get_homedir(username):
1229+def get_homedir(user):
1230 try:
1231- user = pwd.getpwnam(username)
1232- log('get_homedir: {} homdir is {}'.format(username, user.pw_dir))
1233+ user = pwd.getpwnam(user)
1234 return user.pw_dir
1235 except KeyError:
1236- log('Could not get homedir for user %s: user exists?', ERROR)
1237+ log('Could not get homedir for user %s: user exists?' % (user), ERROR)
1238 raise Exception
1239
1240
1241@@ -131,7 +148,7 @@
1242 ssh_dir = os.path.join(home_dir, '.ssh')
1243 auth_keys = os.path.join(ssh_dir, 'authorized_keys')
1244 log('Syncing authorized_keys @ %s.' % auth_keys)
1245- with open(auth_keys, 'wb') as out:
1246+ with open(auth_keys, 'w') as out:
1247 for k in keys:
1248 out.write('%s\n' % k)
1249
1250@@ -143,16 +160,16 @@
1251 khosts = []
1252 for host in hosts:
1253 cmd = ['ssh-keyscan', host]
1254- remote_key = check_output(cmd).strip()
1255+ remote_key = check_output(cmd, universal_newlines=True).strip()
1256 khosts.append(remote_key)
1257 log('Syncing known_hosts @ %s.' % known_hosts)
1258- with open(known_hosts, 'wb') as out:
1259+ with open(known_hosts, 'w') as out:
1260 for host in khosts:
1261 out.write('%s\n' % host)
1262
1263
1264 def ensure_user(user, group=None):
1265- adduser(user)
1266+ adduser(user, pwgen())
1267 if group:
1268 add_user_to_group(user, group)
1269
1270@@ -194,13 +211,14 @@
1271 relation_set(ssh_authorized_hosts=authed_hosts)
1272
1273
1274-def _run_as_user(user):
1275+def _run_as_user(user, gid=None):
1276 try:
1277 user = pwd.getpwnam(user)
1278 except KeyError:
1279 log('Invalid user: %s' % user)
1280 raise Exception
1281- uid, gid = user.pw_uid, user.pw_gid
1282+ uid = user.pw_uid
1283+ gid = gid or user.pw_gid
1284 os.environ['HOME'] = user.pw_dir
1285
1286 def _inner():
1287@@ -209,8 +227,8 @@
1288 return _inner
1289
1290
1291-def run_as_user(user, cmd):
1292- return check_output(cmd, preexec_fn=_run_as_user(user), cwd='/')
1293+def run_as_user(user, cmd, gid=None):
1294+ return check_output(cmd, preexec_fn=_run_as_user(user, gid), cwd='/')
1295
1296
1297 def collect_authed_hosts(peer_interface):
1298@@ -225,19 +243,25 @@
1299 rid=r_id, unit=unit)
1300
1301 if not authed_hosts:
1302- log('Peer %s has not authorized *any* hosts yet, skipping.')
1303+ log('Peer %s has not authorized *any* hosts yet, skipping.' %
1304+ (unit), level=INFO)
1305 continue
1306
1307 if unit_private_ip() in authed_hosts.split(':'):
1308 hosts.append(private_addr)
1309 else:
1310- log('Peer %s has not authorized *this* host yet, skipping.')
1311-
1312+ log('Peer %s has not authorized *this* host yet, skipping.' %
1313+ (unit), level=INFO)
1314 return hosts
1315
1316
1317-def sync_path_to_host(path, host, user, verbose=False):
1318- cmd = copy(BASE_CMD)
1319+def sync_path_to_host(path, host, user, verbose=False, cmd=None, gid=None,
1320+ fatal=False):
1321+ """Sync path to an specific peer host
1322+
1323+ Propagates exception if operation fails and fatal=True.
1324+ """
1325+ cmd = cmd or copy(BASE_CMD)
1326 if not verbose:
1327 cmd.append('-silent')
1328
1329@@ -250,17 +274,33 @@
1330
1331 try:
1332 log('Syncing local path %s to %s@%s:%s' % (path, user, host, path))
1333- run_as_user(user, cmd)
1334+ run_as_user(user, cmd, gid)
1335 except:
1336 log('Error syncing remote files')
1337-
1338-
1339-def sync_to_peer(host, user, paths=[], verbose=False):
1340- '''Sync paths to an specific host'''
1341- [sync_path_to_host(p, host, user, verbose) for p in paths]
1342-
1343-
1344-def sync_to_peers(peer_interface, user, paths=[], verbose=False):
1345- '''Sync all hosts to an specific path'''
1346- for host in collect_authed_hosts(peer_interface):
1347- sync_to_peer(host, user, paths, verbose)
1348+ if fatal:
1349+ raise
1350+
1351+
1352+def sync_to_peer(host, user, paths=None, verbose=False, cmd=None, gid=None,
1353+ fatal=False):
1354+ """Sync paths to an specific peer host
1355+
1356+ Propagates exception if any operation fails and fatal=True.
1357+ """
1358+ if paths:
1359+ for p in paths:
1360+ sync_path_to_host(p, host, user, verbose, cmd, gid, fatal)
1361+
1362+
1363+def sync_to_peers(peer_interface, user, paths=None, verbose=False, cmd=None,
1364+ gid=None, fatal=False):
1365+ """Sync all hosts to an specific path
1366+
1367+ The type of group is integer, it allows user has permissions to
1368+ operate a directory have a different group id with the user id.
1369+
1370+ Propagates exception if any operation fails and fatal=True.
1371+ """
1372+ if paths:
1373+ for host in collect_authed_hosts(peer_interface):
1374+ sync_to_peer(host, user, paths, verbose, cmd, gid, fatal)
1375
1376=== modified file 'lib/charmhelpers/core/__init__.py'
1377--- lib/charmhelpers/core/__init__.py 2014-08-17 21:29:19 +0000
1378+++ lib/charmhelpers/core/__init__.py 2015-06-04 13:22:19 +0000
1379@@ -0,0 +1,15 @@
1380+# Copyright 2014-2015 Canonical Limited.
1381+#
1382+# This file is part of charm-helpers.
1383+#
1384+# charm-helpers is free software: you can redistribute it and/or modify
1385+# it under the terms of the GNU Lesser General Public License version 3 as
1386+# published by the Free Software Foundation.
1387+#
1388+# charm-helpers is distributed in the hope that it will be useful,
1389+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1390+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1391+# GNU Lesser General Public License for more details.
1392+#
1393+# You should have received a copy of the GNU Lesser General Public License
1394+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1395
1396=== modified file 'lib/charmhelpers/core/fstab.py'
1397--- lib/charmhelpers/core/fstab.py 2014-08-17 21:29:19 +0000
1398+++ lib/charmhelpers/core/fstab.py 2015-06-04 13:22:19 +0000
1399@@ -1,12 +1,29 @@
1400 #!/usr/bin/env python
1401 # -*- coding: utf-8 -*-
1402
1403+# Copyright 2014-2015 Canonical Limited.
1404+#
1405+# This file is part of charm-helpers.
1406+#
1407+# charm-helpers is free software: you can redistribute it and/or modify
1408+# it under the terms of the GNU Lesser General Public License version 3 as
1409+# published by the Free Software Foundation.
1410+#
1411+# charm-helpers is distributed in the hope that it will be useful,
1412+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1413+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1414+# GNU Lesser General Public License for more details.
1415+#
1416+# You should have received a copy of the GNU Lesser General Public License
1417+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1418+
1419+import io
1420+import os
1421+
1422 __author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
1423
1424-import os
1425-
1426-
1427-class Fstab(file):
1428+
1429+class Fstab(io.FileIO):
1430 """This class extends file in order to implement a file reader/writer
1431 for file `/etc/fstab`
1432 """
1433@@ -24,8 +41,8 @@
1434 options = "defaults"
1435
1436 self.options = options
1437- self.d = d
1438- self.p = p
1439+ self.d = int(d)
1440+ self.p = int(p)
1441
1442 def __eq__(self, o):
1443 return str(self) == str(o)
1444@@ -45,7 +62,7 @@
1445 self._path = path
1446 else:
1447 self._path = self.DEFAULT_PATH
1448- file.__init__(self, self._path, 'r+')
1449+ super(Fstab, self).__init__(self._path, 'rb+')
1450
1451 def _hydrate_entry(self, line):
1452 # NOTE: use split with no arguments to split on any
1453@@ -58,8 +75,9 @@
1454 def entries(self):
1455 self.seek(0)
1456 for line in self.readlines():
1457+ line = line.decode('us-ascii')
1458 try:
1459- if not line.startswith("#"):
1460+ if line.strip() and not line.strip().startswith("#"):
1461 yield self._hydrate_entry(line)
1462 except ValueError:
1463 pass
1464@@ -75,18 +93,18 @@
1465 if self.get_entry_by_attr('device', entry.device):
1466 return False
1467
1468- self.write(str(entry) + '\n')
1469+ self.write((str(entry) + '\n').encode('us-ascii'))
1470 self.truncate()
1471 return entry
1472
1473 def remove_entry(self, entry):
1474 self.seek(0)
1475
1476- lines = self.readlines()
1477+ lines = [l.decode('us-ascii') for l in self.readlines()]
1478
1479 found = False
1480 for index, line in enumerate(lines):
1481- if not line.startswith("#"):
1482+ if line.strip() and not line.strip().startswith("#"):
1483 if self._hydrate_entry(line) == entry:
1484 found = True
1485 break
1486@@ -97,7 +115,7 @@
1487 lines.remove(line)
1488
1489 self.seek(0)
1490- self.write(''.join(lines))
1491+ self.write(''.join(lines).encode('us-ascii'))
1492 self.truncate()
1493 return True
1494
1495
1496=== modified file 'lib/charmhelpers/core/hookenv.py'
1497--- lib/charmhelpers/core/hookenv.py 2014-09-15 14:42:27 +0000
1498+++ lib/charmhelpers/core/hookenv.py 2015-06-04 13:22:19 +0000
1499@@ -1,17 +1,42 @@
1500+# Copyright 2014-2015 Canonical Limited.
1501+#
1502+# This file is part of charm-helpers.
1503+#
1504+# charm-helpers is free software: you can redistribute it and/or modify
1505+# it under the terms of the GNU Lesser General Public License version 3 as
1506+# published by the Free Software Foundation.
1507+#
1508+# charm-helpers is distributed in the hope that it will be useful,
1509+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1510+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1511+# GNU Lesser General Public License for more details.
1512+#
1513+# You should have received a copy of the GNU Lesser General Public License
1514+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1515+
1516 "Interactions with the Juju environment"
1517 # Copyright 2013 Canonical Ltd.
1518 #
1519 # Authors:
1520 # Charm Helpers Developers <juju@lists.ubuntu.com>
1521
1522+from __future__ import print_function
1523+from functools import wraps
1524 import os
1525 import json
1526 import yaml
1527 import subprocess
1528 import sys
1529-import UserDict
1530+import errno
1531+import tempfile
1532 from subprocess import CalledProcessError
1533
1534+import six
1535+if not six.PY3:
1536+ from UserDict import UserDict
1537+else:
1538+ from collections import UserDict
1539+
1540 CRITICAL = "CRITICAL"
1541 ERROR = "ERROR"
1542 WARNING = "WARNING"
1543@@ -35,15 +60,17 @@
1544
1545 will cache the result of unit_get + 'test' for future calls.
1546 """
1547+ @wraps(func)
1548 def wrapper(*args, **kwargs):
1549 global cache
1550 key = str((func, args, kwargs))
1551 try:
1552 return cache[key]
1553 except KeyError:
1554- res = func(*args, **kwargs)
1555- cache[key] = res
1556- return res
1557+ pass # Drop out of the exception handler scope.
1558+ res = func(*args, **kwargs)
1559+ cache[key] = res
1560+ return res
1561 return wrapper
1562
1563
1564@@ -63,16 +90,29 @@
1565 command = ['juju-log']
1566 if level:
1567 command += ['-l', level]
1568+ if not isinstance(message, six.string_types):
1569+ message = repr(message)
1570 command += [message]
1571- subprocess.call(command)
1572-
1573-
1574-class Serializable(UserDict.IterableUserDict):
1575+ # Missing juju-log should not cause failures in unit tests
1576+ # Send log output to stderr
1577+ try:
1578+ subprocess.call(command)
1579+ except OSError as e:
1580+ if e.errno == errno.ENOENT:
1581+ if level:
1582+ message = "{}: {}".format(level, message)
1583+ message = "juju-log: {}".format(message)
1584+ print(message, file=sys.stderr)
1585+ else:
1586+ raise
1587+
1588+
1589+class Serializable(UserDict):
1590 """Wrapper, an object that can be serialized to yaml or json"""
1591
1592 def __init__(self, obj):
1593 # wrap the object
1594- UserDict.IterableUserDict.__init__(self)
1595+ UserDict.__init__(self)
1596 self.data = obj
1597
1598 def __getattr__(self, attr):
1599@@ -142,7 +182,7 @@
1600
1601 def remote_unit():
1602 """The remote unit for the current relation hook"""
1603- return os.environ['JUJU_REMOTE_UNIT']
1604+ return os.environ.get('JUJU_REMOTE_UNIT', None)
1605
1606
1607 def service_name():
1608@@ -214,6 +254,18 @@
1609 except KeyError:
1610 return (self._prev_dict or {})[key]
1611
1612+ def get(self, key, default=None):
1613+ try:
1614+ return self[key]
1615+ except KeyError:
1616+ return default
1617+
1618+ def keys(self):
1619+ prev_keys = []
1620+ if self._prev_dict is not None:
1621+ prev_keys = self._prev_dict.keys()
1622+ return list(set(prev_keys + list(dict.keys(self))))
1623+
1624 def load_previous(self, path=None):
1625 """Load previous copy of config from disk.
1626
1627@@ -263,7 +315,7 @@
1628
1629 """
1630 if self._prev_dict:
1631- for k, v in self._prev_dict.iteritems():
1632+ for k, v in six.iteritems(self._prev_dict):
1633 if k not in self:
1634 self[k] = v
1635 with open(self.path, 'w') as f:
1636@@ -278,7 +330,8 @@
1637 config_cmd_line.append(scope)
1638 config_cmd_line.append('--format=json')
1639 try:
1640- config_data = json.loads(subprocess.check_output(config_cmd_line))
1641+ config_data = json.loads(
1642+ subprocess.check_output(config_cmd_line).decode('UTF-8'))
1643 if scope is not None:
1644 return config_data
1645 return Config(config_data)
1646@@ -297,10 +350,10 @@
1647 if unit:
1648 _args.append(unit)
1649 try:
1650- return json.loads(subprocess.check_output(_args))
1651+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
1652 except ValueError:
1653 return None
1654- except CalledProcessError, e:
1655+ except CalledProcessError as e:
1656 if e.returncode == 2:
1657 return None
1658 raise
1659@@ -310,18 +363,49 @@
1660 """Set relation information for the current unit"""
1661 relation_settings = relation_settings if relation_settings else {}
1662 relation_cmd_line = ['relation-set']
1663+ accepts_file = "--file" in subprocess.check_output(
1664+ relation_cmd_line + ["--help"], universal_newlines=True)
1665 if relation_id is not None:
1666 relation_cmd_line.extend(('-r', relation_id))
1667- for k, v in (relation_settings.items() + kwargs.items()):
1668- if v is None:
1669- relation_cmd_line.append('{}='.format(k))
1670- else:
1671- relation_cmd_line.append('{}={}'.format(k, v))
1672- subprocess.check_call(relation_cmd_line)
1673+ settings = relation_settings.copy()
1674+ settings.update(kwargs)
1675+ for key, value in settings.items():
1676+ # Force value to be a string: it always should, but some call
1677+ # sites pass in things like dicts or numbers.
1678+ if value is not None:
1679+ settings[key] = "{}".format(value)
1680+ if accepts_file:
1681+ # --file was introduced in Juju 1.23.2. Use it by default if
1682+ # available, since otherwise we'll break if the relation data is
1683+ # too big. Ideally we should tell relation-set to read the data from
1684+ # stdin, but that feature is broken in 1.23.2: Bug #1454678.
1685+ with tempfile.NamedTemporaryFile(delete=False) as settings_file:
1686+ settings_file.write(yaml.safe_dump(settings).encode("utf-8"))
1687+ subprocess.check_call(
1688+ relation_cmd_line + ["--file", settings_file.name])
1689+ os.remove(settings_file.name)
1690+ else:
1691+ for key, value in settings.items():
1692+ if value is None:
1693+ relation_cmd_line.append('{}='.format(key))
1694+ else:
1695+ relation_cmd_line.append('{}={}'.format(key, value))
1696+ subprocess.check_call(relation_cmd_line)
1697 # Flush cache of any relation-gets for local unit
1698 flush(local_unit())
1699
1700
1701+def relation_clear(r_id=None):
1702+ ''' Clears any relation data already set on relation r_id '''
1703+ settings = relation_get(rid=r_id,
1704+ unit=local_unit())
1705+ for setting in settings:
1706+ if setting not in ['public-address', 'private-address']:
1707+ settings[setting] = None
1708+ relation_set(relation_id=r_id,
1709+ **settings)
1710+
1711+
1712 @cached
1713 def relation_ids(reltype=None):
1714 """A list of relation_ids"""
1715@@ -329,7 +413,8 @@
1716 relid_cmd_line = ['relation-ids', '--format=json']
1717 if reltype is not None:
1718 relid_cmd_line.append(reltype)
1719- return json.loads(subprocess.check_output(relid_cmd_line)) or []
1720+ return json.loads(
1721+ subprocess.check_output(relid_cmd_line).decode('UTF-8')) or []
1722 return []
1723
1724
1725@@ -340,7 +425,8 @@
1726 units_cmd_line = ['relation-list', '--format=json']
1727 if relid is not None:
1728 units_cmd_line.extend(('-r', relid))
1729- return json.loads(subprocess.check_output(units_cmd_line)) or []
1730+ return json.loads(
1731+ subprocess.check_output(units_cmd_line).decode('UTF-8')) or []
1732
1733
1734 @cached
1735@@ -380,21 +466,31 @@
1736
1737
1738 @cached
1739+def metadata():
1740+ """Get the current charm metadata.yaml contents as a python object"""
1741+ with open(os.path.join(charm_dir(), 'metadata.yaml')) as md:
1742+ return yaml.safe_load(md)
1743+
1744+
1745+@cached
1746 def relation_types():
1747 """Get a list of relation types supported by this charm"""
1748- charmdir = os.environ.get('CHARM_DIR', '')
1749- mdf = open(os.path.join(charmdir, 'metadata.yaml'))
1750- md = yaml.safe_load(mdf)
1751 rel_types = []
1752+ md = metadata()
1753 for key in ('provides', 'requires', 'peers'):
1754 section = md.get(key)
1755 if section:
1756 rel_types.extend(section.keys())
1757- mdf.close()
1758 return rel_types
1759
1760
1761 @cached
1762+def charm_name():
1763+ """Get the name of the current charm as is specified on metadata.yaml"""
1764+ return metadata().get('name')
1765+
1766+
1767+@cached
1768 def relations():
1769 """Get a nested dictionary of relation data for all related units"""
1770 rels = {}
1771@@ -449,11 +545,16 @@
1772 """Get the unit ID for the remote unit"""
1773 _args = ['unit-get', '--format=json', attribute]
1774 try:
1775- return json.loads(subprocess.check_output(_args))
1776+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
1777 except ValueError:
1778 return None
1779
1780
1781+def unit_public_ip():
1782+ """Get this unit's public IP address"""
1783+ return unit_get('public-address')
1784+
1785+
1786 def unit_private_ip():
1787 """Get this unit's private IP address"""
1788 return unit_get('private-address')
1789@@ -486,9 +587,10 @@
1790 hooks.execute(sys.argv)
1791 """
1792
1793- def __init__(self):
1794+ def __init__(self, config_save=True):
1795 super(Hooks, self).__init__()
1796 self._hooks = {}
1797+ self._config_save = config_save
1798
1799 def register(self, name, function):
1800 """Register a hook"""
1801@@ -499,9 +601,10 @@
1802 hook_name = os.path.basename(args[0])
1803 if hook_name in self._hooks:
1804 self._hooks[hook_name]()
1805- cfg = config()
1806- if cfg.implicit_save:
1807- cfg.save()
1808+ if self._config_save:
1809+ cfg = config()
1810+ if cfg.implicit_save:
1811+ cfg.save()
1812 else:
1813 raise UnregisteredHookError(hook_name)
1814
1815@@ -522,3 +625,120 @@
1816 def charm_dir():
1817 """Return the root directory of the current charm"""
1818 return os.environ.get('CHARM_DIR')
1819+
1820+
1821+@cached
1822+def action_get(key=None):
1823+ """Gets the value of an action parameter, or all key/value param pairs"""
1824+ cmd = ['action-get']
1825+ if key is not None:
1826+ cmd.append(key)
1827+ cmd.append('--format=json')
1828+ action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
1829+ return action_data
1830+
1831+
1832+def action_set(values):
1833+ """Sets the values to be returned after the action finishes"""
1834+ cmd = ['action-set']
1835+ for k, v in list(values.items()):
1836+ cmd.append('{}={}'.format(k, v))
1837+ subprocess.check_call(cmd)
1838+
1839+
1840+def action_fail(message):
1841+ """Sets the action status to failed and sets the error message.
1842+
1843+ The results set by action_set are preserved."""
1844+ subprocess.check_call(['action-fail', message])
1845+
1846+
1847+def status_set(workload_state, message):
1848+ """Set the workload state with a message
1849+
1850+ Use status-set to set the workload state with a message which is visible
1851+ to the user via juju status. If the status-set command is not found then
1852+ assume this is juju < 1.23 and juju-log the message unstead.
1853+
1854+ workload_state -- valid juju workload state.
1855+ message -- status update message
1856+ """
1857+ valid_states = ['maintenance', 'blocked', 'waiting', 'active']
1858+ if workload_state not in valid_states:
1859+ raise ValueError(
1860+ '{!r} is not a valid workload state'.format(workload_state)
1861+ )
1862+ cmd = ['status-set', workload_state, message]
1863+ try:
1864+ ret = subprocess.call(cmd)
1865+ if ret == 0:
1866+ return
1867+ except OSError as e:
1868+ if e.errno != errno.ENOENT:
1869+ raise
1870+ log_message = 'status-set failed: {} {}'.format(workload_state,
1871+ message)
1872+ log(log_message, level='INFO')
1873+
1874+
1875+def status_get():
1876+ """Retrieve the previously set juju workload state
1877+
1878+ If the status-set command is not found then assume this is juju < 1.23 and
1879+ return 'unknown'
1880+ """
1881+ cmd = ['status-get']
1882+ try:
1883+ raw_status = subprocess.check_output(cmd, universal_newlines=True)
1884+ status = raw_status.rstrip()
1885+ return status
1886+ except OSError as e:
1887+ if e.errno == errno.ENOENT:
1888+ return 'unknown'
1889+ else:
1890+ raise
1891+
1892+
1893+def translate_exc(from_exc, to_exc):
1894+ def inner_translate_exc1(f):
1895+ def inner_translate_exc2(*args, **kwargs):
1896+ try:
1897+ return f(*args, **kwargs)
1898+ except from_exc:
1899+ raise to_exc
1900+
1901+ return inner_translate_exc2
1902+
1903+ return inner_translate_exc1
1904+
1905+
1906+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1907+def is_leader():
1908+ """Does the current unit hold the juju leadership
1909+
1910+ Uses juju to determine whether the current unit is the leader of its peers
1911+ """
1912+ cmd = ['is-leader', '--format=json']
1913+ return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
1914+
1915+
1916+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1917+def leader_get(attribute=None):
1918+ """Juju leader get value(s)"""
1919+ cmd = ['leader-get', '--format=json'] + [attribute or '-']
1920+ return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
1921+
1922+
1923+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1924+def leader_set(settings=None, **kwargs):
1925+ """Juju leader set value(s)"""
1926+ log("Juju leader-set '%s'" % (settings), level=DEBUG)
1927+ cmd = ['leader-set']
1928+ settings = settings or {}
1929+ settings.update(kwargs)
1930+ for k, v in settings.iteritems():
1931+ if v is None:
1932+ cmd.append('{}='.format(k))
1933+ else:
1934+ cmd.append('{}={}'.format(k, v))
1935+ subprocess.check_call(cmd)
1936
1937=== modified file 'lib/charmhelpers/core/host.py'
1938--- lib/charmhelpers/core/host.py 2014-08-26 19:46:22 +0000
1939+++ lib/charmhelpers/core/host.py 2015-06-04 13:22:19 +0000
1940@@ -1,3 +1,19 @@
1941+# Copyright 2014-2015 Canonical Limited.
1942+#
1943+# This file is part of charm-helpers.
1944+#
1945+# charm-helpers is free software: you can redistribute it and/or modify
1946+# it under the terms of the GNU Lesser General Public License version 3 as
1947+# published by the Free Software Foundation.
1948+#
1949+# charm-helpers is distributed in the hope that it will be useful,
1950+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1951+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1952+# GNU Lesser General Public License for more details.
1953+#
1954+# You should have received a copy of the GNU Lesser General Public License
1955+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1956+
1957 """Tools for working with the host system"""
1958 # Copyright 2012 Canonical Ltd.
1959 #
1960@@ -6,19 +22,20 @@
1961 # Matthew Wedgwood <matthew.wedgwood@canonical.com>
1962
1963 import os
1964+import re
1965 import pwd
1966 import grp
1967 import random
1968 import string
1969 import subprocess
1970 import hashlib
1971-import shutil
1972 from contextlib import contextmanager
1973-
1974 from collections import OrderedDict
1975
1976-from hookenv import log
1977-from fstab import Fstab
1978+import six
1979+
1980+from .hookenv import log
1981+from .fstab import Fstab
1982
1983
1984 def service_start(service_name):
1985@@ -54,7 +71,9 @@
1986 def service_running(service):
1987 """Determine whether a system service is running"""
1988 try:
1989- output = subprocess.check_output(['service', service, 'status'], stderr=subprocess.STDOUT)
1990+ output = subprocess.check_output(
1991+ ['service', service, 'status'],
1992+ stderr=subprocess.STDOUT).decode('UTF-8')
1993 except subprocess.CalledProcessError:
1994 return False
1995 else:
1996@@ -67,9 +86,11 @@
1997 def service_available(service_name):
1998 """Determine whether a system service is available"""
1999 try:
2000- subprocess.check_output(['service', service_name, 'status'], stderr=subprocess.STDOUT)
2001- except subprocess.CalledProcessError:
2002- return False
2003+ subprocess.check_output(
2004+ ['service', service_name, 'status'],
2005+ stderr=subprocess.STDOUT).decode('UTF-8')
2006+ except subprocess.CalledProcessError as e:
2007+ return b'unrecognized service' not in e.output
2008 else:
2009 return True
2010
2011@@ -96,6 +117,26 @@
2012 return user_info
2013
2014
2015+def add_group(group_name, system_group=False):
2016+ """Add a group to the system"""
2017+ try:
2018+ group_info = grp.getgrnam(group_name)
2019+ log('group {0} already exists!'.format(group_name))
2020+ except KeyError:
2021+ log('creating group {0}'.format(group_name))
2022+ cmd = ['addgroup']
2023+ if system_group:
2024+ cmd.append('--system')
2025+ else:
2026+ cmd.extend([
2027+ '--group',
2028+ ])
2029+ cmd.append(group_name)
2030+ subprocess.check_call(cmd)
2031+ group_info = grp.getgrnam(group_name)
2032+ return group_info
2033+
2034+
2035 def add_user_to_group(username, group):
2036 """Add a user to a group"""
2037 cmd = [
2038@@ -115,7 +156,7 @@
2039 cmd.append(from_path)
2040 cmd.append(to_path)
2041 log(" ".join(cmd))
2042- return subprocess.check_output(cmd).strip()
2043+ return subprocess.check_output(cmd).decode('UTF-8').strip()
2044
2045
2046 def symlink(source, destination):
2047@@ -130,28 +171,31 @@
2048 subprocess.check_call(cmd)
2049
2050
2051-def mkdir(path, owner='root', group='root', perms=0555, force=False):
2052+def mkdir(path, owner='root', group='root', perms=0o555, force=False):
2053 """Create a directory"""
2054 log("Making dir {} {}:{} {:o}".format(path, owner, group,
2055 perms))
2056 uid = pwd.getpwnam(owner).pw_uid
2057 gid = grp.getgrnam(group).gr_gid
2058 realpath = os.path.abspath(path)
2059- if os.path.exists(realpath):
2060- if force and not os.path.isdir(realpath):
2061+ path_exists = os.path.exists(realpath)
2062+ if path_exists and force:
2063+ if not os.path.isdir(realpath):
2064 log("Removing non-directory file {} prior to mkdir()".format(path))
2065 os.unlink(realpath)
2066- else:
2067+ os.makedirs(realpath, perms)
2068+ elif not path_exists:
2069 os.makedirs(realpath, perms)
2070 os.chown(realpath, uid, gid)
2071-
2072-
2073-def write_file(path, content, owner='root', group='root', perms=0444):
2074- """Create or overwrite a file with the contents of a string"""
2075+ os.chmod(realpath, perms)
2076+
2077+
2078+def write_file(path, content, owner='root', group='root', perms=0o444):
2079+ """Create or overwrite a file with the contents of a byte string."""
2080 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
2081 uid = pwd.getpwnam(owner).pw_uid
2082 gid = grp.getgrnam(group).gr_gid
2083- with open(path, 'w') as target:
2084+ with open(path, 'wb') as target:
2085 os.fchown(target.fileno(), uid, gid)
2086 os.fchmod(target.fileno(), perms)
2087 target.write(content)
2088@@ -177,7 +221,7 @@
2089 cmd_args.extend([device, mountpoint])
2090 try:
2091 subprocess.check_output(cmd_args)
2092- except subprocess.CalledProcessError, e:
2093+ except subprocess.CalledProcessError as e:
2094 log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
2095 return False
2096
2097@@ -191,7 +235,7 @@
2098 cmd_args = ['umount', mountpoint]
2099 try:
2100 subprocess.check_output(cmd_args)
2101- except subprocess.CalledProcessError, e:
2102+ except subprocess.CalledProcessError as e:
2103 log('Error unmounting {}\n{}'.format(mountpoint, e.output))
2104 return False
2105
2106@@ -209,17 +253,42 @@
2107 return system_mounts
2108
2109
2110-def file_hash(path):
2111- """Generate a md5 hash of the contents of 'path' or None if not found """
2112+def file_hash(path, hash_type='md5'):
2113+ """
2114+ Generate a hash checksum of the contents of 'path' or None if not found.
2115+
2116+ :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
2117+ such as md5, sha1, sha256, sha512, etc.
2118+ """
2119 if os.path.exists(path):
2120- h = hashlib.md5()
2121- with open(path, 'r') as source:
2122- h.update(source.read()) # IGNORE:E1101 - it does have update
2123+ h = getattr(hashlib, hash_type)()
2124+ with open(path, 'rb') as source:
2125+ h.update(source.read())
2126 return h.hexdigest()
2127 else:
2128 return None
2129
2130
2131+def check_hash(path, checksum, hash_type='md5'):
2132+ """
2133+ Validate a file using a cryptographic checksum.
2134+
2135+ :param str checksum: Value of the checksum used to validate the file.
2136+ :param str hash_type: Hash algorithm used to generate `checksum`.
2137+ Can be any hash alrgorithm supported by :mod:`hashlib`,
2138+ such as md5, sha1, sha256, sha512, etc.
2139+ :raises ChecksumError: If the file fails the checksum
2140+
2141+ """
2142+ actual_checksum = file_hash(path, hash_type)
2143+ if checksum != actual_checksum:
2144+ raise ChecksumError("'%s' != '%s'" % (checksum, actual_checksum))
2145+
2146+
2147+class ChecksumError(ValueError):
2148+ pass
2149+
2150+
2151 def restart_on_change(restart_map, stopstart=False):
2152 """Restart services based on configuration files changing
2153
2154@@ -236,11 +305,11 @@
2155 ceph_client_changed function.
2156 """
2157 def wrap(f):
2158- def wrapped_f(*args):
2159+ def wrapped_f(*args, **kwargs):
2160 checksums = {}
2161 for path in restart_map:
2162 checksums[path] = file_hash(path)
2163- f(*args)
2164+ f(*args, **kwargs)
2165 restarts = []
2166 for path in restart_map:
2167 if checksums[path] != file_hash(path):
2168@@ -270,29 +339,39 @@
2169 def pwgen(length=None):
2170 """Generate a random pasword."""
2171 if length is None:
2172+ # A random length is ok to use a weak PRNG
2173 length = random.choice(range(35, 45))
2174 alphanumeric_chars = [
2175- l for l in (string.letters + string.digits)
2176+ l for l in (string.ascii_letters + string.digits)
2177 if l not in 'l0QD1vAEIOUaeiou']
2178+ # Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the
2179+ # actual password
2180+ random_generator = random.SystemRandom()
2181 random_chars = [
2182- random.choice(alphanumeric_chars) for _ in range(length)]
2183+ random_generator.choice(alphanumeric_chars) for _ in range(length)]
2184 return(''.join(random_chars))
2185
2186
2187 def list_nics(nic_type):
2188 '''Return a list of nics of given type(s)'''
2189- if isinstance(nic_type, basestring):
2190+ if isinstance(nic_type, six.string_types):
2191 int_types = [nic_type]
2192 else:
2193 int_types = nic_type
2194 interfaces = []
2195 for int_type in int_types:
2196 cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
2197- ip_output = subprocess.check_output(cmd).split('\n')
2198+ ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
2199 ip_output = (line for line in ip_output if line)
2200 for line in ip_output:
2201 if line.split()[1].startswith(int_type):
2202- interfaces.append(line.split()[1].replace(":", ""))
2203+ matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line)
2204+ if matched:
2205+ interface = matched.groups()[0]
2206+ else:
2207+ interface = line.split()[1].replace(":", "")
2208+ interfaces.append(interface)
2209+
2210 return interfaces
2211
2212
2213@@ -304,7 +383,7 @@
2214
2215 def get_nic_mtu(nic):
2216 cmd = ['ip', 'addr', 'show', nic]
2217- ip_output = subprocess.check_output(cmd).split('\n')
2218+ ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
2219 mtu = ""
2220 for line in ip_output:
2221 words = line.split()
2222@@ -315,7 +394,7 @@
2223
2224 def get_nic_hwaddr(nic):
2225 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
2226- ip_output = subprocess.check_output(cmd)
2227+ ip_output = subprocess.check_output(cmd).decode('UTF-8')
2228 hwaddr = ""
2229 words = ip_output.split()
2230 if 'link/ether' in words:
2231@@ -330,10 +409,13 @@
2232 * 0 => Installed revno is the same as supplied arg
2233 * -1 => Installed revno is less than supplied arg
2234
2235+ This function imports apt_cache function from charmhelpers.fetch if
2236+ the pkgcache argument is None. Be sure to add charmhelpers.fetch if
2237+ you call this function, or pass an apt_pkg.Cache() instance.
2238 '''
2239 import apt_pkg
2240- from charmhelpers.fetch import apt_cache
2241 if not pkgcache:
2242+ from charmhelpers.fetch import apt_cache
2243 pkgcache = apt_cache()
2244 pkg = pkgcache[package]
2245 return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
2246
2247=== modified file 'lib/charmhelpers/core/services/__init__.py'
2248--- lib/charmhelpers/core/services/__init__.py 2014-08-17 21:29:19 +0000
2249+++ lib/charmhelpers/core/services/__init__.py 2015-06-04 13:22:19 +0000
2250@@ -1,2 +1,18 @@
2251-from .base import *
2252-from .helpers import *
2253+# Copyright 2014-2015 Canonical Limited.
2254+#
2255+# This file is part of charm-helpers.
2256+#
2257+# charm-helpers is free software: you can redistribute it and/or modify
2258+# it under the terms of the GNU Lesser General Public License version 3 as
2259+# published by the Free Software Foundation.
2260+#
2261+# charm-helpers is distributed in the hope that it will be useful,
2262+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2263+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2264+# GNU Lesser General Public License for more details.
2265+#
2266+# You should have received a copy of the GNU Lesser General Public License
2267+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2268+
2269+from .base import * # NOQA
2270+from .helpers import * # NOQA
2271
2272=== modified file 'lib/charmhelpers/core/services/base.py'
2273--- lib/charmhelpers/core/services/base.py 2014-09-02 17:52:32 +0000
2274+++ lib/charmhelpers/core/services/base.py 2015-06-04 13:22:19 +0000
2275@@ -1,7 +1,23 @@
2276+# Copyright 2014-2015 Canonical Limited.
2277+#
2278+# This file is part of charm-helpers.
2279+#
2280+# charm-helpers is free software: you can redistribute it and/or modify
2281+# it under the terms of the GNU Lesser General Public License version 3 as
2282+# published by the Free Software Foundation.
2283+#
2284+# charm-helpers is distributed in the hope that it will be useful,
2285+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2286+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2287+# GNU Lesser General Public License for more details.
2288+#
2289+# You should have received a copy of the GNU Lesser General Public License
2290+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2291+
2292 import os
2293-import re
2294 import json
2295-from collections import Iterable
2296+from inspect import getargspec
2297+from collections import Iterable, OrderedDict
2298
2299 from charmhelpers.core import host
2300 from charmhelpers.core import hookenv
2301@@ -103,7 +119,7 @@
2302 """
2303 self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')
2304 self._ready = None
2305- self.services = {}
2306+ self.services = OrderedDict()
2307 for service in services or []:
2308 service_name = service['service']
2309 self.services[service_name] = service
2310@@ -116,8 +132,8 @@
2311 if hook_name == 'stop':
2312 self.stop_services()
2313 else:
2314+ self.reconfigure_services()
2315 self.provide_data()
2316- self.reconfigure_services()
2317 cfg = hookenv.config()
2318 if cfg.implicit_save:
2319 cfg.save()
2320@@ -129,15 +145,36 @@
2321 A provider must have a `name` attribute, which indicates which relation
2322 to set data on, and a `provide_data()` method, which returns a dict of
2323 data to set.
2324+
2325+ The `provide_data()` method can optionally accept two parameters:
2326+
2327+ * ``remote_service`` The name of the remote service that the data will
2328+ be provided to. The `provide_data()` method will be called once
2329+ for each connected service (not unit). This allows the method to
2330+ tailor its data to the given service.
2331+ * ``service_ready`` Whether or not the service definition had all of
2332+ its requirements met, and thus the ``data_ready`` callbacks run.
2333+
2334+ Note that the ``provided_data`` methods are now called **after** the
2335+ ``data_ready`` callbacks are run. This gives the ``data_ready`` callbacks
2336+ a chance to generate any data necessary for the providing to the remote
2337+ services.
2338 """
2339- hook_name = hookenv.hook_name()
2340- for service in self.services.values():
2341+ for service_name, service in self.services.items():
2342+ service_ready = self.is_ready(service_name)
2343 for provider in service.get('provided_data', []):
2344- if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):
2345- data = provider.provide_data()
2346- _ready = provider._is_ready(data) if hasattr(provider, '_is_ready') else data
2347- if _ready:
2348- hookenv.relation_set(None, data)
2349+ for relid in hookenv.relation_ids(provider.name):
2350+ units = hookenv.related_units(relid)
2351+ if not units:
2352+ continue
2353+ remote_service = units[0].split('/')[0]
2354+ argspec = getargspec(provider.provide_data)
2355+ if len(argspec.args) > 1:
2356+ data = provider.provide_data(remote_service, service_ready)
2357+ else:
2358+ data = provider.provide_data()
2359+ if data:
2360+ hookenv.relation_set(relid, data)
2361
2362 def reconfigure_services(self, *service_names):
2363 """
2364
2365=== modified file 'lib/charmhelpers/core/services/helpers.py'
2366--- lib/charmhelpers/core/services/helpers.py 2014-08-17 21:29:19 +0000
2367+++ lib/charmhelpers/core/services/helpers.py 2015-06-04 13:22:19 +0000
2368@@ -1,3 +1,21 @@
2369+# Copyright 2014-2015 Canonical Limited.
2370+#
2371+# This file is part of charm-helpers.
2372+#
2373+# charm-helpers is free software: you can redistribute it and/or modify
2374+# it under the terms of the GNU Lesser General Public License version 3 as
2375+# published by the Free Software Foundation.
2376+#
2377+# charm-helpers is distributed in the hope that it will be useful,
2378+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2379+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2380+# GNU Lesser General Public License for more details.
2381+#
2382+# You should have received a copy of the GNU Lesser General Public License
2383+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2384+
2385+import os
2386+import yaml
2387 from charmhelpers.core import hookenv
2388 from charmhelpers.core import templating
2389
2390@@ -19,15 +37,23 @@
2391 the `name` attribute that are complete will used to populate the dictionary
2392 values (see `get_data`, below).
2393
2394- The generated context will be namespaced under the interface type, to prevent
2395- potential naming conflicts.
2396+ The generated context will be namespaced under the relation :attr:`name`,
2397+ to prevent potential naming conflicts.
2398+
2399+ :param str name: Override the relation :attr:`name`, since it can vary from charm to charm
2400+ :param list additional_required_keys: Extend the list of :attr:`required_keys`
2401 """
2402 name = None
2403 interface = None
2404- required_keys = []
2405-
2406- def __init__(self, *args, **kwargs):
2407- super(RelationContext, self).__init__(*args, **kwargs)
2408+
2409+ def __init__(self, name=None, additional_required_keys=None):
2410+ if not hasattr(self, 'required_keys'):
2411+ self.required_keys = []
2412+
2413+ if name is not None:
2414+ self.name = name
2415+ if additional_required_keys:
2416+ self.required_keys.extend(additional_required_keys)
2417 self.get_data()
2418
2419 def __bool__(self):
2420@@ -101,11 +127,127 @@
2421 return {}
2422
2423
2424+class MysqlRelation(RelationContext):
2425+ """
2426+ Relation context for the `mysql` interface.
2427+
2428+ :param str name: Override the relation :attr:`name`, since it can vary from charm to charm
2429+ :param list additional_required_keys: Extend the list of :attr:`required_keys`
2430+ """
2431+ name = 'db'
2432+ interface = 'mysql'
2433+
2434+ def __init__(self, *args, **kwargs):
2435+ self.required_keys = ['host', 'user', 'password', 'database']
2436+ RelationContext.__init__(self, *args, **kwargs)
2437+
2438+
2439+class HttpRelation(RelationContext):
2440+ """
2441+ Relation context for the `http` interface.
2442+
2443+ :param str name: Override the relation :attr:`name`, since it can vary from charm to charm
2444+ :param list additional_required_keys: Extend the list of :attr:`required_keys`
2445+ """
2446+ name = 'website'
2447+ interface = 'http'
2448+
2449+ def __init__(self, *args, **kwargs):
2450+ self.required_keys = ['host', 'port']
2451+ RelationContext.__init__(self, *args, **kwargs)
2452+
2453+ def provide_data(self):
2454+ return {
2455+ 'host': hookenv.unit_get('private-address'),
2456+ 'port': 80,
2457+ }
2458+
2459+
2460+class RequiredConfig(dict):
2461+ """
2462+ Data context that loads config options with one or more mandatory options.
2463+
2464+ Once the required options have been changed from their default values, all
2465+ config options will be available, namespaced under `config` to prevent
2466+ potential naming conflicts (for example, between a config option and a
2467+ relation property).
2468+
2469+ :param list *args: List of options that must be changed from their default values.
2470+ """
2471+
2472+ def __init__(self, *args):
2473+ self.required_options = args
2474+ self['config'] = hookenv.config()
2475+ with open(os.path.join(hookenv.charm_dir(), 'config.yaml')) as fp:
2476+ self.config = yaml.load(fp).get('options', {})
2477+
2478+ def __bool__(self):
2479+ for option in self.required_options:
2480+ if option not in self['config']:
2481+ return False
2482+ current_value = self['config'][option]
2483+ default_value = self.config[option].get('default')
2484+ if current_value == default_value:
2485+ return False
2486+ if current_value in (None, '') and default_value in (None, ''):
2487+ return False
2488+ return True
2489+
2490+ def __nonzero__(self):
2491+ return self.__bool__()
2492+
2493+
2494+class StoredContext(dict):
2495+ """
2496+ A data context that always returns the data that it was first created with.
2497+
2498+ This is useful to do a one-time generation of things like passwords, that
2499+ will thereafter use the same value that was originally generated, instead
2500+ of generating a new value each time it is run.
2501+ """
2502+ def __init__(self, file_name, config_data):
2503+ """
2504+ If the file exists, populate `self` with the data from the file.
2505+ Otherwise, populate with the given data and persist it to the file.
2506+ """
2507+ if os.path.exists(file_name):
2508+ self.update(self.read_context(file_name))
2509+ else:
2510+ self.store_context(file_name, config_data)
2511+ self.update(config_data)
2512+
2513+ def store_context(self, file_name, config_data):
2514+ if not os.path.isabs(file_name):
2515+ file_name = os.path.join(hookenv.charm_dir(), file_name)
2516+ with open(file_name, 'w') as file_stream:
2517+ os.fchmod(file_stream.fileno(), 0o600)
2518+ yaml.dump(config_data, file_stream)
2519+
2520+ def read_context(self, file_name):
2521+ if not os.path.isabs(file_name):
2522+ file_name = os.path.join(hookenv.charm_dir(), file_name)
2523+ with open(file_name, 'r') as file_stream:
2524+ data = yaml.load(file_stream)
2525+ if not data:
2526+ raise OSError("%s is empty" % file_name)
2527+ return data
2528+
2529+
2530 class TemplateCallback(ManagerCallback):
2531 """
2532- Callback class that will render a template, for use as a ready action.
2533+ Callback class that will render a Jinja2 template, for use as a ready
2534+ action.
2535+
2536+ :param str source: The template source file, relative to
2537+ `$CHARM_DIR/templates`
2538+
2539+ :param str target: The target to write the rendered template to
2540+ :param str owner: The owner of the rendered file
2541+ :param str group: The group of the rendered file
2542+ :param int perms: The permissions of the rendered file
2543 """
2544- def __init__(self, source, target, owner='root', group='root', perms=0444):
2545+ def __init__(self, source, target,
2546+ owner='root', group='root', perms=0o444):
2547 self.source = source
2548 self.target = target
2549 self.owner = owner
2550
2551=== modified file 'lib/charmhelpers/core/templating.py'
2552--- lib/charmhelpers/core/templating.py 2014-08-17 21:29:19 +0000
2553+++ lib/charmhelpers/core/templating.py 2015-06-04 13:22:19 +0000
2554@@ -1,10 +1,27 @@
2555+# Copyright 2014-2015 Canonical Limited.
2556+#
2557+# This file is part of charm-helpers.
2558+#
2559+# charm-helpers is free software: you can redistribute it and/or modify
2560+# it under the terms of the GNU Lesser General Public License version 3 as
2561+# published by the Free Software Foundation.
2562+#
2563+# charm-helpers is distributed in the hope that it will be useful,
2564+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2565+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2566+# GNU Lesser General Public License for more details.
2567+#
2568+# You should have received a copy of the GNU Lesser General Public License
2569+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2570+
2571 import os
2572
2573 from charmhelpers.core import host
2574 from charmhelpers.core import hookenv
2575
2576
2577-def render(source, target, context, owner='root', group='root', perms=0444, templates_dir=None):
2578+def render(source, target, context, owner='root', group='root',
2579+ perms=0o444, templates_dir=None, encoding='UTF-8'):
2580 """
2581 Render a template.
2582
2583@@ -47,5 +64,5 @@
2584 level=hookenv.ERROR)
2585 raise e
2586 content = template.render(context)
2587- host.mkdir(os.path.dirname(target))
2588- host.write_file(target, content, owner, group, perms)
2589+ host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
2590+ host.write_file(target, content.encode(encoding), owner, group, perms)
2591
2592=== modified file 'lib/charmhelpers/fetch/__init__.py'
2593--- lib/charmhelpers/fetch/__init__.py 2014-08-26 13:59:01 +0000
2594+++ lib/charmhelpers/fetch/__init__.py 2015-06-04 13:22:19 +0000
2595@@ -1,3 +1,19 @@
2596+# Copyright 2014-2015 Canonical Limited.
2597+#
2598+# This file is part of charm-helpers.
2599+#
2600+# charm-helpers is free software: you can redistribute it and/or modify
2601+# it under the terms of the GNU Lesser General Public License version 3 as
2602+# published by the Free Software Foundation.
2603+#
2604+# charm-helpers is distributed in the hope that it will be useful,
2605+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2606+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2607+# GNU Lesser General Public License for more details.
2608+#
2609+# You should have received a copy of the GNU Lesser General Public License
2610+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2611+
2612 import importlib
2613 from tempfile import NamedTemporaryFile
2614 import time
2615@@ -5,10 +21,6 @@
2616 from charmhelpers.core.host import (
2617 lsb_release
2618 )
2619-from urlparse import (
2620- urlparse,
2621- urlunparse,
2622-)
2623 import subprocess
2624 from charmhelpers.core.hookenv import (
2625 config,
2626@@ -16,6 +28,12 @@
2627 )
2628 import os
2629
2630+import six
2631+if six.PY3:
2632+ from urllib.parse import urlparse, urlunparse
2633+else:
2634+ from urlparse import urlparse, urlunparse
2635+
2636
2637 CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
2638 deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
2639@@ -62,9 +80,16 @@
2640 'trusty-juno/updates': 'trusty-updates/juno',
2641 'trusty-updates/juno': 'trusty-updates/juno',
2642 'juno/proposed': 'trusty-proposed/juno',
2643- 'juno/proposed': 'trusty-proposed/juno',
2644 'trusty-juno/proposed': 'trusty-proposed/juno',
2645 'trusty-proposed/juno': 'trusty-proposed/juno',
2646+ # Kilo
2647+ 'kilo': 'trusty-updates/kilo',
2648+ 'trusty-kilo': 'trusty-updates/kilo',
2649+ 'trusty-kilo/updates': 'trusty-updates/kilo',
2650+ 'trusty-updates/kilo': 'trusty-updates/kilo',
2651+ 'kilo/proposed': 'trusty-proposed/kilo',
2652+ 'trusty-kilo/proposed': 'trusty-proposed/kilo',
2653+ 'trusty-proposed/kilo': 'trusty-proposed/kilo',
2654 }
2655
2656 # The order of this list is very important. Handlers should be listed in from
2657@@ -72,6 +97,7 @@
2658 FETCH_HANDLERS = (
2659 'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler',
2660 'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler',
2661+ 'charmhelpers.fetch.giturl.GitUrlFetchHandler',
2662 )
2663
2664 APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
2665@@ -132,7 +158,7 @@
2666
2667 def apt_cache(in_memory=True):
2668 """Build and return an apt cache"""
2669- import apt_pkg
2670+ from apt import apt_pkg
2671 apt_pkg.init()
2672 if in_memory:
2673 apt_pkg.config.set("Dir::Cache::pkgcache", "")
2674@@ -148,7 +174,7 @@
2675 cmd = ['apt-get', '--assume-yes']
2676 cmd.extend(options)
2677 cmd.append('install')
2678- if isinstance(packages, basestring):
2679+ if isinstance(packages, six.string_types):
2680 cmd.append(packages)
2681 else:
2682 cmd.extend(packages)
2683@@ -181,7 +207,7 @@
2684 def apt_purge(packages, fatal=False):
2685 """Purge one or more packages"""
2686 cmd = ['apt-get', '--assume-yes', 'purge']
2687- if isinstance(packages, basestring):
2688+ if isinstance(packages, six.string_types):
2689 cmd.append(packages)
2690 else:
2691 cmd.extend(packages)
2692@@ -192,7 +218,7 @@
2693 def apt_hold(packages, fatal=False):
2694 """Hold one or more packages"""
2695 cmd = ['apt-mark', 'hold']
2696- if isinstance(packages, basestring):
2697+ if isinstance(packages, six.string_types):
2698 cmd.append(packages)
2699 else:
2700 cmd.extend(packages)
2701@@ -208,7 +234,8 @@
2702 """Add a package source to this system.
2703
2704 @param source: a URL or sources.list entry, as supported by
2705- add-apt-repository(1). Examples:
2706+ add-apt-repository(1). Examples::
2707+
2708 ppa:charmers/example
2709 deb https://stub:key@private.example.com/ubuntu trusty main
2710
2711@@ -217,6 +244,7 @@
2712 pocket for the release.
2713 'cloud:' may be used to activate official cloud archive pockets,
2714 such as 'cloud:icehouse'
2715+ 'distro' may be used as a noop
2716
2717 @param key: A key to be added to the system's APT keyring and used
2718 to verify the signatures on packages. Ideally, this should be an
2719@@ -250,12 +278,14 @@
2720 release = lsb_release()['DISTRIB_CODENAME']
2721 with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
2722 apt.write(PROPOSED_POCKET.format(release))
2723+ elif source == 'distro':
2724+ pass
2725 else:
2726- raise SourceConfigError("Unknown source: {!r}".format(source))
2727+ log("Unknown source: {!r}".format(source))
2728
2729 if key:
2730 if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
2731- with NamedTemporaryFile() as key_file:
2732+ with NamedTemporaryFile('w+') as key_file:
2733 key_file.write(key)
2734 key_file.flush()
2735 key_file.seek(0)
2736@@ -292,14 +322,14 @@
2737 sources = safe_load((config(sources_var) or '').strip()) or []
2738 keys = safe_load((config(keys_var) or '').strip()) or None
2739
2740- if isinstance(sources, basestring):
2741+ if isinstance(sources, six.string_types):
2742 sources = [sources]
2743
2744 if keys is None:
2745 for source in sources:
2746 add_source(source, None)
2747 else:
2748- if isinstance(keys, basestring):
2749+ if isinstance(keys, six.string_types):
2750 keys = [keys]
2751
2752 if len(sources) != len(keys):
2753@@ -311,22 +341,35 @@
2754 apt_update(fatal=True)
2755
2756
2757-def install_remote(source):
2758+def install_remote(source, *args, **kwargs):
2759 """
2760 Install a file tree from a remote source
2761
2762 The specified source should be a url of the form:
2763 scheme://[host]/path[#[option=value][&...]]
2764
2765- Schemes supported are based on this modules submodules
2766- Options supported are submodule-specific"""
2767+ Schemes supported are based on this modules submodules.
2768+ Options supported are submodule-specific.
2769+ Additional arguments are passed through to the submodule.
2770+
2771+ For example::
2772+
2773+ dest = install_remote('http://example.com/archive.tgz',
2774+ checksum='deadbeef',
2775+ hash_type='sha1')
2776+
2777+ This will download `archive.tgz`, validate it using SHA1 and, if
2778+ the file is ok, extract it and return the directory in which it
2779+ was extracted. If the checksum fails, it will raise
2780+ :class:`charmhelpers.core.host.ChecksumError`.
2781+ """
2782 # We ONLY check for True here because can_handle may return a string
2783 # explaining why it can't handle a given source.
2784 handlers = [h for h in plugins() if h.can_handle(source) is True]
2785 installed_to = None
2786 for handler in handlers:
2787 try:
2788- installed_to = handler.install(source)
2789+ installed_to = handler.install(source, *args, **kwargs)
2790 except UnhandledSource:
2791 pass
2792 if not installed_to:
2793@@ -383,7 +426,7 @@
2794 while result is None or result == APT_NO_LOCK:
2795 try:
2796 result = subprocess.check_call(cmd, env=env)
2797- except subprocess.CalledProcessError, e:
2798+ except subprocess.CalledProcessError as e:
2799 retry_count = retry_count + 1
2800 if retry_count > APT_NO_LOCK_RETRY_COUNT:
2801 raise
2802
2803=== modified file 'lib/charmhelpers/fetch/archiveurl.py'
2804--- lib/charmhelpers/fetch/archiveurl.py 2014-09-15 14:42:27 +0000
2805+++ lib/charmhelpers/fetch/archiveurl.py 2015-06-04 13:22:19 +0000
2806@@ -1,8 +1,22 @@
2807+# Copyright 2014-2015 Canonical Limited.
2808+#
2809+# This file is part of charm-helpers.
2810+#
2811+# charm-helpers is free software: you can redistribute it and/or modify
2812+# it under the terms of the GNU Lesser General Public License version 3 as
2813+# published by the Free Software Foundation.
2814+#
2815+# charm-helpers is distributed in the hope that it will be useful,
2816+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2817+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2818+# GNU Lesser General Public License for more details.
2819+#
2820+# You should have received a copy of the GNU Lesser General Public License
2821+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2822+
2823 import os
2824-import urllib2
2825-from urllib import urlretrieve
2826-import urlparse
2827 import hashlib
2828+import re
2829
2830 from charmhelpers.fetch import (
2831 BaseFetchHandler,
2832@@ -12,21 +26,54 @@
2833 get_archive_handler,
2834 extract,
2835 )
2836-from charmhelpers.core.host import mkdir
2837-
2838-"""
2839-This class is a plugin for charmhelpers.fetch.install_remote.
2840-
2841-It grabs, validates and installs remote archives fetched over "http", "https", "ftp" or "file" protocols. The contents of the archive are installed in $CHARM_DIR/fetched/.
2842-
2843-Example usage:
2844-install_remote("https://example.com/some/archive.tar.gz")
2845-# Installs the contents of archive.tar.gz in $CHARM_DIR/fetched/.
2846-
2847-See charmhelpers.fetch.archiveurl.get_archivehandler for supported archive types.
2848-"""
2849+from charmhelpers.core.host import mkdir, check_hash
2850+
2851+import six
2852+if six.PY3:
2853+ from urllib.request import (
2854+ build_opener, install_opener, urlopen, urlretrieve,
2855+ HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler,
2856+ )
2857+ from urllib.parse import urlparse, urlunparse, parse_qs
2858+ from urllib.error import URLError
2859+else:
2860+ from urllib import urlretrieve
2861+ from urllib2 import (
2862+ build_opener, install_opener, urlopen,
2863+ HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler,
2864+ URLError
2865+ )
2866+ from urlparse import urlparse, urlunparse, parse_qs
2867+
2868+
2869+def splituser(host):
2870+ '''urllib.splituser(), but six's support of this seems broken'''
2871+ _userprog = re.compile('^(.*)@(.*)$')
2872+ match = _userprog.match(host)
2873+ if match:
2874+ return match.group(1, 2)
2875+ return None, host
2876+
2877+
2878+def splitpasswd(user):
2879+ '''urllib.splitpasswd(), but six's support of this is missing'''
2880+ _passwdprog = re.compile('^([^:]*):(.*)$', re.S)
2881+ match = _passwdprog.match(user)
2882+ if match:
2883+ return match.group(1, 2)
2884+ return user, None
2885+
2886+
2887 class ArchiveUrlFetchHandler(BaseFetchHandler):
2888- """Handler for archives via generic URLs"""
2889+ """
2890+ Handler to download archive files from arbitrary URLs.
2891+
2892+ Can fetch from http, https, ftp, and file URLs.
2893+
2894+ Can install either tarballs (.tar, .tgz, .tbz2, etc) or zip files.
2895+
2896+ Installs the contents of the archive in $CHARM_DIR/fetched/.
2897+ """
2898 def can_handle(self, source):
2899 url_parts = self.parse_url(source)
2900 if url_parts.scheme not in ('http', 'https', 'ftp', 'file'):
2901@@ -36,22 +83,28 @@
2902 return False
2903
2904 def download(self, source, dest):
2905+ """
2906+ Download an archive file.
2907+
2908+ :param str source: URL pointing to an archive file.
2909+ :param str dest: Local path location to download archive file to.
2910+ """
2911 # propogate all exceptions
2912 # URLError, OSError, etc
2913- proto, netloc, path, params, query, fragment = urlparse.urlparse(source)
2914+ proto, netloc, path, params, query, fragment = urlparse(source)
2915 if proto in ('http', 'https'):
2916- auth, barehost = urllib2.splituser(netloc)
2917+ auth, barehost = splituser(netloc)
2918 if auth is not None:
2919- source = urlparse.urlunparse((proto, barehost, path, params, query, fragment))
2920- username, password = urllib2.splitpasswd(auth)
2921- passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
2922+ source = urlunparse((proto, barehost, path, params, query, fragment))
2923+ username, password = splitpasswd(auth)
2924+ passman = HTTPPasswordMgrWithDefaultRealm()
2925 # Realm is set to None in add_password to force the username and password
2926 # to be used whatever the realm
2927 passman.add_password(None, source, username, password)
2928- authhandler = urllib2.HTTPBasicAuthHandler(passman)
2929- opener = urllib2.build_opener(authhandler)
2930- urllib2.install_opener(opener)
2931- response = urllib2.urlopen(source)
2932+ authhandler = HTTPBasicAuthHandler(passman)
2933+ opener = build_opener(authhandler)
2934+ install_opener(opener)
2935+ response = urlopen(source)
2936 try:
2937 with open(dest, 'w') as dest_file:
2938 dest_file.write(response.read())
2939@@ -60,44 +113,49 @@
2940 os.unlink(dest)
2941 raise e
2942
2943- def install(self, source):
2944+ # Mandatory file validation via Sha1 or MD5 hashing.
2945+ def download_and_validate(self, url, hashsum, validate="sha1"):
2946+ tempfile, headers = urlretrieve(url)
2947+ check_hash(tempfile, hashsum, validate)
2948+ return tempfile
2949+
2950+ def install(self, source, dest=None, checksum=None, hash_type='sha1'):
2951+ """
2952+ Download and install an archive file, with optional checksum validation.
2953+
2954+ The checksum can also be given on the `source` URL's fragment.
2955+ For example::
2956+
2957+ handler.install('http://example.com/file.tgz#sha1=deadbeef')
2958+
2959+ :param str source: URL pointing to an archive file.
2960+ :param str dest: Local destination path to install to. If not given,
2961+ installs to `$CHARM_DIR/archives/archive_file_name`.
2962+ :param str checksum: If given, validate the archive file after download.
2963+ :param str hash_type: Algorithm used to generate `checksum`.
2964+ Can be any hash alrgorithm supported by :mod:`hashlib`,
2965+ such as md5, sha1, sha256, sha512, etc.
2966+
2967+ """
2968 url_parts = self.parse_url(source)
2969 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), 'fetched')
2970 if not os.path.exists(dest_dir):
2971- mkdir(dest_dir, perms=0755)
2972+ mkdir(dest_dir, perms=0o755)
2973 dld_file = os.path.join(dest_dir, os.path.basename(url_parts.path))
2974 try:
2975 self.download(source, dld_file)
2976- except urllib2.URLError as e:
2977+ except URLError as e:
2978 raise UnhandledSource(e.reason)
2979 except OSError as e:
2980 raise UnhandledSource(e.strerror)
2981- return extract(dld_file)
2982-
2983- # Mandatory file validation via Sha1 or MD5 hashing.
2984- def download_and_validate(self, url, hashsum, validate="sha1"):
2985- if validate == 'sha1' and len(hashsum) != 40:
2986- raise ValueError("HashSum must be = 40 characters when using sha1"
2987- " validation")
2988- if validate == 'md5' and len(hashsum) != 32:
2989- raise ValueError("HashSum must be = 32 characters when using md5"
2990- " validation")
2991- tempfile, headers = urlretrieve(url)
2992- self.validate_file(tempfile, hashsum, validate)
2993- return tempfile
2994-
2995- # Predicate method that returns status of hash matching expected hash.
2996- def validate_file(self, source, hashsum, vmethod='sha1'):
2997- if vmethod != 'sha1' and vmethod != 'md5':
2998- raise ValueError("Validation Method not supported")
2999-
3000- if vmethod == 'md5':
3001- m = hashlib.md5()
3002- if vmethod == 'sha1':
3003- m = hashlib.sha1()
3004- with open(source) as f:
3005- for line in f:
3006- m.update(line)
3007- if hashsum != m.hexdigest():
3008- msg = "Hash Mismatch on {} expected {} got {}"
3009- raise ValueError(msg.format(source, hashsum, m.hexdigest()))
3010+ options = parse_qs(url_parts.fragment)
3011+ for key, value in options.items():
3012+ if not six.PY3:
3013+ algorithms = hashlib.algorithms
3014+ else:
3015+ algorithms = hashlib.algorithms_available
3016+ if key in algorithms:
3017+ check_hash(dld_file, value, key)
3018+ if checksum:
3019+ check_hash(dld_file, checksum, hash_type)
3020+ return extract(dld_file, dest)
3021
3022=== modified file 'lib/charmhelpers/fetch/bzrurl.py'
3023--- lib/charmhelpers/fetch/bzrurl.py 2014-08-17 21:29:19 +0000
3024+++ lib/charmhelpers/fetch/bzrurl.py 2015-06-04 13:22:19 +0000
3025@@ -1,3 +1,19 @@
3026+# Copyright 2014-2015 Canonical Limited.
3027+#
3028+# This file is part of charm-helpers.
3029+#
3030+# charm-helpers is free software: you can redistribute it and/or modify
3031+# it under the terms of the GNU Lesser General Public License version 3 as
3032+# published by the Free Software Foundation.
3033+#
3034+# charm-helpers is distributed in the hope that it will be useful,
3035+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3036+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3037+# GNU Lesser General Public License for more details.
3038+#
3039+# You should have received a copy of the GNU Lesser General Public License
3040+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3041+
3042 import os
3043 from charmhelpers.fetch import (
3044 BaseFetchHandler,
3045@@ -5,12 +21,18 @@
3046 )
3047 from charmhelpers.core.host import mkdir
3048
3049+import six
3050+if six.PY3:
3051+ raise ImportError('bzrlib does not support Python3')
3052+
3053 try:
3054 from bzrlib.branch import Branch
3055+ from bzrlib import bzrdir, workingtree, errors
3056 except ImportError:
3057 from charmhelpers.fetch import apt_install
3058 apt_install("python-bzrlib")
3059 from bzrlib.branch import Branch
3060+ from bzrlib import bzrdir, workingtree, errors
3061
3062
3063 class BzrUrlFetchHandler(BaseFetchHandler):
3064@@ -31,8 +53,14 @@
3065 from bzrlib.plugin import load_plugins
3066 load_plugins()
3067 try:
3068+ local_branch = bzrdir.BzrDir.create_branch_convenience(dest)
3069+ except errors.AlreadyControlDirError:
3070+ local_branch = Branch.open(dest)
3071+ try:
3072 remote_branch = Branch.open(source)
3073- remote_branch.bzrdir.sprout(dest).open_branch()
3074+ remote_branch.push(local_branch)
3075+ tree = workingtree.WorkingTree.open(dest)
3076+ tree.update()
3077 except Exception as e:
3078 raise e
3079
3080@@ -42,7 +70,7 @@
3081 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
3082 branch_name)
3083 if not os.path.exists(dest_dir):
3084- mkdir(dest_dir, perms=0755)
3085+ mkdir(dest_dir, perms=0o755)
3086 try:
3087 self.branch(source, dest_dir)
3088 except OSError as e:
3089
3090=== removed file 'patches/hookenv-0001-fix_config_lookups.patch'
3091--- patches/hookenv-0001-fix_config_lookups.patch 2014-09-05 17:35:28 +0000
3092+++ patches/hookenv-0001-fix_config_lookups.patch 1970-01-01 00:00:00 +0000
3093@@ -1,30 +0,0 @@
3094-------------------------------------------------------------
3095-revno: 101
3096-committer: Robert Jennings <robert.jennings@canonical.com>
3097-branch nick: ubuntu-repository-cache
3098-timestamp: Fri 2014-09-05 12:33:45 -0500
3099-message:
3100- Fix config lookups so that user-saved values are returned properly.
3101-
3102- https://code.launchpad.net/~tvansteenburgh/charm-helpers/fix-config-lookups/+merge/233558
3103-diff:
3104-=== modified file 'lib/charmhelpers/core/hookenv.py'
3105---- lib/charmhelpers/core/hookenv.py 2014-09-02 17:52:32 +0000
3106-+++ lib/charmhelpers/core/hookenv.py 2014-09-05 17:33:45 +0000
3107-@@ -203,6 +203,17 @@
3108- if os.path.exists(self.path):
3109- self.load_previous()
3110-
3111-+ def __getitem__(self, key):
3112-+ """For regular dict lookups, check the current juju config first,
3113-+ the the previous (saved) copy. This ensures that user-saved values
3114-+ will be returned by a dict lookup.
3115-+
3116-+ """
3117-+ try:
3118-+ return dict.__getitem__(self, key)
3119-+ except KeyError:
3120-+ return (self._prev_dict or {})[key]
3121-+
3122- def load_previous(self, path=None):
3123- """Load previous copy of config from disk.
3124
3125=== removed file 'patches/lchownr.patch'
3126--- patches/lchownr.patch 2014-09-02 17:52:32 +0000
3127+++ patches/lchownr.patch 1970-01-01 00:00:00 +0000
3128@@ -1,42 +0,0 @@
3129-------------------------------------------------------------
3130-revno: 37
3131-committer: Robert Jennings <robert.jennings@canonical.com>
3132-branch nick: ubuntu-repository-cache
3133-timestamp: Tue 2014-08-26 14:46:22 -0500
3134-message:
3135- lchown() variant for charmhelpers.core.host.chownr()
3136-
3137- Port of MP#232306
3138- https://code.launchpad.net/~rcj/charm-helpers/lchownr/+merge/232306
3139-
3140- Create hosts.lchownr() as a variant that doesn't follow links
3141- lchownr() will walk the path without following links on the walk() and
3142- will use os.lchown() to change owner/group on the link itself.
3143-diff:
3144-=== modified file 'lib/charmhelpers/core/host.py'
3145---- lib/charmhelpers/core/host.py 2014-08-26 13:59:01 +0000
3146-+++ lib/charmhelpers/core/host.py 2014-08-26 19:46:22 +0000
3147-@@ -348,13 +348,21 @@
3148- os.chdir(cur)
3149-
3150-
3151--def chownr(path, owner, group):
3152-+def chownr(path, owner, group, follow_links=True):
3153- uid = pwd.getpwnam(owner).pw_uid
3154- gid = grp.getgrnam(group).gr_gid
3155-+ if follow_links:
3156-+ chown = os.chown
3157-+ else:
3158-+ chown = os.lchown
3159-
3160- for root, dirs, files in os.walk(path):
3161- for name in dirs + files:
3162- full = os.path.join(root, name)
3163- broken_symlink = os.path.lexists(full) and not os.path.exists(full)
3164- if not broken_symlink:
3165-- os.chown(full, uid, gid)
3166-+ chown(full, uid, gid)
3167-+
3168-+
3169-+def lchownr(path, owner, group):
3170-+ chownr(path, owner, group, follow_links=False)
3171
3172=== removed file 'patches/mirror-0001-devel_reduce_dists.patch'
3173--- patches/mirror-0001-devel_reduce_dists.patch 2014-09-08 14:57:57 +0000
3174+++ patches/mirror-0001-devel_reduce_dists.patch 1970-01-01 00:00:00 +0000
3175@@ -1,16 +0,0 @@
3176-=== modified file 'lib/ubuntu_repository_cache/mirror.py'
3177---- lib/ubuntu_repository_cache/mirror.py 2014-09-08 14:55:51 +0000
3178-+++ lib/ubuntu_repository_cache/mirror.py 2014-09-08 14:56:47 +0000
3179-@@ -88,6 +88,11 @@
3180- '--exclude=dists/*/*/dist-upgrader-all',
3181- '--exclude=dists/*/*/installer*',
3182- '--exclude=dists/*/*/uefi',
3183-+ '--exclude=dists/lucid-*', # TODO Remove before flight
3184-+ '--exclude=dists/precis*', # TODO Remove before flight
3185-+ '--exclude=dists/sauc*', # TODO Remove before flight
3186-+ '--exclude=dists/trust*', # TODO Remove before flight
3187-+ '--exclude=dists/utopic*', # TODO Remove before flight
3188- )
3189- host.rsync(rsync_source,
3190- dest,
3191-
3192
3193=== removed file 'patches/storage-0001-is_device_mounted.patch'
3194--- patches/storage-0001-is_device_mounted.patch 2014-09-16 13:52:19 +0000
3195+++ patches/storage-0001-is_device_mounted.patch 1970-01-01 00:00:00 +0000
3196@@ -1,31 +0,0 @@
3197-------------------------------------------------------------
3198-revno: 137
3199-committer: Robert Jennings <robert.jennings@canonical.com>
3200-branch nick: ubuntu-repository-cache
3201-timestamp: Tue 2014-09-16 08:50:02 -0500
3202-message:
3203- storage.linux.util: Fix swapped is_device_mounted regex logic (lp:1370053)
3204-
3205- The is_device_mounted() function checks if the passed device is a
3206- partition and tailors the regex based on this. However, the search
3207- expressions were swapped and would fail. When a partition is provided,
3208- a match will only be found if the parent device is mounted.
3209-
3210- For example, if we call is_device_mounted(device='/dev/sda1') and
3211- '/dev/sda1' is in the mount command output, the result is false.
3212- Similarly, is_device_mounted(device='/dev/sda') and '/dev/sda' is in
3213- the mount command output, the result is false. Instead, if the device
3214- is /dev/sda1 and /dev/sda is in the mount output, we get a True result
3215- from the function.
3216-diff:
3217-=== modified file 'lib/charmhelpers/contrib/storage/linux/utils.py'
3218---- lib/charmhelpers/contrib/storage/linux/utils.py 2014-09-15 14:42:27 +0000
3219-+++ lib/charmhelpers/contrib/storage/linux/utils.py 2014-09-16 13:50:02 +0000
3220-@@ -48,6 +48,6 @@
3221- '''
3222- is_partition = bool(re.search(r".*[0-9]+\b", device))
3223- out = check_output(['mount'])
3224-- if is_partition:
3225-+ if not is_partition:
3226- return bool(re.search(device + r"\b", out))
3227- return bool(re.search(device + r"[0-9]+\b", out))
3228
3229=== modified file 'patches/unison-0002-keyscan.patch'
3230--- patches/unison-0002-keyscan.patch 2014-09-03 21:16:33 +0000
3231+++ patches/unison-0002-keyscan.patch 2015-06-04 13:22:19 +0000
3232@@ -14,12 +14,12 @@
3233 === modified file 'lib/charmhelpers/contrib/unison/__init__.py'
3234 --- lib/charmhelpers/contrib/unison/__init__.py 2014-09-03 19:44:16 +0000
3235 +++ lib/charmhelpers/contrib/unison/__init__.py 2014-09-03 19:47:58 +0000
3236-@@ -142,7 +142,7 @@
3237+@@ -159,7 +159,7 @@
3238 known_hosts = os.path.join(ssh_dir, 'known_hosts')
3239 khosts = []
3240 for host in hosts:
3241 - cmd = ['ssh-keyscan', '-H', '-t', 'rsa', host]
3242 + cmd = ['ssh-keyscan', host]
3243- remote_key = check_output(cmd).strip()
3244+ remote_key = check_output(cmd, universal_newlines=True).strip()
3245 khosts.append(remote_key)
3246 log('Syncing known_hosts @ %s.' % known_hosts)

Subscribers

People subscribed via source and target branches

to all changes: