Merge lp:~1chb1n/charms/trusty/swift-proxy/vivid-kilo-ch-sync into lp:~openstack-charmers-archive/charms/trusty/swift-proxy/trunk

Proposed by Ryan Beisner
Status: Superseded
Proposed branch: lp:~1chb1n/charms/trusty/swift-proxy/vivid-kilo-ch-sync
Merge into: lp:~openstack-charmers-archive/charms/trusty/swift-proxy/trunk
Diff against target: 1218 lines (+853/-46)
19 files modified
hooks/charmhelpers/contrib/charmsupport/nrpe.py (+41/-7)
hooks/charmhelpers/contrib/hahelpers/cluster.py (+5/-1)
hooks/charmhelpers/contrib/openstack/amulet/deployment.py (+5/-2)
hooks/charmhelpers/contrib/openstack/files/__init__.py (+18/-0)
hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh (+32/-0)
hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh (+30/-0)
hooks/charmhelpers/contrib/openstack/ip.py (+37/-0)
hooks/charmhelpers/contrib/openstack/utils.py (+1/-0)
hooks/charmhelpers/contrib/python/packages.py (+2/-2)
hooks/charmhelpers/core/fstab.py (+4/-4)
hooks/charmhelpers/core/host.py (+5/-5)
hooks/charmhelpers/core/strutils.py (+42/-0)
hooks/charmhelpers/core/sysctl.py (+13/-7)
hooks/charmhelpers/core/templating.py (+3/-3)
hooks/charmhelpers/core/unitdata.py (+477/-0)
hooks/charmhelpers/fetch/archiveurl.py (+10/-10)
hooks/charmhelpers/fetch/giturl.py (+1/-1)
tests/charmhelpers/contrib/amulet/utils.py (+122/-2)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+5/-2)
To merge this branch: bzr merge lp:~1chb1n/charms/trusty/swift-proxy/vivid-kilo-ch-sync
Reviewer Review Type Date Requested Status
OpenStack Charmers Pending
Review via email: mp+250374@code.launchpad.net

This proposal has been superseded by a proposal from 2015-02-19.

Description of the change

Charmhelper sync to resolve swift version table issue.
https://bugs.launchpad.net/charms/+source/swift-proxy/+bug/1423680

To post a comment you must log in.
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_lint_check #2121 swift-proxy for 1chb1n mp250374
    LINT FAIL: lint-test failed
    LINT FAIL: charm-proof failed

LINT Results (max last 2 lines):
  W: config.yaml: option keystone-admin-password does not have the keys: default
  make: *** [lint] Error 100

Full lint test output: http://paste.ubuntu.com/10313191/
Build: http://10.245.162.77:8080/job/charm_lint_check/2121/

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

charm_unit_test #1911 swift-proxy for 1chb1n mp250374
    UNIT OK: passed

Build: http://10.245.162.77:8080/job/charm_unit_test/1911/

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

charm_amulet_test #2030 swift-proxy for 1chb1n mp250374
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
  ERROR subprocess encountered error code 1
  make: *** [test] Error 1

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

81. By Ryan Beisner

resolve charm proof warnings

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

charm_lint_check #2122 swift-proxy for 1chb1n mp250374
    LINT OK: passed

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

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

charm_lint_check #2125 swift-proxy for 1chb1n mp250374
    LINT OK: passed

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

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

charm_unit_test #1914 swift-proxy for 1chb1n mp250374
    UNIT OK: passed

Build: http://10.245.162.77:8080/job/charm_unit_test/1914/

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py'
2--- hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-01-26 09:45:43 +0000
3+++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-02-19 20:31:59 +0000
4@@ -24,6 +24,8 @@
5 import pwd
6 import grp
7 import os
8+import glob
9+import shutil
10 import re
11 import shlex
12 import yaml
13@@ -161,7 +163,7 @@
14 log('Check command not found: {}'.format(parts[0]))
15 return ''
16
17- def write(self, nagios_context, hostname, nagios_servicegroups=None):
18+ def write(self, nagios_context, hostname, nagios_servicegroups):
19 nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format(
20 self.command)
21 with open(nrpe_check_file, 'w') as nrpe_check_config:
22@@ -177,14 +179,11 @@
23 nagios_servicegroups)
24
25 def write_service_config(self, nagios_context, hostname,
26- nagios_servicegroups=None):
27+ nagios_servicegroups):
28 for f in os.listdir(NRPE.nagios_exportdir):
29 if re.search('.*{}.cfg'.format(self.command), f):
30 os.remove(os.path.join(NRPE.nagios_exportdir, f))
31
32- if not nagios_servicegroups:
33- nagios_servicegroups = nagios_context
34-
35 templ_vars = {
36 'nagios_hostname': hostname,
37 'nagios_servicegroup': nagios_servicegroups,
38@@ -211,10 +210,10 @@
39 super(NRPE, self).__init__()
40 self.config = config()
41 self.nagios_context = self.config['nagios_context']
42- if 'nagios_servicegroups' in self.config:
43+ if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']:
44 self.nagios_servicegroups = self.config['nagios_servicegroups']
45 else:
46- self.nagios_servicegroups = 'juju'
47+ self.nagios_servicegroups = self.nagios_context
48 self.unit_name = local_unit().replace('/', '-')
49 if hostname:
50 self.hostname = hostname
51@@ -322,3 +321,38 @@
52 check_cmd='check_status_file.py -f '
53 '/var/lib/nagios/service-check-%s.txt' % svc,
54 )
55+
56+
57+def copy_nrpe_checks():
58+ """
59+ Copy the nrpe checks into place
60+
61+ """
62+ NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins'
63+ nrpe_files_dir = os.path.join(os.getenv('CHARM_DIR'), 'hooks',
64+ 'charmhelpers', 'contrib', 'openstack',
65+ 'files')
66+
67+ if not os.path.exists(NAGIOS_PLUGINS):
68+ os.makedirs(NAGIOS_PLUGINS)
69+ for fname in glob.glob(os.path.join(nrpe_files_dir, "check_*")):
70+ if os.path.isfile(fname):
71+ shutil.copy2(fname,
72+ os.path.join(NAGIOS_PLUGINS, os.path.basename(fname)))
73+
74+
75+def add_haproxy_checks(nrpe, unit_name):
76+ """
77+ Add checks for each service in list
78+
79+ :param NRPE nrpe: NRPE object to add check to
80+ :param str unit_name: Unit name to use in check description
81+ """
82+ nrpe.add_check(
83+ shortname='haproxy_servers',
84+ description='Check HAProxy {%s}' % unit_name,
85+ check_cmd='check_haproxy.sh')
86+ nrpe.add_check(
87+ shortname='haproxy_queue',
88+ description='Check HAProxy queue depth {%s}' % unit_name,
89+ check_cmd='check_haproxy_queue_depth.sh')
90
91=== modified file 'hooks/charmhelpers/contrib/hahelpers/cluster.py'
92--- hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-01-26 09:45:43 +0000
93+++ hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-02-19 20:31:59 +0000
94@@ -48,6 +48,9 @@
95 from charmhelpers.core.decorators import (
96 retry_on_exception,
97 )
98+from charmhelpers.core.strutils import (
99+ bool_from_string,
100+)
101
102
103 class HAIncompleteConfig(Exception):
104@@ -164,7 +167,8 @@
105 .
106 returns: boolean
107 '''
108- if config_get('use-https') == "yes":
109+ use_https = config_get('use-https')
110+ if use_https and bool_from_string(use_https):
111 return True
112 if config_get('ssl_cert') and config_get('ssl_key'):
113 return True
114
115=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py'
116--- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-01-26 09:45:43 +0000
117+++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-02-19 20:31:59 +0000
118@@ -71,16 +71,19 @@
119 services.append(this_service)
120 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
121 'ceph-osd', 'ceph-radosgw']
122+ # Openstack subordinate charms do not expose an origin option as that
123+ # is controlled by the principle
124+ ignore = ['neutron-openvswitch']
125
126 if self.openstack:
127 for svc in services:
128- if svc['name'] not in use_source:
129+ if svc['name'] not in use_source + ignore:
130 config = {'openstack-origin': self.openstack}
131 self.d.configure(svc['name'], config)
132
133 if self.source:
134 for svc in services:
135- if svc['name'] in use_source:
136+ if svc['name'] in use_source and svc['name'] not in ignore:
137 config = {'source': self.source}
138 self.d.configure(svc['name'], config)
139
140
141=== added directory 'hooks/charmhelpers/contrib/openstack/files'
142=== added file 'hooks/charmhelpers/contrib/openstack/files/__init__.py'
143--- hooks/charmhelpers/contrib/openstack/files/__init__.py 1970-01-01 00:00:00 +0000
144+++ hooks/charmhelpers/contrib/openstack/files/__init__.py 2015-02-19 20:31:59 +0000
145@@ -0,0 +1,18 @@
146+# Copyright 2014-2015 Canonical Limited.
147+#
148+# This file is part of charm-helpers.
149+#
150+# charm-helpers is free software: you can redistribute it and/or modify
151+# it under the terms of the GNU Lesser General Public License version 3 as
152+# published by the Free Software Foundation.
153+#
154+# charm-helpers is distributed in the hope that it will be useful,
155+# but WITHOUT ANY WARRANTY; without even the implied warranty of
156+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
157+# GNU Lesser General Public License for more details.
158+#
159+# You should have received a copy of the GNU Lesser General Public License
160+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
161+
162+# dummy __init__.py to fool syncer into thinking this is a syncable python
163+# module
164
165=== added file 'hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh'
166--- hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh 1970-01-01 00:00:00 +0000
167+++ hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh 2015-02-19 20:31:59 +0000
168@@ -0,0 +1,32 @@
169+#!/bin/bash
170+#--------------------------------------------
171+# This file is managed by Juju
172+#--------------------------------------------
173+#
174+# Copyright 2009,2012 Canonical Ltd.
175+# Author: Tom Haddon
176+
177+CRITICAL=0
178+NOTACTIVE=''
179+LOGFILE=/var/log/nagios/check_haproxy.log
180+AUTH=$(grep -r "stats auth" /etc/haproxy | head -1 | awk '{print $4}')
181+
182+for appserver in $(grep ' server' /etc/haproxy/haproxy.cfg | awk '{print $2'});
183+do
184+ output=$(/usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 --regex="class=\"(active|backup)(2|3).*${appserver}" -e ' 200 OK')
185+ if [ $? != 0 ]; then
186+ date >> $LOGFILE
187+ echo $output >> $LOGFILE
188+ /usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 -v | grep $appserver >> $LOGFILE 2>&1
189+ CRITICAL=1
190+ NOTACTIVE="${NOTACTIVE} $appserver"
191+ fi
192+done
193+
194+if [ $CRITICAL = 1 ]; then
195+ echo "CRITICAL:${NOTACTIVE}"
196+ exit 2
197+fi
198+
199+echo "OK: All haproxy instances looking good"
200+exit 0
201
202=== added file 'hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh'
203--- hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh 1970-01-01 00:00:00 +0000
204+++ hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh 2015-02-19 20:31:59 +0000
205@@ -0,0 +1,30 @@
206+#!/bin/bash
207+#--------------------------------------------
208+# This file is managed by Juju
209+#--------------------------------------------
210+#
211+# Copyright 2009,2012 Canonical Ltd.
212+# Author: Tom Haddon
213+
214+# These should be config options at some stage
215+CURRQthrsh=0
216+MAXQthrsh=100
217+
218+AUTH=$(grep -r "stats auth" /etc/haproxy | head -1 | awk '{print $4}')
219+
220+HAPROXYSTATS=$(/usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 -u '/;csv' -v)
221+
222+for BACKEND in $(echo $HAPROXYSTATS| xargs -n1 | grep BACKEND | awk -F , '{print $1}')
223+do
224+ CURRQ=$(echo "$HAPROXYSTATS" | grep $BACKEND | grep BACKEND | cut -d , -f 3)
225+ MAXQ=$(echo "$HAPROXYSTATS" | grep $BACKEND | grep BACKEND | cut -d , -f 4)
226+
227+ if [[ $CURRQ -gt $CURRQthrsh || $MAXQ -gt $MAXQthrsh ]] ; then
228+ echo "CRITICAL: queue depth for $BACKEND - CURRENT:$CURRQ MAX:$MAXQ"
229+ exit 2
230+ fi
231+done
232+
233+echo "OK: All haproxy queue depths looking good"
234+exit 0
235+
236
237=== modified file 'hooks/charmhelpers/contrib/openstack/ip.py'
238--- hooks/charmhelpers/contrib/openstack/ip.py 2015-01-26 09:45:43 +0000
239+++ hooks/charmhelpers/contrib/openstack/ip.py 2015-02-19 20:31:59 +0000
240@@ -26,6 +26,8 @@
241 )
242 from charmhelpers.contrib.hahelpers.cluster import is_clustered
243
244+from functools import partial
245+
246 PUBLIC = 'public'
247 INTERNAL = 'int'
248 ADMIN = 'admin'
249@@ -107,3 +109,38 @@
250 "clustered=%s)" % (net_type, clustered))
251
252 return resolved_address
253+
254+
255+def endpoint_url(configs, url_template, port, endpoint_type=PUBLIC,
256+ override=None):
257+ """Returns the correct endpoint URL to advertise to Keystone.
258+
259+ This method provides the correct endpoint URL which should be advertised to
260+ the keystone charm for endpoint creation. This method allows for the url to
261+ be overridden to force a keystone endpoint to have specific URL for any of
262+ the defined scopes (admin, internal, public).
263+
264+ :param configs: OSTemplateRenderer config templating object to inspect
265+ for a complete https context.
266+ :param url_template: str format string for creating the url template. Only
267+ two values will be passed - the scheme+hostname
268+ returned by the canonical_url and the port.
269+ :param endpoint_type: str endpoint type to resolve.
270+ :param override: str the name of the config option which overrides the
271+ endpoint URL defined by the charm itself. None will
272+ disable any overrides (default).
273+ """
274+ if override:
275+ # Return any user-defined overrides for the keystone endpoint URL.
276+ user_value = config(override)
277+ if user_value:
278+ return user_value.strip()
279+
280+ return url_template % (canonical_url(configs, endpoint_type), port)
281+
282+
283+public_endpoint = partial(endpoint_url, endpoint_type=PUBLIC)
284+
285+internal_endpoint = partial(endpoint_url, endpoint_type=INTERNAL)
286+
287+admin_endpoint = partial(endpoint_url, endpoint_type=ADMIN)
288
289=== modified file 'hooks/charmhelpers/contrib/openstack/utils.py'
290--- hooks/charmhelpers/contrib/openstack/utils.py 2015-01-26 09:45:43 +0000
291+++ hooks/charmhelpers/contrib/openstack/utils.py 2015-02-19 20:31:59 +0000
292@@ -103,6 +103,7 @@
293 ('2.1.0', 'juno'),
294 ('2.2.0', 'juno'),
295 ('2.2.1', 'kilo'),
296+ ('2.2.2', 'kilo'),
297 ])
298
299 DEFAULT_LOOPBACK_SIZE = '5G'
300
301=== modified file 'hooks/charmhelpers/contrib/python/packages.py'
302--- hooks/charmhelpers/contrib/python/packages.py 2015-01-26 09:45:43 +0000
303+++ hooks/charmhelpers/contrib/python/packages.py 2015-02-19 20:31:59 +0000
304@@ -17,8 +17,6 @@
305 # You should have received a copy of the GNU Lesser General Public License
306 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
307
308-__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
309-
310 from charmhelpers.fetch import apt_install, apt_update
311 from charmhelpers.core.hookenv import log
312
313@@ -29,6 +27,8 @@
314 apt_install('python-pip')
315 from pip import main as pip_execute
316
317+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
318+
319
320 def parse_options(given, available):
321 """Given a set of options, check if available"""
322
323=== modified file 'hooks/charmhelpers/core/fstab.py'
324--- hooks/charmhelpers/core/fstab.py 2015-01-26 09:45:43 +0000
325+++ hooks/charmhelpers/core/fstab.py 2015-02-19 20:31:59 +0000
326@@ -17,11 +17,11 @@
327 # You should have received a copy of the GNU Lesser General Public License
328 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
329
330-__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
331-
332 import io
333 import os
334
335+__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
336+
337
338 class Fstab(io.FileIO):
339 """This class extends file in order to implement a file reader/writer
340@@ -77,7 +77,7 @@
341 for line in self.readlines():
342 line = line.decode('us-ascii')
343 try:
344- if line.strip() and not line.startswith("#"):
345+ if line.strip() and not line.strip().startswith("#"):
346 yield self._hydrate_entry(line)
347 except ValueError:
348 pass
349@@ -104,7 +104,7 @@
350
351 found = False
352 for index, line in enumerate(lines):
353- if not line.startswith("#"):
354+ if line.strip() and not line.strip().startswith("#"):
355 if self._hydrate_entry(line) == entry:
356 found = True
357 break
358
359=== modified file 'hooks/charmhelpers/core/host.py'
360--- hooks/charmhelpers/core/host.py 2015-01-26 09:45:43 +0000
361+++ hooks/charmhelpers/core/host.py 2015-02-19 20:31:59 +0000
362@@ -191,11 +191,11 @@
363
364
365 def write_file(path, content, owner='root', group='root', perms=0o444):
366- """Create or overwrite a file with the contents of a string"""
367+ """Create or overwrite a file with the contents of a byte string."""
368 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
369 uid = pwd.getpwnam(owner).pw_uid
370 gid = grp.getgrnam(group).gr_gid
371- with open(path, 'w') as target:
372+ with open(path, 'wb') as target:
373 os.fchown(target.fileno(), uid, gid)
374 os.fchmod(target.fileno(), perms)
375 target.write(content)
376@@ -305,11 +305,11 @@
377 ceph_client_changed function.
378 """
379 def wrap(f):
380- def wrapped_f(*args):
381+ def wrapped_f(*args, **kwargs):
382 checksums = {}
383 for path in restart_map:
384 checksums[path] = file_hash(path)
385- f(*args)
386+ f(*args, **kwargs)
387 restarts = []
388 for path in restart_map:
389 if checksums[path] != file_hash(path):
390@@ -361,7 +361,7 @@
391 ip_output = (line for line in ip_output if line)
392 for line in ip_output:
393 if line.split()[1].startswith(int_type):
394- matched = re.search('.*: (bond[0-9]+\.[0-9]+)@.*', line)
395+ matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line)
396 if matched:
397 interface = matched.groups()[0]
398 else:
399
400=== added file 'hooks/charmhelpers/core/strutils.py'
401--- hooks/charmhelpers/core/strutils.py 1970-01-01 00:00:00 +0000
402+++ hooks/charmhelpers/core/strutils.py 2015-02-19 20:31:59 +0000
403@@ -0,0 +1,42 @@
404+#!/usr/bin/env python
405+# -*- coding: utf-8 -*-
406+
407+# Copyright 2014-2015 Canonical Limited.
408+#
409+# This file is part of charm-helpers.
410+#
411+# charm-helpers is free software: you can redistribute it and/or modify
412+# it under the terms of the GNU Lesser General Public License version 3 as
413+# published by the Free Software Foundation.
414+#
415+# charm-helpers is distributed in the hope that it will be useful,
416+# but WITHOUT ANY WARRANTY; without even the implied warranty of
417+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
418+# GNU Lesser General Public License for more details.
419+#
420+# You should have received a copy of the GNU Lesser General Public License
421+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
422+
423+import six
424+
425+
426+def bool_from_string(value):
427+ """Interpret string value as boolean.
428+
429+ Returns True if value translates to True otherwise False.
430+ """
431+ if isinstance(value, six.string_types):
432+ value = six.text_type(value)
433+ else:
434+ msg = "Unable to interpret non-string value '%s' as boolean" % (value)
435+ raise ValueError(msg)
436+
437+ value = value.strip().lower()
438+
439+ if value in ['y', 'yes', 'true', 't']:
440+ return True
441+ elif value in ['n', 'no', 'false', 'f']:
442+ return False
443+
444+ msg = "Unable to interpret string value '%s' as boolean" % (value)
445+ raise ValueError(msg)
446
447=== modified file 'hooks/charmhelpers/core/sysctl.py'
448--- hooks/charmhelpers/core/sysctl.py 2015-01-26 09:45:43 +0000
449+++ hooks/charmhelpers/core/sysctl.py 2015-02-19 20:31:59 +0000
450@@ -17,8 +17,6 @@
451 # You should have received a copy of the GNU Lesser General Public License
452 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
453
454-__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
455-
456 import yaml
457
458 from subprocess import check_call
459@@ -26,25 +24,33 @@
460 from charmhelpers.core.hookenv import (
461 log,
462 DEBUG,
463+ ERROR,
464 )
465
466+__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
467+
468
469 def create(sysctl_dict, sysctl_file):
470 """Creates a sysctl.conf file from a YAML associative array
471
472- :param sysctl_dict: a dict of sysctl options eg { 'kernel.max_pid': 1337 }
473- :type sysctl_dict: dict
474+ :param sysctl_dict: a YAML-formatted string of sysctl options eg "{ 'kernel.max_pid': 1337 }"
475+ :type sysctl_dict: str
476 :param sysctl_file: path to the sysctl file to be saved
477 :type sysctl_file: str or unicode
478 :returns: None
479 """
480- sysctl_dict = yaml.load(sysctl_dict)
481+ try:
482+ sysctl_dict_parsed = yaml.safe_load(sysctl_dict)
483+ except yaml.YAMLError:
484+ log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict),
485+ level=ERROR)
486+ return
487
488 with open(sysctl_file, "w") as fd:
489- for key, value in sysctl_dict.items():
490+ for key, value in sysctl_dict_parsed.items():
491 fd.write("{}={}\n".format(key, value))
492
493- log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict),
494+ log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict_parsed),
495 level=DEBUG)
496
497 check_call(["sysctl", "-p", sysctl_file])
498
499=== modified file 'hooks/charmhelpers/core/templating.py'
500--- hooks/charmhelpers/core/templating.py 2015-01-26 09:45:43 +0000
501+++ hooks/charmhelpers/core/templating.py 2015-02-19 20:31:59 +0000
502@@ -21,7 +21,7 @@
503
504
505 def render(source, target, context, owner='root', group='root',
506- perms=0o444, templates_dir=None):
507+ perms=0o444, templates_dir=None, encoding='UTF-8'):
508 """
509 Render a template.
510
511@@ -64,5 +64,5 @@
512 level=hookenv.ERROR)
513 raise e
514 content = template.render(context)
515- host.mkdir(os.path.dirname(target), owner, group)
516- host.write_file(target, content, owner, group, perms)
517+ host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
518+ host.write_file(target, content.encode(encoding), owner, group, perms)
519
520=== added file 'hooks/charmhelpers/core/unitdata.py'
521--- hooks/charmhelpers/core/unitdata.py 1970-01-01 00:00:00 +0000
522+++ hooks/charmhelpers/core/unitdata.py 2015-02-19 20:31:59 +0000
523@@ -0,0 +1,477 @@
524+#!/usr/bin/env python
525+# -*- coding: utf-8 -*-
526+#
527+# Copyright 2014-2015 Canonical Limited.
528+#
529+# This file is part of charm-helpers.
530+#
531+# charm-helpers is free software: you can redistribute it and/or modify
532+# it under the terms of the GNU Lesser General Public License version 3 as
533+# published by the Free Software Foundation.
534+#
535+# charm-helpers is distributed in the hope that it will be useful,
536+# but WITHOUT ANY WARRANTY; without even the implied warranty of
537+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
538+# GNU Lesser General Public License for more details.
539+#
540+# You should have received a copy of the GNU Lesser General Public License
541+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
542+#
543+#
544+# Authors:
545+# Kapil Thangavelu <kapil.foss@gmail.com>
546+#
547+"""
548+Intro
549+-----
550+
551+A simple way to store state in units. This provides a key value
552+storage with support for versioned, transactional operation,
553+and can calculate deltas from previous values to simplify unit logic
554+when processing changes.
555+
556+
557+Hook Integration
558+----------------
559+
560+There are several extant frameworks for hook execution, including
561+
562+ - charmhelpers.core.hookenv.Hooks
563+ - charmhelpers.core.services.ServiceManager
564+
565+The storage classes are framework agnostic, one simple integration is
566+via the HookData contextmanager. It will record the current hook
567+execution environment (including relation data, config data, etc.),
568+setup a transaction and allow easy access to the changes from
569+previously seen values. One consequence of the integration is the
570+reservation of particular keys ('rels', 'unit', 'env', 'config',
571+'charm_revisions') for their respective values.
572+
573+Here's a fully worked integration example using hookenv.Hooks::
574+
575+ from charmhelper.core import hookenv, unitdata
576+
577+ hook_data = unitdata.HookData()
578+ db = unitdata.kv()
579+ hooks = hookenv.Hooks()
580+
581+ @hooks.hook
582+ def config_changed():
583+ # Print all changes to configuration from previously seen
584+ # values.
585+ for changed, (prev, cur) in hook_data.conf.items():
586+ print('config changed', changed,
587+ 'previous value', prev,
588+ 'current value', cur)
589+
590+ # Get some unit specific bookeeping
591+ if not db.get('pkg_key'):
592+ key = urllib.urlopen('https://example.com/pkg_key').read()
593+ db.set('pkg_key', key)
594+
595+ # Directly access all charm config as a mapping.
596+ conf = db.getrange('config', True)
597+
598+ # Directly access all relation data as a mapping
599+ rels = db.getrange('rels', True)
600+
601+ if __name__ == '__main__':
602+ with hook_data():
603+ hook.execute()
604+
605+
606+A more basic integration is via the hook_scope context manager which simply
607+manages transaction scope (and records hook name, and timestamp)::
608+
609+ >>> from unitdata import kv
610+ >>> db = kv()
611+ >>> with db.hook_scope('install'):
612+ ... # do work, in transactional scope.
613+ ... db.set('x', 1)
614+ >>> db.get('x')
615+ 1
616+
617+
618+Usage
619+-----
620+
621+Values are automatically json de/serialized to preserve basic typing
622+and complex data struct capabilities (dicts, lists, ints, booleans, etc).
623+
624+Individual values can be manipulated via get/set::
625+
626+ >>> kv.set('y', True)
627+ >>> kv.get('y')
628+ True
629+
630+ # We can set complex values (dicts, lists) as a single key.
631+ >>> kv.set('config', {'a': 1, 'b': True'})
632+
633+ # Also supports returning dictionaries as a record which
634+ # provides attribute access.
635+ >>> config = kv.get('config', record=True)
636+ >>> config.b
637+ True
638+
639+
640+Groups of keys can be manipulated with update/getrange::
641+
642+ >>> kv.update({'z': 1, 'y': 2}, prefix="gui.")
643+ >>> kv.getrange('gui.', strip=True)
644+ {'z': 1, 'y': 2}
645+
646+When updating values, its very helpful to understand which values
647+have actually changed and how have they changed. The storage
648+provides a delta method to provide for this::
649+
650+ >>> data = {'debug': True, 'option': 2}
651+ >>> delta = kv.delta(data, 'config.')
652+ >>> delta.debug.previous
653+ None
654+ >>> delta.debug.current
655+ True
656+ >>> delta
657+ {'debug': (None, True), 'option': (None, 2)}
658+
659+Note the delta method does not persist the actual change, it needs to
660+be explicitly saved via 'update' method::
661+
662+ >>> kv.update(data, 'config.')
663+
664+Values modified in the context of a hook scope retain historical values
665+associated to the hookname.
666+
667+ >>> with db.hook_scope('config-changed'):
668+ ... db.set('x', 42)
669+ >>> db.gethistory('x')
670+ [(1, u'x', 1, u'install', u'2015-01-21T16:49:30.038372'),
671+ (2, u'x', 42, u'config-changed', u'2015-01-21T16:49:30.038786')]
672+
673+"""
674+
675+import collections
676+import contextlib
677+import datetime
678+import json
679+import os
680+import pprint
681+import sqlite3
682+import sys
683+
684+__author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>'
685+
686+
687+class Storage(object):
688+ """Simple key value database for local unit state within charms.
689+
690+ Modifications are automatically committed at hook exit. That's
691+ currently regardless of exit code.
692+
693+ To support dicts, lists, integer, floats, and booleans values
694+ are automatically json encoded/decoded.
695+ """
696+ def __init__(self, path=None):
697+ self.db_path = path
698+ if path is None:
699+ self.db_path = os.path.join(
700+ os.environ.get('CHARM_DIR', ''), '.unit-state.db')
701+ self.conn = sqlite3.connect('%s' % self.db_path)
702+ self.cursor = self.conn.cursor()
703+ self.revision = None
704+ self._closed = False
705+ self._init()
706+
707+ def close(self):
708+ if self._closed:
709+ return
710+ self.flush(False)
711+ self.cursor.close()
712+ self.conn.close()
713+ self._closed = True
714+
715+ def _scoped_query(self, stmt, params=None):
716+ if params is None:
717+ params = []
718+ return stmt, params
719+
720+ def get(self, key, default=None, record=False):
721+ self.cursor.execute(
722+ *self._scoped_query(
723+ 'select data from kv where key=?', [key]))
724+ result = self.cursor.fetchone()
725+ if not result:
726+ return default
727+ if record:
728+ return Record(json.loads(result[0]))
729+ return json.loads(result[0])
730+
731+ def getrange(self, key_prefix, strip=False):
732+ stmt = "select key, data from kv where key like '%s%%'" % key_prefix
733+ self.cursor.execute(*self._scoped_query(stmt))
734+ result = self.cursor.fetchall()
735+
736+ if not result:
737+ return None
738+ if not strip:
739+ key_prefix = ''
740+ return dict([
741+ (k[len(key_prefix):], json.loads(v)) for k, v in result])
742+
743+ def update(self, mapping, prefix=""):
744+ for k, v in mapping.items():
745+ self.set("%s%s" % (prefix, k), v)
746+
747+ def unset(self, key):
748+ self.cursor.execute('delete from kv where key=?', [key])
749+ if self.revision and self.cursor.rowcount:
750+ self.cursor.execute(
751+ 'insert into kv_revisions values (?, ?, ?)',
752+ [key, self.revision, json.dumps('DELETED')])
753+
754+ def set(self, key, value):
755+ serialized = json.dumps(value)
756+
757+ self.cursor.execute(
758+ 'select data from kv where key=?', [key])
759+ exists = self.cursor.fetchone()
760+
761+ # Skip mutations to the same value
762+ if exists:
763+ if exists[0] == serialized:
764+ return value
765+
766+ if not exists:
767+ self.cursor.execute(
768+ 'insert into kv (key, data) values (?, ?)',
769+ (key, serialized))
770+ else:
771+ self.cursor.execute('''
772+ update kv
773+ set data = ?
774+ where key = ?''', [serialized, key])
775+
776+ # Save
777+ if not self.revision:
778+ return value
779+
780+ self.cursor.execute(
781+ 'select 1 from kv_revisions where key=? and revision=?',
782+ [key, self.revision])
783+ exists = self.cursor.fetchone()
784+
785+ if not exists:
786+ self.cursor.execute(
787+ '''insert into kv_revisions (
788+ revision, key, data) values (?, ?, ?)''',
789+ (self.revision, key, serialized))
790+ else:
791+ self.cursor.execute(
792+ '''
793+ update kv_revisions
794+ set data = ?
795+ where key = ?
796+ and revision = ?''',
797+ [serialized, key, self.revision])
798+
799+ return value
800+
801+ def delta(self, mapping, prefix):
802+ """
803+ return a delta containing values that have changed.
804+ """
805+ previous = self.getrange(prefix, strip=True)
806+ if not previous:
807+ pk = set()
808+ else:
809+ pk = set(previous.keys())
810+ ck = set(mapping.keys())
811+ delta = DeltaSet()
812+
813+ # added
814+ for k in ck.difference(pk):
815+ delta[k] = Delta(None, mapping[k])
816+
817+ # removed
818+ for k in pk.difference(ck):
819+ delta[k] = Delta(previous[k], None)
820+
821+ # changed
822+ for k in pk.intersection(ck):
823+ c = mapping[k]
824+ p = previous[k]
825+ if c != p:
826+ delta[k] = Delta(p, c)
827+
828+ return delta
829+
830+ @contextlib.contextmanager
831+ def hook_scope(self, name=""):
832+ """Scope all future interactions to the current hook execution
833+ revision."""
834+ assert not self.revision
835+ self.cursor.execute(
836+ 'insert into hooks (hook, date) values (?, ?)',
837+ (name or sys.argv[0],
838+ datetime.datetime.utcnow().isoformat()))
839+ self.revision = self.cursor.lastrowid
840+ try:
841+ yield self.revision
842+ self.revision = None
843+ except:
844+ self.flush(False)
845+ self.revision = None
846+ raise
847+ else:
848+ self.flush()
849+
850+ def flush(self, save=True):
851+ if save:
852+ self.conn.commit()
853+ elif self._closed:
854+ return
855+ else:
856+ self.conn.rollback()
857+
858+ def _init(self):
859+ self.cursor.execute('''
860+ create table if not exists kv (
861+ key text,
862+ data text,
863+ primary key (key)
864+ )''')
865+ self.cursor.execute('''
866+ create table if not exists kv_revisions (
867+ key text,
868+ revision integer,
869+ data text,
870+ primary key (key, revision)
871+ )''')
872+ self.cursor.execute('''
873+ create table if not exists hooks (
874+ version integer primary key autoincrement,
875+ hook text,
876+ date text
877+ )''')
878+ self.conn.commit()
879+
880+ def gethistory(self, key, deserialize=False):
881+ self.cursor.execute(
882+ '''
883+ select kv.revision, kv.key, kv.data, h.hook, h.date
884+ from kv_revisions kv,
885+ hooks h
886+ where kv.key=?
887+ and kv.revision = h.version
888+ ''', [key])
889+ if deserialize is False:
890+ return self.cursor.fetchall()
891+ return map(_parse_history, self.cursor.fetchall())
892+
893+ def debug(self, fh=sys.stderr):
894+ self.cursor.execute('select * from kv')
895+ pprint.pprint(self.cursor.fetchall(), stream=fh)
896+ self.cursor.execute('select * from kv_revisions')
897+ pprint.pprint(self.cursor.fetchall(), stream=fh)
898+
899+
900+def _parse_history(d):
901+ return (d[0], d[1], json.loads(d[2]), d[3],
902+ datetime.datetime.strptime(d[-1], "%Y-%m-%dT%H:%M:%S.%f"))
903+
904+
905+class HookData(object):
906+ """Simple integration for existing hook exec frameworks.
907+
908+ Records all unit information, and stores deltas for processing
909+ by the hook.
910+
911+ Sample::
912+
913+ from charmhelper.core import hookenv, unitdata
914+
915+ changes = unitdata.HookData()
916+ db = unitdata.kv()
917+ hooks = hookenv.Hooks()
918+
919+ @hooks.hook
920+ def config_changed():
921+ # View all changes to configuration
922+ for changed, (prev, cur) in changes.conf.items():
923+ print('config changed', changed,
924+ 'previous value', prev,
925+ 'current value', cur)
926+
927+ # Get some unit specific bookeeping
928+ if not db.get('pkg_key'):
929+ key = urllib.urlopen('https://example.com/pkg_key').read()
930+ db.set('pkg_key', key)
931+
932+ if __name__ == '__main__':
933+ with changes():
934+ hook.execute()
935+
936+ """
937+ def __init__(self):
938+ self.kv = kv()
939+ self.conf = None
940+ self.rels = None
941+
942+ @contextlib.contextmanager
943+ def __call__(self):
944+ from charmhelpers.core import hookenv
945+ hook_name = hookenv.hook_name()
946+
947+ with self.kv.hook_scope(hook_name):
948+ self._record_charm_version(hookenv.charm_dir())
949+ delta_config, delta_relation = self._record_hook(hookenv)
950+ yield self.kv, delta_config, delta_relation
951+
952+ def _record_charm_version(self, charm_dir):
953+ # Record revisions.. charm revisions are meaningless
954+ # to charm authors as they don't control the revision.
955+ # so logic dependnent on revision is not particularly
956+ # useful, however it is useful for debugging analysis.
957+ charm_rev = open(
958+ os.path.join(charm_dir, 'revision')).read().strip()
959+ charm_rev = charm_rev or '0'
960+ revs = self.kv.get('charm_revisions', [])
961+ if charm_rev not in revs:
962+ revs.append(charm_rev.strip() or '0')
963+ self.kv.set('charm_revisions', revs)
964+
965+ def _record_hook(self, hookenv):
966+ data = hookenv.execution_environment()
967+ self.conf = conf_delta = self.kv.delta(data['conf'], 'config')
968+ self.rels = rels_delta = self.kv.delta(data['rels'], 'rels')
969+ self.kv.set('env', data['env'])
970+ self.kv.set('unit', data['unit'])
971+ self.kv.set('relid', data.get('relid'))
972+ return conf_delta, rels_delta
973+
974+
975+class Record(dict):
976+
977+ __slots__ = ()
978+
979+ def __getattr__(self, k):
980+ if k in self:
981+ return self[k]
982+ raise AttributeError(k)
983+
984+
985+class DeltaSet(Record):
986+
987+ __slots__ = ()
988+
989+
990+Delta = collections.namedtuple('Delta', ['previous', 'current'])
991+
992+
993+_KV = None
994+
995+
996+def kv():
997+ global _KV
998+ if _KV is None:
999+ _KV = Storage()
1000+ return _KV
1001
1002=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
1003--- hooks/charmhelpers/fetch/archiveurl.py 2015-01-26 09:45:43 +0000
1004+++ hooks/charmhelpers/fetch/archiveurl.py 2015-02-19 20:31:59 +0000
1005@@ -18,6 +18,16 @@
1006 import hashlib
1007 import re
1008
1009+from charmhelpers.fetch import (
1010+ BaseFetchHandler,
1011+ UnhandledSource
1012+)
1013+from charmhelpers.payload.archive import (
1014+ get_archive_handler,
1015+ extract,
1016+)
1017+from charmhelpers.core.host import mkdir, check_hash
1018+
1019 import six
1020 if six.PY3:
1021 from urllib.request import (
1022@@ -35,16 +45,6 @@
1023 )
1024 from urlparse import urlparse, urlunparse, parse_qs
1025
1026-from charmhelpers.fetch import (
1027- BaseFetchHandler,
1028- UnhandledSource
1029-)
1030-from charmhelpers.payload.archive import (
1031- get_archive_handler,
1032- extract,
1033-)
1034-from charmhelpers.core.host import mkdir, check_hash
1035-
1036
1037 def splituser(host):
1038 '''urllib.splituser(), but six's support of this seems broken'''
1039
1040=== modified file 'hooks/charmhelpers/fetch/giturl.py'
1041--- hooks/charmhelpers/fetch/giturl.py 2015-01-26 09:45:43 +0000
1042+++ hooks/charmhelpers/fetch/giturl.py 2015-02-19 20:31:59 +0000
1043@@ -32,7 +32,7 @@
1044 apt_install("python-git")
1045 from git import Repo
1046
1047-from git.exc import GitCommandError
1048+from git.exc import GitCommandError # noqa E402
1049
1050
1051 class GitUrlFetchHandler(BaseFetchHandler):
1052
1053=== modified file 'tests/charmhelpers/contrib/amulet/utils.py'
1054--- tests/charmhelpers/contrib/amulet/utils.py 2015-01-26 09:45:43 +0000
1055+++ tests/charmhelpers/contrib/amulet/utils.py 2015-02-19 20:31:59 +0000
1056@@ -169,8 +169,13 @@
1057 cmd = 'pgrep -o -f {}'.format(service)
1058 else:
1059 cmd = 'pgrep -o {}'.format(service)
1060- proc_dir = '/proc/{}'.format(sentry_unit.run(cmd)[0].strip())
1061- return self._get_dir_mtime(sentry_unit, proc_dir)
1062+ cmd = cmd + ' | grep -v pgrep || exit 0'
1063+ cmd_out = sentry_unit.run(cmd)
1064+ self.log.debug('CMDout: ' + str(cmd_out))
1065+ if cmd_out[0]:
1066+ self.log.debug('Pid for %s %s' % (service, str(cmd_out[0])))
1067+ proc_dir = '/proc/{}'.format(cmd_out[0].strip())
1068+ return self._get_dir_mtime(sentry_unit, proc_dir)
1069
1070 def service_restarted(self, sentry_unit, service, filename,
1071 pgrep_full=False, sleep_time=20):
1072@@ -187,6 +192,121 @@
1073 else:
1074 return False
1075
1076+ def service_restarted_since(self, sentry_unit, mtime, service,
1077+ pgrep_full=False, sleep_time=20,
1078+ retry_count=2):
1079+ """Check if service was been started after a given time.
1080+
1081+ Args:
1082+ sentry_unit (sentry): The sentry unit to check for the service on
1083+ mtime (float): The epoch time to check against
1084+ service (string): service name to look for in process table
1085+ pgrep_full (boolean): Use full command line search mode with pgrep
1086+ sleep_time (int): Seconds to sleep before looking for process
1087+ retry_count (int): If service is not found, how many times to retry
1088+
1089+ Returns:
1090+ bool: True if service found and its start time it newer than mtime,
1091+ False if service is older than mtime or if service was
1092+ not found.
1093+ """
1094+ self.log.debug('Checking %s restarted since %s' % (service, mtime))
1095+ time.sleep(sleep_time)
1096+ proc_start_time = self._get_proc_start_time(sentry_unit, service,
1097+ pgrep_full)
1098+ while retry_count > 0 and not proc_start_time:
1099+ self.log.debug('No pid file found for service %s, will retry %i '
1100+ 'more times' % (service, retry_count))
1101+ time.sleep(30)
1102+ proc_start_time = self._get_proc_start_time(sentry_unit, service,
1103+ pgrep_full)
1104+ retry_count = retry_count - 1
1105+
1106+ if not proc_start_time:
1107+ self.log.warn('No proc start time found, assuming service did '
1108+ 'not start')
1109+ return False
1110+ if proc_start_time >= mtime:
1111+ self.log.debug('proc start time is newer than provided mtime'
1112+ '(%s >= %s)' % (proc_start_time, mtime))
1113+ return True
1114+ else:
1115+ self.log.warn('proc start time (%s) is older than provided mtime '
1116+ '(%s), service did not restart' % (proc_start_time,
1117+ mtime))
1118+ return False
1119+
1120+ def config_updated_since(self, sentry_unit, filename, mtime,
1121+ sleep_time=20):
1122+ """Check if file was modified after a given time.
1123+
1124+ Args:
1125+ sentry_unit (sentry): The sentry unit to check the file mtime on
1126+ filename (string): The file to check mtime of
1127+ mtime (float): The epoch time to check against
1128+ sleep_time (int): Seconds to sleep before looking for process
1129+
1130+ Returns:
1131+ bool: True if file was modified more recently than mtime, False if
1132+ file was modified before mtime,
1133+ """
1134+ self.log.debug('Checking %s updated since %s' % (filename, mtime))
1135+ time.sleep(sleep_time)
1136+ file_mtime = self._get_file_mtime(sentry_unit, filename)
1137+ if file_mtime >= mtime:
1138+ self.log.debug('File mtime is newer than provided mtime '
1139+ '(%s >= %s)' % (file_mtime, mtime))
1140+ return True
1141+ else:
1142+ self.log.warn('File mtime %s is older than provided mtime %s'
1143+ % (file_mtime, mtime))
1144+ return False
1145+
1146+ def validate_service_config_changed(self, sentry_unit, mtime, service,
1147+ filename, pgrep_full=False,
1148+ sleep_time=20, retry_count=2):
1149+ """Check service and file were updated after mtime
1150+
1151+ Args:
1152+ sentry_unit (sentry): The sentry unit to check for the service on
1153+ mtime (float): The epoch time to check against
1154+ service (string): service name to look for in process table
1155+ filename (string): The file to check mtime of
1156+ pgrep_full (boolean): Use full command line search mode with pgrep
1157+ sleep_time (int): Seconds to sleep before looking for process
1158+ retry_count (int): If service is not found, how many times to retry
1159+
1160+ Typical Usage:
1161+ u = OpenStackAmuletUtils(ERROR)
1162+ ...
1163+ mtime = u.get_sentry_time(self.cinder_sentry)
1164+ self.d.configure('cinder', {'verbose': 'True', 'debug': 'True'})
1165+ if not u.validate_service_config_changed(self.cinder_sentry,
1166+ mtime,
1167+ 'cinder-api',
1168+ '/etc/cinder/cinder.conf')
1169+ amulet.raise_status(amulet.FAIL, msg='update failed')
1170+ Returns:
1171+ bool: True if both service and file where updated/restarted after
1172+ mtime, False if service is older than mtime or if service was
1173+ not found or if filename was modified before mtime.
1174+ """
1175+ self.log.debug('Checking %s restarted since %s' % (service, mtime))
1176+ time.sleep(sleep_time)
1177+ service_restart = self.service_restarted_since(sentry_unit, mtime,
1178+ service,
1179+ pgrep_full=pgrep_full,
1180+ sleep_time=0,
1181+ retry_count=retry_count)
1182+ config_update = self.config_updated_since(sentry_unit, filename, mtime,
1183+ sleep_time=0)
1184+ return service_restart and config_update
1185+
1186+ def get_sentry_time(self, sentry_unit):
1187+ """Return current epoch time on a sentry"""
1188+ cmd = "date +'%s'"
1189+ return float(sentry_unit.run(cmd)[0])
1190+
1191 def relation_error(self, name, data):
1192 return 'unexpected relation data in {} - {}'.format(name, data)
1193
1194
1195=== modified file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py'
1196--- tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-01-26 09:45:43 +0000
1197+++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-02-19 20:31:59 +0000
1198@@ -71,16 +71,19 @@
1199 services.append(this_service)
1200 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
1201 'ceph-osd', 'ceph-radosgw']
1202+ # Openstack subordinate charms do not expose an origin option as that
1203+ # is controlled by the principle
1204+ ignore = ['neutron-openvswitch']
1205
1206 if self.openstack:
1207 for svc in services:
1208- if svc['name'] not in use_source:
1209+ if svc['name'] not in use_source + ignore:
1210 config = {'openstack-origin': self.openstack}
1211 self.d.configure(svc['name'], config)
1212
1213 if self.source:
1214 for svc in services:
1215- if svc['name'] in use_source:
1216+ if svc['name'] in use_source and svc['name'] not in ignore:
1217 config = {'source': self.source}
1218 self.d.configure(svc['name'], config)
1219

Subscribers

People subscribed via source and target branches