Merge lp:~1chb1n/charms/trusty/ceph-osd/next-ch-sync-mitaka into lp:~openstack-charmers-archive/charms/trusty/ceph-osd/next

Proposed by Ryan Beisner
Status: Merged
Merged at revision: 59
Proposed branch: lp:~1chb1n/charms/trusty/ceph-osd/next-ch-sync-mitaka
Merge into: lp:~openstack-charmers-archive/charms/trusty/ceph-osd/next
Diff against target: 2384 lines (+1314/-241)
18 files modified
hooks/charmhelpers/cli/__init__.py (+3/-3)
hooks/charmhelpers/contrib/charmsupport/nrpe.py (+52/-14)
hooks/charmhelpers/contrib/network/ip.py (+26/-22)
hooks/charmhelpers/core/hookenv.py (+84/-4)
hooks/charmhelpers/core/host.py (+153/-50)
hooks/charmhelpers/core/hugepage.py (+10/-1)
hooks/charmhelpers/core/kernel.py (+68/-0)
hooks/charmhelpers/core/services/helpers.py (+14/-5)
hooks/charmhelpers/core/strutils.py (+30/-0)
hooks/charmhelpers/core/templating.py (+21/-8)
hooks/charmhelpers/fetch/__init__.py (+10/-2)
hooks/charmhelpers/fetch/archiveurl.py (+1/-1)
hooks/charmhelpers/fetch/bzrurl.py (+22/-32)
hooks/charmhelpers/fetch/giturl.py (+20/-23)
tests/charmhelpers/contrib/amulet/deployment.py (+4/-2)
tests/charmhelpers/contrib/amulet/utils.py (+284/-62)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+131/-12)
tests/charmhelpers/contrib/openstack/amulet/utils.py (+381/-0)
To merge this branch: bzr merge lp:~1chb1n/charms/trusty/ceph-osd/next-ch-sync-mitaka
Reviewer Review Type Date Requested Status
Corey Bryant (community) Approve
Review via email: mp+283704@code.launchpad.net

Commit message

sync charm helpers for mitaka cloud archive recognition

Description of the change

sync charm helpers for mitaka cloud archive recognition

To post a comment you must log in.
60. By Ryan Beisner

enable liberty amulet test targets

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

charm_unit_test #16782 ceph-osd-next for 1chb1n mp283704
    UNIT OK: passed

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

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

charm_lint_check #17960 ceph-osd-next for 1chb1n mp283704
    LINT OK: passed

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

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

charm_amulet_test #8982 ceph-osd-next for 1chb1n mp283704
    AMULET OK: passed

Build: http://10.245.162.77:8080/job/charm_amulet_test/8982/

Revision history for this message
Corey Bryant (corey.bryant) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'hooks/charmhelpers/cli/__init__.py'
2--- hooks/charmhelpers/cli/__init__.py 2015-08-19 13:50:42 +0000
3+++ hooks/charmhelpers/cli/__init__.py 2016-01-22 22:32:27 +0000
4@@ -20,7 +20,7 @@
5
6 from six.moves import zip
7
8-from charmhelpers.core import unitdata
9+import charmhelpers.core.unitdata
10
11
12 class OutputFormatter(object):
13@@ -163,8 +163,8 @@
14 if getattr(arguments.func, '_cli_no_output', False):
15 output = ''
16 self.formatter.format_output(output, arguments.format)
17- if unitdata._KV:
18- unitdata._KV.flush()
19+ if charmhelpers.core.unitdata._KV:
20+ charmhelpers.core.unitdata._KV.flush()
21
22
23 cmdline = CommandLine()
24
25=== modified file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py'
26--- hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-06-17 16:59:15 +0000
27+++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 2016-01-22 22:32:27 +0000
28@@ -148,6 +148,13 @@
29 self.description = description
30 self.check_cmd = self._locate_cmd(check_cmd)
31
32+ def _get_check_filename(self):
33+ return os.path.join(NRPE.nrpe_confdir, '{}.cfg'.format(self.command))
34+
35+ def _get_service_filename(self, hostname):
36+ return os.path.join(NRPE.nagios_exportdir,
37+ 'service__{}_{}.cfg'.format(hostname, self.command))
38+
39 def _locate_cmd(self, check_cmd):
40 search_path = (
41 '/usr/lib/nagios/plugins',
42@@ -163,9 +170,21 @@
43 log('Check command not found: {}'.format(parts[0]))
44 return ''
45
46+ def _remove_service_files(self):
47+ if not os.path.exists(NRPE.nagios_exportdir):
48+ return
49+ for f in os.listdir(NRPE.nagios_exportdir):
50+ if f.endswith('_{}.cfg'.format(self.command)):
51+ os.remove(os.path.join(NRPE.nagios_exportdir, f))
52+
53+ def remove(self, hostname):
54+ nrpe_check_file = self._get_check_filename()
55+ if os.path.exists(nrpe_check_file):
56+ os.remove(nrpe_check_file)
57+ self._remove_service_files()
58+
59 def write(self, nagios_context, hostname, nagios_servicegroups):
60- nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format(
61- self.command)
62+ nrpe_check_file = self._get_check_filename()
63 with open(nrpe_check_file, 'w') as nrpe_check_config:
64 nrpe_check_config.write("# check {}\n".format(self.shortname))
65 nrpe_check_config.write("command[{}]={}\n".format(
66@@ -180,9 +199,7 @@
67
68 def write_service_config(self, nagios_context, hostname,
69 nagios_servicegroups):
70- for f in os.listdir(NRPE.nagios_exportdir):
71- if re.search('.*{}.cfg'.format(self.command), f):
72- os.remove(os.path.join(NRPE.nagios_exportdir, f))
73+ self._remove_service_files()
74
75 templ_vars = {
76 'nagios_hostname': hostname,
77@@ -192,8 +209,7 @@
78 'command': self.command,
79 }
80 nrpe_service_text = Check.service_template.format(**templ_vars)
81- nrpe_service_file = '{}/service__{}_{}.cfg'.format(
82- NRPE.nagios_exportdir, hostname, self.command)
83+ nrpe_service_file = self._get_service_filename(hostname)
84 with open(nrpe_service_file, 'w') as nrpe_service_config:
85 nrpe_service_config.write(str(nrpe_service_text))
86
87@@ -218,12 +234,32 @@
88 if hostname:
89 self.hostname = hostname
90 else:
91- self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
92+ nagios_hostname = get_nagios_hostname()
93+ if nagios_hostname:
94+ self.hostname = nagios_hostname
95+ else:
96+ self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
97 self.checks = []
98
99 def add_check(self, *args, **kwargs):
100 self.checks.append(Check(*args, **kwargs))
101
102+ def remove_check(self, *args, **kwargs):
103+ if kwargs.get('shortname') is None:
104+ raise ValueError('shortname of check must be specified')
105+
106+ # Use sensible defaults if they're not specified - these are not
107+ # actually used during removal, but they're required for constructing
108+ # the Check object; check_disk is chosen because it's part of the
109+ # nagios-plugins-basic package.
110+ if kwargs.get('check_cmd') is None:
111+ kwargs['check_cmd'] = 'check_disk'
112+ if kwargs.get('description') is None:
113+ kwargs['description'] = ''
114+
115+ check = Check(*args, **kwargs)
116+ check.remove(self.hostname)
117+
118 def write(self):
119 try:
120 nagios_uid = pwd.getpwnam('nagios').pw_uid
121@@ -260,7 +296,7 @@
122 :param str relation_name: Name of relation nrpe sub joined to
123 """
124 for rel in relations_of_type(relation_name):
125- if 'nagios_hostname' in rel:
126+ if 'nagios_host_context' in rel:
127 return rel['nagios_host_context']
128
129
130@@ -301,11 +337,13 @@
131 upstart_init = '/etc/init/%s.conf' % svc
132 sysv_init = '/etc/init.d/%s' % svc
133 if os.path.exists(upstart_init):
134- nrpe.add_check(
135- shortname=svc,
136- description='process check {%s}' % unit_name,
137- check_cmd='check_upstart_job %s' % svc
138- )
139+ # Don't add a check for these services from neutron-gateway
140+ if svc not in ['ext-port', 'os-charm-phy-nic-mtu']:
141+ nrpe.add_check(
142+ shortname=svc,
143+ description='process check {%s}' % unit_name,
144+ check_cmd='check_upstart_job %s' % svc
145+ )
146 elif os.path.exists(sysv_init):
147 cronpath = '/etc/cron.d/nagios-service-check-%s' % svc
148 cron_file = ('*/5 * * * * root '
149
150=== modified file 'hooks/charmhelpers/contrib/network/ip.py'
151--- hooks/charmhelpers/contrib/network/ip.py 2015-09-03 09:42:18 +0000
152+++ hooks/charmhelpers/contrib/network/ip.py 2016-01-22 22:32:27 +0000
153@@ -23,7 +23,7 @@
154 from functools import partial
155
156 from charmhelpers.core.hookenv import unit_get
157-from charmhelpers.fetch import apt_install
158+from charmhelpers.fetch import apt_install, apt_update
159 from charmhelpers.core.hookenv import (
160 log,
161 WARNING,
162@@ -32,13 +32,15 @@
163 try:
164 import netifaces
165 except ImportError:
166- apt_install('python-netifaces')
167+ apt_update(fatal=True)
168+ apt_install('python-netifaces', fatal=True)
169 import netifaces
170
171 try:
172 import netaddr
173 except ImportError:
174- apt_install('python-netaddr')
175+ apt_update(fatal=True)
176+ apt_install('python-netaddr', fatal=True)
177 import netaddr
178
179
180@@ -51,7 +53,7 @@
181
182
183 def no_ip_found_error_out(network):
184- errmsg = ("No IP address found in network: %s" % network)
185+ errmsg = ("No IP address found in network(s): %s" % network)
186 raise ValueError(errmsg)
187
188
189@@ -59,7 +61,7 @@
190 """Get an IPv4 or IPv6 address within the network from the host.
191
192 :param network (str): CIDR presentation format. For example,
193- '192.168.1.0/24'.
194+ '192.168.1.0/24'. Supports multiple networks as a space-delimited list.
195 :param fallback (str): If no address is found, return fallback.
196 :param fatal (boolean): If no address is found, fallback is not
197 set and fatal is True then exit(1).
198@@ -73,24 +75,26 @@
199 else:
200 return None
201
202- _validate_cidr(network)
203- network = netaddr.IPNetwork(network)
204- for iface in netifaces.interfaces():
205- addresses = netifaces.ifaddresses(iface)
206- if network.version == 4 and netifaces.AF_INET in addresses:
207- addr = addresses[netifaces.AF_INET][0]['addr']
208- netmask = addresses[netifaces.AF_INET][0]['netmask']
209- cidr = netaddr.IPNetwork("%s/%s" % (addr, netmask))
210- if cidr in network:
211- return str(cidr.ip)
212+ networks = network.split() or [network]
213+ for network in networks:
214+ _validate_cidr(network)
215+ network = netaddr.IPNetwork(network)
216+ for iface in netifaces.interfaces():
217+ addresses = netifaces.ifaddresses(iface)
218+ if network.version == 4 and netifaces.AF_INET in addresses:
219+ addr = addresses[netifaces.AF_INET][0]['addr']
220+ netmask = addresses[netifaces.AF_INET][0]['netmask']
221+ cidr = netaddr.IPNetwork("%s/%s" % (addr, netmask))
222+ if cidr in network:
223+ return str(cidr.ip)
224
225- if network.version == 6 and netifaces.AF_INET6 in addresses:
226- for addr in addresses[netifaces.AF_INET6]:
227- if not addr['addr'].startswith('fe80'):
228- cidr = netaddr.IPNetwork("%s/%s" % (addr['addr'],
229- addr['netmask']))
230- if cidr in network:
231- return str(cidr.ip)
232+ if network.version == 6 and netifaces.AF_INET6 in addresses:
233+ for addr in addresses[netifaces.AF_INET6]:
234+ if not addr['addr'].startswith('fe80'):
235+ cidr = netaddr.IPNetwork("%s/%s" % (addr['addr'],
236+ addr['netmask']))
237+ if cidr in network:
238+ return str(cidr.ip)
239
240 if fallback is not None:
241 return fallback
242
243=== modified file 'hooks/charmhelpers/core/hookenv.py'
244--- hooks/charmhelpers/core/hookenv.py 2015-09-03 09:42:18 +0000
245+++ hooks/charmhelpers/core/hookenv.py 2016-01-22 22:32:27 +0000
246@@ -491,6 +491,19 @@
247
248
249 @cached
250+def peer_relation_id():
251+ '''Get the peers relation id if a peers relation has been joined, else None.'''
252+ md = metadata()
253+ section = md.get('peers')
254+ if section:
255+ for key in section:
256+ relids = relation_ids(key)
257+ if relids:
258+ return relids[0]
259+ return None
260+
261+
262+@cached
263 def relation_to_interface(relation_name):
264 """
265 Given the name of a relation, return the interface that relation uses.
266@@ -504,12 +517,12 @@
267 def relation_to_role_and_interface(relation_name):
268 """
269 Given the name of a relation, return the role and the name of the interface
270- that relation uses (where role is one of ``provides``, ``requires``, or ``peer``).
271+ that relation uses (where role is one of ``provides``, ``requires``, or ``peers``).
272
273 :returns: A tuple containing ``(role, interface)``, or ``(None, None)``.
274 """
275 _metadata = metadata()
276- for role in ('provides', 'requires', 'peer'):
277+ for role in ('provides', 'requires', 'peers'):
278 interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')
279 if interface:
280 return role, interface
281@@ -521,7 +534,7 @@
282 """
283 Given a role and interface name, return a list of relation names for the
284 current charm that use that interface under that role (where role is one
285- of ``provides``, ``requires``, or ``peer``).
286+ of ``provides``, ``requires``, or ``peers``).
287
288 :returns: A list of relation names.
289 """
290@@ -542,7 +555,7 @@
291 :returns: A list of relation names.
292 """
293 results = []
294- for role in ('provides', 'requires', 'peer'):
295+ for role in ('provides', 'requires', 'peers'):
296 results.extend(role_and_interface_to_relations(role, interface_name))
297 return results
298
299@@ -623,6 +636,38 @@
300 return unit_get('private-address')
301
302
303+@cached
304+def storage_get(attribute=None, storage_id=None):
305+ """Get storage attributes"""
306+ _args = ['storage-get', '--format=json']
307+ if storage_id:
308+ _args.extend(('-s', storage_id))
309+ if attribute:
310+ _args.append(attribute)
311+ try:
312+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
313+ except ValueError:
314+ return None
315+
316+
317+@cached
318+def storage_list(storage_name=None):
319+ """List the storage IDs for the unit"""
320+ _args = ['storage-list', '--format=json']
321+ if storage_name:
322+ _args.append(storage_name)
323+ try:
324+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
325+ except ValueError:
326+ return None
327+ except OSError as e:
328+ import errno
329+ if e.errno == errno.ENOENT:
330+ # storage-list does not exist
331+ return []
332+ raise
333+
334+
335 class UnregisteredHookError(Exception):
336 """Raised when an undefined hook is called"""
337 pass
338@@ -788,6 +833,7 @@
339
340 def translate_exc(from_exc, to_exc):
341 def inner_translate_exc1(f):
342+ @wraps(f)
343 def inner_translate_exc2(*args, **kwargs):
344 try:
345 return f(*args, **kwargs)
346@@ -832,6 +878,40 @@
347 subprocess.check_call(cmd)
348
349
350+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
351+def payload_register(ptype, klass, pid):
352+ """ is used while a hook is running to let Juju know that a
353+ payload has been started."""
354+ cmd = ['payload-register']
355+ for x in [ptype, klass, pid]:
356+ cmd.append(x)
357+ subprocess.check_call(cmd)
358+
359+
360+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
361+def payload_unregister(klass, pid):
362+ """ is used while a hook is running to let Juju know
363+ that a payload has been manually stopped. The <class> and <id> provided
364+ must match a payload that has been previously registered with juju using
365+ payload-register."""
366+ cmd = ['payload-unregister']
367+ for x in [klass, pid]:
368+ cmd.append(x)
369+ subprocess.check_call(cmd)
370+
371+
372+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
373+def payload_status_set(klass, pid, status):
374+ """is used to update the current status of a registered payload.
375+ The <class> and <id> provided must match a payload that has been previously
376+ registered with juju using payload-register. The <status> must be one of the
377+ follow: starting, started, stopping, stopped"""
378+ cmd = ['payload-status-set']
379+ for x in [klass, pid, status]:
380+ cmd.append(x)
381+ subprocess.check_call(cmd)
382+
383+
384 @cached
385 def juju_version():
386 """Full version string (eg. '1.23.3.1-trusty-amd64')"""
387
388=== modified file 'hooks/charmhelpers/core/host.py'
389--- hooks/charmhelpers/core/host.py 2015-08-19 13:50:42 +0000
390+++ hooks/charmhelpers/core/host.py 2016-01-22 22:32:27 +0000
391@@ -63,55 +63,86 @@
392 return service_result
393
394
395-def service_pause(service_name, init_dir=None):
396+def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):
397 """Pause a system service.
398
399 Stop it, and prevent it from starting again at boot."""
400- if init_dir is None:
401- init_dir = "/etc/init"
402- stopped = service_stop(service_name)
403- # XXX: Support systemd too
404- override_path = os.path.join(
405- init_dir, '{}.override'.format(service_name))
406- with open(override_path, 'w') as fh:
407- fh.write("manual\n")
408+ stopped = True
409+ if service_running(service_name):
410+ stopped = service_stop(service_name)
411+ upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
412+ sysv_file = os.path.join(initd_dir, service_name)
413+ if init_is_systemd():
414+ service('disable', service_name)
415+ elif os.path.exists(upstart_file):
416+ override_path = os.path.join(
417+ init_dir, '{}.override'.format(service_name))
418+ with open(override_path, 'w') as fh:
419+ fh.write("manual\n")
420+ elif os.path.exists(sysv_file):
421+ subprocess.check_call(["update-rc.d", service_name, "disable"])
422+ else:
423+ raise ValueError(
424+ "Unable to detect {0} as SystemD, Upstart {1} or"
425+ " SysV {2}".format(
426+ service_name, upstart_file, sysv_file))
427 return stopped
428
429
430-def service_resume(service_name, init_dir=None):
431+def service_resume(service_name, init_dir="/etc/init",
432+ initd_dir="/etc/init.d"):
433 """Resume a system service.
434
435 Reenable starting again at boot. Start the service"""
436- # XXX: Support systemd too
437- if init_dir is None:
438- init_dir = "/etc/init"
439- override_path = os.path.join(
440- init_dir, '{}.override'.format(service_name))
441- if os.path.exists(override_path):
442- os.unlink(override_path)
443- started = service_start(service_name)
444+ upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
445+ sysv_file = os.path.join(initd_dir, service_name)
446+ if init_is_systemd():
447+ service('enable', service_name)
448+ elif os.path.exists(upstart_file):
449+ override_path = os.path.join(
450+ init_dir, '{}.override'.format(service_name))
451+ if os.path.exists(override_path):
452+ os.unlink(override_path)
453+ elif os.path.exists(sysv_file):
454+ subprocess.check_call(["update-rc.d", service_name, "enable"])
455+ else:
456+ raise ValueError(
457+ "Unable to detect {0} as SystemD, Upstart {1} or"
458+ " SysV {2}".format(
459+ service_name, upstart_file, sysv_file))
460+
461+ started = service_running(service_name)
462+ if not started:
463+ started = service_start(service_name)
464 return started
465
466
467 def service(action, service_name):
468 """Control a system service"""
469- cmd = ['service', service_name, action]
470+ if init_is_systemd():
471+ cmd = ['systemctl', action, service_name]
472+ else:
473+ cmd = ['service', service_name, action]
474 return subprocess.call(cmd) == 0
475
476
477-def service_running(service):
478+def service_running(service_name):
479 """Determine whether a system service is running"""
480- try:
481- output = subprocess.check_output(
482- ['service', service, 'status'],
483- stderr=subprocess.STDOUT).decode('UTF-8')
484- except subprocess.CalledProcessError:
485- return False
486+ if init_is_systemd():
487+ return service('is-active', service_name)
488 else:
489- if ("start/running" in output or "is running" in output):
490- return True
491- else:
492+ try:
493+ output = subprocess.check_output(
494+ ['service', service_name, 'status'],
495+ stderr=subprocess.STDOUT).decode('UTF-8')
496+ except subprocess.CalledProcessError:
497 return False
498+ else:
499+ if ("start/running" in output or "is running" in output or
500+ "up and running" in output):
501+ return True
502+ else:
503+ return False
504
505
506 def service_available(service_name):
507@@ -126,8 +157,29 @@
508 return True
509
510
511-def adduser(username, password=None, shell='/bin/bash', system_user=False):
512- """Add a user to the system"""
513+SYSTEMD_SYSTEM = '/run/systemd/system'
514+
515+
516+def init_is_systemd():
517+ """Return True if the host system uses systemd, False otherwise."""
518+ return os.path.isdir(SYSTEMD_SYSTEM)
519+
520+
521+def adduser(username, password=None, shell='/bin/bash', system_user=False,
522+ primary_group=None, secondary_groups=None):
523+ """Add a user to the system.
524+
525+ Will log but otherwise succeed if the user already exists.
526+
527+ :param str username: Username to create
528+ :param str password: Password for user; if ``None``, create a system user
529+ :param str shell: The default shell for the user
530+ :param bool system_user: Whether to create a login or system user
531+ :param str primary_group: Primary group for user; defaults to username
532+ :param list secondary_groups: Optional list of additional groups
533+
534+ :returns: The password database entry struct, as returned by `pwd.getpwnam`
535+ """
536 try:
537 user_info = pwd.getpwnam(username)
538 log('user {0} already exists!'.format(username))
539@@ -142,6 +194,16 @@
540 '--shell', shell,
541 '--password', password,
542 ])
543+ if not primary_group:
544+ try:
545+ grp.getgrnam(username)
546+ primary_group = username # avoid "group exists" error
547+ except KeyError:
548+ pass
549+ if primary_group:
550+ cmd.extend(['-g', primary_group])
551+ if secondary_groups:
552+ cmd.extend(['-G', ','.join(secondary_groups)])
553 cmd.append(username)
554 subprocess.check_call(cmd)
555 user_info = pwd.getpwnam(username)
556@@ -239,14 +301,12 @@
557
558
559 def fstab_remove(mp):
560- """Remove the given mountpoint entry from /etc/fstab
561- """
562+ """Remove the given mountpoint entry from /etc/fstab"""
563 return Fstab.remove_by_mountpoint(mp)
564
565
566 def fstab_add(dev, mp, fs, options=None):
567- """Adds the given device entry to the /etc/fstab file
568- """
569+ """Adds the given device entry to the /etc/fstab file"""
570 return Fstab.add(dev, mp, fs, options=options)
571
572
573@@ -302,8 +362,7 @@
574
575
576 def file_hash(path, hash_type='md5'):
577- """
578- Generate a hash checksum of the contents of 'path' or None if not found.
579+ """Generate a hash checksum of the contents of 'path' or None if not found.
580
581 :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
582 such as md5, sha1, sha256, sha512, etc.
583@@ -318,10 +377,9 @@
584
585
586 def path_hash(path):
587- """
588- Generate a hash checksum of all files matching 'path'. Standard wildcards
589- like '*' and '?' are supported, see documentation for the 'glob' module for
590- more information.
591+ """Generate a hash checksum of all files matching 'path'. Standard
592+ wildcards like '*' and '?' are supported, see documentation for the 'glob'
593+ module for more information.
594
595 :return: dict: A { filename: hash } dictionary for all matched files.
596 Empty if none found.
597@@ -333,8 +391,7 @@
598
599
600 def check_hash(path, checksum, hash_type='md5'):
601- """
602- Validate a file using a cryptographic checksum.
603+ """Validate a file using a cryptographic checksum.
604
605 :param str checksum: Value of the checksum used to validate the file.
606 :param str hash_type: Hash algorithm used to generate `checksum`.
607@@ -349,6 +406,7 @@
608
609
610 class ChecksumError(ValueError):
611+ """A class derived from Value error to indicate the checksum failed."""
612 pass
613
614
615@@ -454,7 +512,7 @@
616
617
618 def list_nics(nic_type=None):
619- '''Return a list of nics of given type(s)'''
620+ """Return a list of nics of given type(s)"""
621 if isinstance(nic_type, six.string_types):
622 int_types = [nic_type]
623 else:
624@@ -496,12 +554,13 @@
625
626
627 def set_nic_mtu(nic, mtu):
628- '''Set MTU on a network interface'''
629+ """Set the Maximum Transmission Unit (MTU) on a network interface."""
630 cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
631 subprocess.check_call(cmd)
632
633
634 def get_nic_mtu(nic):
635+ """Return the Maximum Transmission Unit (MTU) for a network interface."""
636 cmd = ['ip', 'addr', 'show', nic]
637 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
638 mtu = ""
639@@ -513,6 +572,7 @@
640
641
642 def get_nic_hwaddr(nic):
643+ """Return the Media Access Control (MAC) for a network interface."""
644 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
645 ip_output = subprocess.check_output(cmd).decode('UTF-8')
646 hwaddr = ""
647@@ -523,7 +583,7 @@
648
649
650 def cmp_pkgrevno(package, revno, pkgcache=None):
651- '''Compare supplied revno with the revno of the installed package
652+ """Compare supplied revno with the revno of the installed package
653
654 * 1 => Installed revno is greater than supplied arg
655 * 0 => Installed revno is the same as supplied arg
656@@ -532,7 +592,7 @@
657 This function imports apt_cache function from charmhelpers.fetch if
658 the pkgcache argument is None. Be sure to add charmhelpers.fetch if
659 you call this function, or pass an apt_pkg.Cache() instance.
660- '''
661+ """
662 import apt_pkg
663 if not pkgcache:
664 from charmhelpers.fetch import apt_cache
665@@ -542,15 +602,30 @@
666
667
668 @contextmanager
669-def chdir(d):
670+def chdir(directory):
671+ """Change the current working directory to a different directory for a code
672+ block and return the previous directory after the block exits. Useful to
673+ run commands from a specificed directory.
674+
675+ :param str directory: The directory path to change to for this context.
676+ """
677 cur = os.getcwd()
678 try:
679- yield os.chdir(d)
680+ yield os.chdir(directory)
681 finally:
682 os.chdir(cur)
683
684
685-def chownr(path, owner, group, follow_links=True):
686+def chownr(path, owner, group, follow_links=True, chowntopdir=False):
687+ """Recursively change user and group ownership of files and directories
688+ in given path. Doesn't chown path itself by default, only its children.
689+
690+ :param str path: The string path to start changing ownership.
691+ :param str owner: The owner string to use when looking up the uid.
692+ :param str group: The group string to use when looking up the gid.
693+ :param bool follow_links: Also Chown links if True
694+ :param bool chowntopdir: Also chown path itself if True
695+ """
696 uid = pwd.getpwnam(owner).pw_uid
697 gid = grp.getgrnam(group).gr_gid
698 if follow_links:
699@@ -558,6 +633,10 @@
700 else:
701 chown = os.lchown
702
703+ if chowntopdir:
704+ broken_symlink = os.path.lexists(path) and not os.path.exists(path)
705+ if not broken_symlink:
706+ chown(path, uid, gid)
707 for root, dirs, files in os.walk(path):
708 for name in dirs + files:
709 full = os.path.join(root, name)
710@@ -567,4 +646,28 @@
711
712
713 def lchownr(path, owner, group):
714+ """Recursively change user and group ownership of files and directories
715+ in a given path, not following symbolic links. See the documentation for
716+ 'os.lchown' for more information.
717+
718+ :param str path: The string path to start changing ownership.
719+ :param str owner: The owner string to use when looking up the uid.
720+ :param str group: The group string to use when looking up the gid.
721+ """
722 chownr(path, owner, group, follow_links=False)
723+
724+
725+def get_total_ram():
726+ """The total amount of system RAM in bytes.
727+
728+ This is what is reported by the OS, and may be overcommitted when
729+ there are multiple containers hosted on the same machine.
730+ """
731+ with open('/proc/meminfo', 'r') as f:
732+ for line in f.readlines():
733+ if line:
734+ key, value, unit = line.split()
735+ if key == 'MemTotal:':
736+ assert unit == 'kB', 'Unknown unit'
737+ return int(value) * 1024 # Classic, not KiB.
738+ raise NotImplementedError()
739
740=== modified file 'hooks/charmhelpers/core/hugepage.py'
741--- hooks/charmhelpers/core/hugepage.py 2015-08-19 13:50:42 +0000
742+++ hooks/charmhelpers/core/hugepage.py 2016-01-22 22:32:27 +0000
743@@ -25,11 +25,13 @@
744 fstab_mount,
745 mkdir,
746 )
747+from charmhelpers.core.strutils import bytes_from_string
748+from subprocess import check_output
749
750
751 def hugepage_support(user, group='hugetlb', nr_hugepages=256,
752 max_map_count=65536, mnt_point='/run/hugepages/kvm',
753- pagesize='2MB', mount=True):
754+ pagesize='2MB', mount=True, set_shmmax=False):
755 """Enable hugepages on system.
756
757 Args:
758@@ -44,11 +46,18 @@
759 group_info = add_group(group)
760 gid = group_info.gr_gid
761 add_user_to_group(user, group)
762+ if max_map_count < 2 * nr_hugepages:
763+ max_map_count = 2 * nr_hugepages
764 sysctl_settings = {
765 'vm.nr_hugepages': nr_hugepages,
766 'vm.max_map_count': max_map_count,
767 'vm.hugetlb_shm_group': gid,
768 }
769+ if set_shmmax:
770+ shmmax_current = int(check_output(['sysctl', '-n', 'kernel.shmmax']))
771+ shmmax_minsize = bytes_from_string(pagesize) * nr_hugepages
772+ if shmmax_minsize > shmmax_current:
773+ sysctl_settings['kernel.shmmax'] = shmmax_minsize
774 sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf')
775 mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False)
776 lfstab = fstab.Fstab()
777
778=== added file 'hooks/charmhelpers/core/kernel.py'
779--- hooks/charmhelpers/core/kernel.py 1970-01-01 00:00:00 +0000
780+++ hooks/charmhelpers/core/kernel.py 2016-01-22 22:32:27 +0000
781@@ -0,0 +1,68 @@
782+#!/usr/bin/env python
783+# -*- coding: utf-8 -*-
784+
785+# Copyright 2014-2015 Canonical Limited.
786+#
787+# This file is part of charm-helpers.
788+#
789+# charm-helpers is free software: you can redistribute it and/or modify
790+# it under the terms of the GNU Lesser General Public License version 3 as
791+# published by the Free Software Foundation.
792+#
793+# charm-helpers is distributed in the hope that it will be useful,
794+# but WITHOUT ANY WARRANTY; without even the implied warranty of
795+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
796+# GNU Lesser General Public License for more details.
797+#
798+# You should have received a copy of the GNU Lesser General Public License
799+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
800+
801+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
802+
803+from charmhelpers.core.hookenv import (
804+ log,
805+ INFO
806+)
807+
808+from subprocess import check_call, check_output
809+import re
810+
811+
812+def modprobe(module, persist=True):
813+ """Load a kernel module and configure for auto-load on reboot."""
814+ cmd = ['modprobe', module]
815+
816+ log('Loading kernel module %s' % module, level=INFO)
817+
818+ check_call(cmd)
819+ if persist:
820+ with open('/etc/modules', 'r+') as modules:
821+ if module not in modules.read():
822+ modules.write(module)
823+
824+
825+def rmmod(module, force=False):
826+ """Remove a module from the linux kernel"""
827+ cmd = ['rmmod']
828+ if force:
829+ cmd.append('-f')
830+ cmd.append(module)
831+ log('Removing kernel module %s' % module, level=INFO)
832+ return check_call(cmd)
833+
834+
835+def lsmod():
836+ """Shows what kernel modules are currently loaded"""
837+ return check_output(['lsmod'],
838+ universal_newlines=True)
839+
840+
841+def is_module_loaded(module):
842+ """Checks if a kernel module is already loaded"""
843+ matches = re.findall('^%s[ ]+' % module, lsmod(), re.M)
844+ return len(matches) > 0
845+
846+
847+def update_initramfs(version='all'):
848+ """Updates an initramfs image"""
849+ return check_call(["update-initramfs", "-k", version, "-u"])
850
851=== modified file 'hooks/charmhelpers/core/services/helpers.py'
852--- hooks/charmhelpers/core/services/helpers.py 2015-08-19 13:50:42 +0000
853+++ hooks/charmhelpers/core/services/helpers.py 2016-01-22 22:32:27 +0000
854@@ -243,33 +243,40 @@
855 :param str source: The template source file, relative to
856 `$CHARM_DIR/templates`
857
858- :param str target: The target to write the rendered template to
859+ :param str target: The target to write the rendered template to (or None)
860 :param str owner: The owner of the rendered file
861 :param str group: The group of the rendered file
862 :param int perms: The permissions of the rendered file
863 :param partial on_change_action: functools partial to be executed when
864 rendered file changes
865+ :param jinja2 loader template_loader: A jinja2 template loader
866+
867+ :return str: The rendered template
868 """
869 def __init__(self, source, target,
870 owner='root', group='root', perms=0o444,
871- on_change_action=None):
872+ on_change_action=None, template_loader=None):
873 self.source = source
874 self.target = target
875 self.owner = owner
876 self.group = group
877 self.perms = perms
878 self.on_change_action = on_change_action
879+ self.template_loader = template_loader
880
881 def __call__(self, manager, service_name, event_name):
882 pre_checksum = ''
883 if self.on_change_action and os.path.isfile(self.target):
884 pre_checksum = host.file_hash(self.target)
885 service = manager.get_service(service_name)
886- context = {}
887+ context = {'ctx': {}}
888 for ctx in service.get('required_data', []):
889 context.update(ctx)
890- templating.render(self.source, self.target, context,
891- self.owner, self.group, self.perms)
892+ context['ctx'].update(ctx)
893+
894+ result = templating.render(self.source, self.target, context,
895+ self.owner, self.group, self.perms,
896+ template_loader=self.template_loader)
897 if self.on_change_action:
898 if pre_checksum == host.file_hash(self.target):
899 hookenv.log(
900@@ -278,6 +285,8 @@
901 else:
902 self.on_change_action()
903
904+ return result
905+
906
907 # Convenience aliases for templates
908 render_template = template = TemplateCallback
909
910=== modified file 'hooks/charmhelpers/core/strutils.py'
911--- hooks/charmhelpers/core/strutils.py 2015-04-16 21:32:48 +0000
912+++ hooks/charmhelpers/core/strutils.py 2016-01-22 22:32:27 +0000
913@@ -18,6 +18,7 @@
914 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
915
916 import six
917+import re
918
919
920 def bool_from_string(value):
921@@ -40,3 +41,32 @@
922
923 msg = "Unable to interpret string value '%s' as boolean" % (value)
924 raise ValueError(msg)
925+
926+
927+def bytes_from_string(value):
928+ """Interpret human readable string value as bytes.
929+
930+ Returns int
931+ """
932+ BYTE_POWER = {
933+ 'K': 1,
934+ 'KB': 1,
935+ 'M': 2,
936+ 'MB': 2,
937+ 'G': 3,
938+ 'GB': 3,
939+ 'T': 4,
940+ 'TB': 4,
941+ 'P': 5,
942+ 'PB': 5,
943+ }
944+ if isinstance(value, six.string_types):
945+ value = six.text_type(value)
946+ else:
947+ msg = "Unable to interpret non-string value '%s' as boolean" % (value)
948+ raise ValueError(msg)
949+ matches = re.match("([0-9]+)([a-zA-Z]+)", value)
950+ if not matches:
951+ msg = "Unable to interpret string value '%s' as bytes" % (value)
952+ raise ValueError(msg)
953+ return int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)])
954
955=== modified file 'hooks/charmhelpers/core/templating.py'
956--- hooks/charmhelpers/core/templating.py 2015-02-26 13:37:18 +0000
957+++ hooks/charmhelpers/core/templating.py 2016-01-22 22:32:27 +0000
958@@ -21,13 +21,14 @@
959
960
961 def render(source, target, context, owner='root', group='root',
962- perms=0o444, templates_dir=None, encoding='UTF-8'):
963+ perms=0o444, templates_dir=None, encoding='UTF-8', template_loader=None):
964 """
965 Render a template.
966
967 The `source` path, if not absolute, is relative to the `templates_dir`.
968
969- The `target` path should be absolute.
970+ The `target` path should be absolute. It can also be `None`, in which
971+ case no file will be written.
972
973 The context should be a dict containing the values to be replaced in the
974 template.
975@@ -36,6 +37,9 @@
976
977 If omitted, `templates_dir` defaults to the `templates` folder in the charm.
978
979+ The rendered template will be written to the file as well as being returned
980+ as a string.
981+
982 Note: Using this requires python-jinja2; if it is not installed, calling
983 this will attempt to use charmhelpers.fetch.apt_install to install it.
984 """
985@@ -52,17 +56,26 @@
986 apt_install('python-jinja2', fatal=True)
987 from jinja2 import FileSystemLoader, Environment, exceptions
988
989- if templates_dir is None:
990- templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
991- loader = Environment(loader=FileSystemLoader(templates_dir))
992+ if template_loader:
993+ template_env = Environment(loader=template_loader)
994+ else:
995+ if templates_dir is None:
996+ templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
997+ template_env = Environment(loader=FileSystemLoader(templates_dir))
998 try:
999 source = source
1000- template = loader.get_template(source)
1001+ template = template_env.get_template(source)
1002 except exceptions.TemplateNotFound as e:
1003 hookenv.log('Could not load template %s from %s.' %
1004 (source, templates_dir),
1005 level=hookenv.ERROR)
1006 raise e
1007 content = template.render(context)
1008- host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
1009- host.write_file(target, content.encode(encoding), owner, group, perms)
1010+ if target is not None:
1011+ target_dir = os.path.dirname(target)
1012+ if not os.path.exists(target_dir):
1013+ # This is a terrible default directory permission, as the file
1014+ # or its siblings will often contain secrets.
1015+ host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
1016+ host.write_file(target, content.encode(encoding), owner, group, perms)
1017+ return content
1018
1019=== modified file 'hooks/charmhelpers/fetch/__init__.py'
1020--- hooks/charmhelpers/fetch/__init__.py 2015-08-19 13:50:42 +0000
1021+++ hooks/charmhelpers/fetch/__init__.py 2016-01-22 22:32:27 +0000
1022@@ -98,6 +98,14 @@
1023 'liberty/proposed': 'trusty-proposed/liberty',
1024 'trusty-liberty/proposed': 'trusty-proposed/liberty',
1025 'trusty-proposed/liberty': 'trusty-proposed/liberty',
1026+ # Mitaka
1027+ 'mitaka': 'trusty-updates/mitaka',
1028+ 'trusty-mitaka': 'trusty-updates/mitaka',
1029+ 'trusty-mitaka/updates': 'trusty-updates/mitaka',
1030+ 'trusty-updates/mitaka': 'trusty-updates/mitaka',
1031+ 'mitaka/proposed': 'trusty-proposed/mitaka',
1032+ 'trusty-mitaka/proposed': 'trusty-proposed/mitaka',
1033+ 'trusty-proposed/mitaka': 'trusty-proposed/mitaka',
1034 }
1035
1036 # The order of this list is very important. Handlers should be listed in from
1037@@ -225,12 +233,12 @@
1038
1039 def apt_mark(packages, mark, fatal=False):
1040 """Flag one or more packages using apt-mark"""
1041+ log("Marking {} as {}".format(packages, mark))
1042 cmd = ['apt-mark', mark]
1043 if isinstance(packages, six.string_types):
1044 cmd.append(packages)
1045 else:
1046 cmd.extend(packages)
1047- log("Holding {}".format(packages))
1048
1049 if fatal:
1050 subprocess.check_call(cmd, universal_newlines=True)
1051@@ -411,7 +419,7 @@
1052 importlib.import_module(package),
1053 classname)
1054 plugin_list.append(handler_class())
1055- except (ImportError, AttributeError):
1056+ except NotImplementedError:
1057 # Skip missing plugins so that they can be ommitted from
1058 # installation if desired
1059 log("FetchHandler {} not found, skipping plugin".format(
1060
1061=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
1062--- hooks/charmhelpers/fetch/archiveurl.py 2015-08-03 14:53:05 +0000
1063+++ hooks/charmhelpers/fetch/archiveurl.py 2016-01-22 22:32:27 +0000
1064@@ -108,7 +108,7 @@
1065 install_opener(opener)
1066 response = urlopen(source)
1067 try:
1068- with open(dest, 'w') as dest_file:
1069+ with open(dest, 'wb') as dest_file:
1070 dest_file.write(response.read())
1071 except Exception as e:
1072 if os.path.isfile(dest):
1073
1074=== modified file 'hooks/charmhelpers/fetch/bzrurl.py'
1075--- hooks/charmhelpers/fetch/bzrurl.py 2015-01-26 11:51:28 +0000
1076+++ hooks/charmhelpers/fetch/bzrurl.py 2016-01-22 22:32:27 +0000
1077@@ -15,60 +15,50 @@
1078 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1079
1080 import os
1081+from subprocess import check_call
1082 from charmhelpers.fetch import (
1083 BaseFetchHandler,
1084- UnhandledSource
1085+ UnhandledSource,
1086+ filter_installed_packages,
1087+ apt_install,
1088 )
1089 from charmhelpers.core.host import mkdir
1090
1091-import six
1092-if six.PY3:
1093- raise ImportError('bzrlib does not support Python3')
1094
1095-try:
1096- from bzrlib.branch import Branch
1097- from bzrlib import bzrdir, workingtree, errors
1098-except ImportError:
1099- from charmhelpers.fetch import apt_install
1100- apt_install("python-bzrlib")
1101- from bzrlib.branch import Branch
1102- from bzrlib import bzrdir, workingtree, errors
1103+if filter_installed_packages(['bzr']) != []:
1104+ apt_install(['bzr'])
1105+ if filter_installed_packages(['bzr']) != []:
1106+ raise NotImplementedError('Unable to install bzr')
1107
1108
1109 class BzrUrlFetchHandler(BaseFetchHandler):
1110 """Handler for bazaar branches via generic and lp URLs"""
1111 def can_handle(self, source):
1112 url_parts = self.parse_url(source)
1113- if url_parts.scheme not in ('bzr+ssh', 'lp'):
1114+ if url_parts.scheme not in ('bzr+ssh', 'lp', ''):
1115 return False
1116+ elif not url_parts.scheme:
1117+ return os.path.exists(os.path.join(source, '.bzr'))
1118 else:
1119 return True
1120
1121 def branch(self, source, dest):
1122- url_parts = self.parse_url(source)
1123- # If we use lp:branchname scheme we need to load plugins
1124 if not self.can_handle(source):
1125 raise UnhandledSource("Cannot handle {}".format(source))
1126- if url_parts.scheme == "lp":
1127- from bzrlib.plugin import load_plugins
1128- load_plugins()
1129- try:
1130- local_branch = bzrdir.BzrDir.create_branch_convenience(dest)
1131- except errors.AlreadyControlDirError:
1132- local_branch = Branch.open(dest)
1133- try:
1134- remote_branch = Branch.open(source)
1135- remote_branch.push(local_branch)
1136- tree = workingtree.WorkingTree.open(dest)
1137- tree.update()
1138- except Exception as e:
1139- raise e
1140+ if os.path.exists(dest):
1141+ check_call(['bzr', 'pull', '--overwrite', '-d', dest, source])
1142+ else:
1143+ check_call(['bzr', 'branch', source, dest])
1144
1145- def install(self, source):
1146+ def install(self, source, dest=None):
1147 url_parts = self.parse_url(source)
1148 branch_name = url_parts.path.strip("/").split("/")[-1]
1149- dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
1150- branch_name)
1151+ if dest:
1152+ dest_dir = os.path.join(dest, branch_name)
1153+ else:
1154+ dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
1155+ branch_name)
1156+
1157 if not os.path.exists(dest_dir):
1158 mkdir(dest_dir, perms=0o755)
1159 try:
1160
1161=== modified file 'hooks/charmhelpers/fetch/giturl.py'
1162--- hooks/charmhelpers/fetch/giturl.py 2015-08-03 14:53:05 +0000
1163+++ hooks/charmhelpers/fetch/giturl.py 2016-01-22 22:32:27 +0000
1164@@ -15,24 +15,18 @@
1165 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1166
1167 import os
1168+from subprocess import check_call, CalledProcessError
1169 from charmhelpers.fetch import (
1170 BaseFetchHandler,
1171- UnhandledSource
1172+ UnhandledSource,
1173+ filter_installed_packages,
1174+ apt_install,
1175 )
1176-from charmhelpers.core.host import mkdir
1177-
1178-import six
1179-if six.PY3:
1180- raise ImportError('GitPython does not support Python 3')
1181-
1182-try:
1183- from git import Repo
1184-except ImportError:
1185- from charmhelpers.fetch import apt_install
1186- apt_install("python-git")
1187- from git import Repo
1188-
1189-from git.exc import GitCommandError # noqa E402
1190+
1191+if filter_installed_packages(['git']) != []:
1192+ apt_install(['git'])
1193+ if filter_installed_packages(['git']) != []:
1194+ raise NotImplementedError('Unable to install git')
1195
1196
1197 class GitUrlFetchHandler(BaseFetchHandler):
1198@@ -40,19 +34,24 @@
1199 def can_handle(self, source):
1200 url_parts = self.parse_url(source)
1201 # TODO (mattyw) no support for ssh git@ yet
1202- if url_parts.scheme not in ('http', 'https', 'git'):
1203+ if url_parts.scheme not in ('http', 'https', 'git', ''):
1204 return False
1205+ elif not url_parts.scheme:
1206+ return os.path.exists(os.path.join(source, '.git'))
1207 else:
1208 return True
1209
1210- def clone(self, source, dest, branch, depth=None):
1211+ def clone(self, source, dest, branch="master", depth=None):
1212 if not self.can_handle(source):
1213 raise UnhandledSource("Cannot handle {}".format(source))
1214
1215- if depth:
1216- Repo.clone_from(source, dest, branch=branch, depth=depth)
1217+ if os.path.exists(dest):
1218+ cmd = ['git', '-C', dest, 'pull', source, branch]
1219 else:
1220- Repo.clone_from(source, dest, branch=branch)
1221+ cmd = ['git', 'clone', source, dest, '--branch', branch]
1222+ if depth:
1223+ cmd.extend(['--depth', depth])
1224+ check_call(cmd)
1225
1226 def install(self, source, branch="master", dest=None, depth=None):
1227 url_parts = self.parse_url(source)
1228@@ -62,11 +61,9 @@
1229 else:
1230 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
1231 branch_name)
1232- if not os.path.exists(dest_dir):
1233- mkdir(dest_dir, perms=0o755)
1234 try:
1235 self.clone(source, dest_dir, branch, depth)
1236- except GitCommandError as e:
1237+ except CalledProcessError as e:
1238 raise UnhandledSource(e)
1239 except OSError as e:
1240 raise UnhandledSource(e.strerror)
1241
1242=== modified file 'tests/018-basic-trusty-liberty' (properties changed: -x to +x)
1243=== modified file 'tests/020-basic-wily-liberty' (properties changed: -x to +x)
1244=== modified file 'tests/charmhelpers/contrib/amulet/deployment.py'
1245--- tests/charmhelpers/contrib/amulet/deployment.py 2015-01-26 11:51:28 +0000
1246+++ tests/charmhelpers/contrib/amulet/deployment.py 2016-01-22 22:32:27 +0000
1247@@ -51,7 +51,8 @@
1248 if 'units' not in this_service:
1249 this_service['units'] = 1
1250
1251- self.d.add(this_service['name'], units=this_service['units'])
1252+ self.d.add(this_service['name'], units=this_service['units'],
1253+ constraints=this_service.get('constraints'))
1254
1255 for svc in other_services:
1256 if 'location' in svc:
1257@@ -64,7 +65,8 @@
1258 if 'units' not in svc:
1259 svc['units'] = 1
1260
1261- self.d.add(svc['name'], charm=branch_location, units=svc['units'])
1262+ self.d.add(svc['name'], charm=branch_location, units=svc['units'],
1263+ constraints=svc.get('constraints'))
1264
1265 def _add_relations(self, relations):
1266 """Add all of the relations for the services."""
1267
1268=== modified file 'tests/charmhelpers/contrib/amulet/utils.py'
1269--- tests/charmhelpers/contrib/amulet/utils.py 2015-08-19 13:50:42 +0000
1270+++ tests/charmhelpers/contrib/amulet/utils.py 2016-01-22 22:32:27 +0000
1271@@ -19,9 +19,11 @@
1272 import logging
1273 import os
1274 import re
1275+import socket
1276 import subprocess
1277 import sys
1278 import time
1279+import uuid
1280
1281 import amulet
1282 import distro_info
1283@@ -114,7 +116,7 @@
1284 # /!\ DEPRECATION WARNING (beisner):
1285 # New and existing tests should be rewritten to use
1286 # validate_services_by_name() as it is aware of init systems.
1287- self.log.warn('/!\\ DEPRECATION WARNING: use '
1288+ self.log.warn('DEPRECATION WARNING: use '
1289 'validate_services_by_name instead of validate_services '
1290 'due to init system differences.')
1291
1292@@ -269,33 +271,52 @@
1293 """Get last modification time of directory."""
1294 return sentry_unit.directory_stat(directory)['mtime']
1295
1296- def _get_proc_start_time(self, sentry_unit, service, pgrep_full=False):
1297- """Get process' start time.
1298-
1299- Determine start time of the process based on the last modification
1300- time of the /proc/pid directory. If pgrep_full is True, the process
1301- name is matched against the full command line.
1302- """
1303- if pgrep_full:
1304- cmd = 'pgrep -o -f {}'.format(service)
1305- else:
1306- cmd = 'pgrep -o {}'.format(service)
1307- cmd = cmd + ' | grep -v pgrep || exit 0'
1308- cmd_out = sentry_unit.run(cmd)
1309- self.log.debug('CMDout: ' + str(cmd_out))
1310- if cmd_out[0]:
1311- self.log.debug('Pid for %s %s' % (service, str(cmd_out[0])))
1312- proc_dir = '/proc/{}'.format(cmd_out[0].strip())
1313- return self._get_dir_mtime(sentry_unit, proc_dir)
1314+ def _get_proc_start_time(self, sentry_unit, service, pgrep_full=None):
1315+ """Get start time of a process based on the last modification time
1316+ of the /proc/pid directory.
1317+
1318+ :sentry_unit: The sentry unit to check for the service on
1319+ :service: service name to look for in process table
1320+ :pgrep_full: [Deprecated] Use full command line search mode with pgrep
1321+ :returns: epoch time of service process start
1322+ :param commands: list of bash commands
1323+ :param sentry_units: list of sentry unit pointers
1324+ :returns: None if successful; Failure message otherwise
1325+ """
1326+ if pgrep_full is not None:
1327+ # /!\ DEPRECATION WARNING (beisner):
1328+ # No longer implemented, as pidof is now used instead of pgrep.
1329+ # https://bugs.launchpad.net/charm-helpers/+bug/1474030
1330+ self.log.warn('DEPRECATION WARNING: pgrep_full bool is no '
1331+ 'longer implemented re: lp 1474030.')
1332+
1333+ pid_list = self.get_process_id_list(sentry_unit, service)
1334+ pid = pid_list[0]
1335+ proc_dir = '/proc/{}'.format(pid)
1336+ self.log.debug('Pid for {} on {}: {}'.format(
1337+ service, sentry_unit.info['unit_name'], pid))
1338+
1339+ return self._get_dir_mtime(sentry_unit, proc_dir)
1340
1341 def service_restarted(self, sentry_unit, service, filename,
1342- pgrep_full=False, sleep_time=20):
1343+ pgrep_full=None, sleep_time=20):
1344 """Check if service was restarted.
1345
1346 Compare a service's start time vs a file's last modification time
1347 (such as a config file for that service) to determine if the service
1348 has been restarted.
1349 """
1350+ # /!\ DEPRECATION WARNING (beisner):
1351+ # This method is prone to races in that no before-time is known.
1352+ # Use validate_service_config_changed instead.
1353+
1354+ # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
1355+ # used instead of pgrep. pgrep_full is still passed through to ensure
1356+ # deprecation WARNS. lp1474030
1357+ self.log.warn('DEPRECATION WARNING: use '
1358+ 'validate_service_config_changed instead of '
1359+ 'service_restarted due to known races.')
1360+
1361 time.sleep(sleep_time)
1362 if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >=
1363 self._get_file_mtime(sentry_unit, filename)):
1364@@ -304,78 +325,122 @@
1365 return False
1366
1367 def service_restarted_since(self, sentry_unit, mtime, service,
1368- pgrep_full=False, sleep_time=20,
1369- retry_count=2):
1370+ pgrep_full=None, sleep_time=20,
1371+ retry_count=30, retry_sleep_time=10):
1372 """Check if service was been started after a given time.
1373
1374 Args:
1375 sentry_unit (sentry): The sentry unit to check for the service on
1376 mtime (float): The epoch time to check against
1377 service (string): service name to look for in process table
1378- pgrep_full (boolean): Use full command line search mode with pgrep
1379- sleep_time (int): Seconds to sleep before looking for process
1380- retry_count (int): If service is not found, how many times to retry
1381+ pgrep_full: [Deprecated] Use full command line search mode with pgrep
1382+ sleep_time (int): Initial sleep time (s) before looking for file
1383+ retry_sleep_time (int): Time (s) to sleep between retries
1384+ retry_count (int): If file is not found, how many times to retry
1385
1386 Returns:
1387 bool: True if service found and its start time it newer than mtime,
1388 False if service is older than mtime or if service was
1389 not found.
1390 """
1391- self.log.debug('Checking %s restarted since %s' % (service, mtime))
1392+ # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
1393+ # used instead of pgrep. pgrep_full is still passed through to ensure
1394+ # deprecation WARNS. lp1474030
1395+
1396+ unit_name = sentry_unit.info['unit_name']
1397+ self.log.debug('Checking that %s service restarted since %s on '
1398+ '%s' % (service, mtime, unit_name))
1399 time.sleep(sleep_time)
1400- proc_start_time = self._get_proc_start_time(sentry_unit, service,
1401- pgrep_full)
1402- while retry_count > 0 and not proc_start_time:
1403- self.log.debug('No pid file found for service %s, will retry %i '
1404- 'more times' % (service, retry_count))
1405- time.sleep(30)
1406- proc_start_time = self._get_proc_start_time(sentry_unit, service,
1407- pgrep_full)
1408- retry_count = retry_count - 1
1409+ proc_start_time = None
1410+ tries = 0
1411+ while tries <= retry_count and not proc_start_time:
1412+ try:
1413+ proc_start_time = self._get_proc_start_time(sentry_unit,
1414+ service,
1415+ pgrep_full)
1416+ self.log.debug('Attempt {} to get {} proc start time on {} '
1417+ 'OK'.format(tries, service, unit_name))
1418+ except IOError as e:
1419+ # NOTE(beisner) - race avoidance, proc may not exist yet.
1420+ # https://bugs.launchpad.net/charm-helpers/+bug/1474030
1421+ self.log.debug('Attempt {} to get {} proc start time on {} '
1422+ 'failed\n{}'.format(tries, service,
1423+ unit_name, e))
1424+ time.sleep(retry_sleep_time)
1425+ tries += 1
1426
1427 if not proc_start_time:
1428 self.log.warn('No proc start time found, assuming service did '
1429 'not start')
1430 return False
1431 if proc_start_time >= mtime:
1432- self.log.debug('proc start time is newer than provided mtime'
1433- '(%s >= %s)' % (proc_start_time, mtime))
1434+ self.log.debug('Proc start time is newer than provided mtime'
1435+ '(%s >= %s) on %s (OK)' % (proc_start_time,
1436+ mtime, unit_name))
1437 return True
1438 else:
1439- self.log.warn('proc start time (%s) is older than provided mtime '
1440- '(%s), service did not restart' % (proc_start_time,
1441- mtime))
1442+ self.log.warn('Proc start time (%s) is older than provided mtime '
1443+ '(%s) on %s, service did not '
1444+ 'restart' % (proc_start_time, mtime, unit_name))
1445 return False
1446
1447 def config_updated_since(self, sentry_unit, filename, mtime,
1448- sleep_time=20):
1449+ sleep_time=20, retry_count=30,
1450+ retry_sleep_time=10):
1451 """Check if file was modified after a given time.
1452
1453 Args:
1454 sentry_unit (sentry): The sentry unit to check the file mtime on
1455 filename (string): The file to check mtime of
1456 mtime (float): The epoch time to check against
1457- sleep_time (int): Seconds to sleep before looking for process
1458+ sleep_time (int): Initial sleep time (s) before looking for file
1459+ retry_sleep_time (int): Time (s) to sleep between retries
1460+ retry_count (int): If file is not found, how many times to retry
1461
1462 Returns:
1463 bool: True if file was modified more recently than mtime, False if
1464- file was modified before mtime,
1465+ file was modified before mtime, or if file not found.
1466 """
1467- self.log.debug('Checking %s updated since %s' % (filename, mtime))
1468+ unit_name = sentry_unit.info['unit_name']
1469+ self.log.debug('Checking that %s updated since %s on '
1470+ '%s' % (filename, mtime, unit_name))
1471 time.sleep(sleep_time)
1472- file_mtime = self._get_file_mtime(sentry_unit, filename)
1473+ file_mtime = None
1474+ tries = 0
1475+ while tries <= retry_count and not file_mtime:
1476+ try:
1477+ file_mtime = self._get_file_mtime(sentry_unit, filename)
1478+ self.log.debug('Attempt {} to get {} file mtime on {} '
1479+ 'OK'.format(tries, filename, unit_name))
1480+ except IOError as e:
1481+ # NOTE(beisner) - race avoidance, file may not exist yet.
1482+ # https://bugs.launchpad.net/charm-helpers/+bug/1474030
1483+ self.log.debug('Attempt {} to get {} file mtime on {} '
1484+ 'failed\n{}'.format(tries, filename,
1485+ unit_name, e))
1486+ time.sleep(retry_sleep_time)
1487+ tries += 1
1488+
1489+ if not file_mtime:
1490+ self.log.warn('Could not determine file mtime, assuming '
1491+ 'file does not exist')
1492+ return False
1493+
1494 if file_mtime >= mtime:
1495 self.log.debug('File mtime is newer than provided mtime '
1496- '(%s >= %s)' % (file_mtime, mtime))
1497+ '(%s >= %s) on %s (OK)' % (file_mtime,
1498+ mtime, unit_name))
1499 return True
1500 else:
1501- self.log.warn('File mtime %s is older than provided mtime %s'
1502- % (file_mtime, mtime))
1503+ self.log.warn('File mtime is older than provided mtime'
1504+ '(%s < on %s) on %s' % (file_mtime,
1505+ mtime, unit_name))
1506 return False
1507
1508 def validate_service_config_changed(self, sentry_unit, mtime, service,
1509- filename, pgrep_full=False,
1510- sleep_time=20, retry_count=2):
1511+ filename, pgrep_full=None,
1512+ sleep_time=20, retry_count=30,
1513+ retry_sleep_time=10):
1514 """Check service and file were updated after mtime
1515
1516 Args:
1517@@ -383,9 +448,10 @@
1518 mtime (float): The epoch time to check against
1519 service (string): service name to look for in process table
1520 filename (string): The file to check mtime of
1521- pgrep_full (boolean): Use full command line search mode with pgrep
1522- sleep_time (int): Seconds to sleep before looking for process
1523+ pgrep_full: [Deprecated] Use full command line search mode with pgrep
1524+ sleep_time (int): Initial sleep in seconds to pass to test helpers
1525 retry_count (int): If service is not found, how many times to retry
1526+ retry_sleep_time (int): Time in seconds to wait between retries
1527
1528 Typical Usage:
1529 u = OpenStackAmuletUtils(ERROR)
1530@@ -402,15 +468,27 @@
1531 mtime, False if service is older than mtime or if service was
1532 not found or if filename was modified before mtime.
1533 """
1534- self.log.debug('Checking %s restarted since %s' % (service, mtime))
1535- time.sleep(sleep_time)
1536- service_restart = self.service_restarted_since(sentry_unit, mtime,
1537- service,
1538- pgrep_full=pgrep_full,
1539- sleep_time=0,
1540- retry_count=retry_count)
1541- config_update = self.config_updated_since(sentry_unit, filename, mtime,
1542- sleep_time=0)
1543+
1544+ # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
1545+ # used instead of pgrep. pgrep_full is still passed through to ensure
1546+ # deprecation WARNS. lp1474030
1547+
1548+ service_restart = self.service_restarted_since(
1549+ sentry_unit, mtime,
1550+ service,
1551+ pgrep_full=pgrep_full,
1552+ sleep_time=sleep_time,
1553+ retry_count=retry_count,
1554+ retry_sleep_time=retry_sleep_time)
1555+
1556+ config_update = self.config_updated_since(
1557+ sentry_unit,
1558+ filename,
1559+ mtime,
1560+ sleep_time=sleep_time,
1561+ retry_count=retry_count,
1562+ retry_sleep_time=retry_sleep_time)
1563+
1564 return service_restart and config_update
1565
1566 def get_sentry_time(self, sentry_unit):
1567@@ -428,7 +506,6 @@
1568 """Return a list of all Ubuntu releases in order of release."""
1569 _d = distro_info.UbuntuDistroInfo()
1570 _release_list = _d.all
1571- self.log.debug('Ubuntu release list: {}'.format(_release_list))
1572 return _release_list
1573
1574 def file_to_url(self, file_rel_path):
1575@@ -568,6 +645,142 @@
1576
1577 return None
1578
1579+ def validate_sectionless_conf(self, file_contents, expected):
1580+ """A crude conf parser. Useful to inspect configuration files which
1581+ do not have section headers (as would be necessary in order to use
1582+ the configparser). Such as openstack-dashboard or rabbitmq confs."""
1583+ for line in file_contents.split('\n'):
1584+ if '=' in line:
1585+ args = line.split('=')
1586+ if len(args) <= 1:
1587+ continue
1588+ key = args[0].strip()
1589+ value = args[1].strip()
1590+ if key in expected.keys():
1591+ if expected[key] != value:
1592+ msg = ('Config mismatch. Expected, actual: {}, '
1593+ '{}'.format(expected[key], value))
1594+ amulet.raise_status(amulet.FAIL, msg=msg)
1595+
1596+ def get_unit_hostnames(self, units):
1597+ """Return a dict of juju unit names to hostnames."""
1598+ host_names = {}
1599+ for unit in units:
1600+ host_names[unit.info['unit_name']] = \
1601+ str(unit.file_contents('/etc/hostname').strip())
1602+ self.log.debug('Unit host names: {}'.format(host_names))
1603+ return host_names
1604+
1605+ def run_cmd_unit(self, sentry_unit, cmd):
1606+ """Run a command on a unit, return the output and exit code."""
1607+ output, code = sentry_unit.run(cmd)
1608+ if code == 0:
1609+ self.log.debug('{} `{}` command returned {} '
1610+ '(OK)'.format(sentry_unit.info['unit_name'],
1611+ cmd, code))
1612+ else:
1613+ msg = ('{} `{}` command returned {} '
1614+ '{}'.format(sentry_unit.info['unit_name'],
1615+ cmd, code, output))
1616+ amulet.raise_status(amulet.FAIL, msg=msg)
1617+ return str(output), code
1618+
1619+ def file_exists_on_unit(self, sentry_unit, file_name):
1620+ """Check if a file exists on a unit."""
1621+ try:
1622+ sentry_unit.file_stat(file_name)
1623+ return True
1624+ except IOError:
1625+ return False
1626+ except Exception as e:
1627+ msg = 'Error checking file {}: {}'.format(file_name, e)
1628+ amulet.raise_status(amulet.FAIL, msg=msg)
1629+
1630+ def file_contents_safe(self, sentry_unit, file_name,
1631+ max_wait=60, fatal=False):
1632+ """Get file contents from a sentry unit. Wrap amulet file_contents
1633+ with retry logic to address races where a file checks as existing,
1634+ but no longer exists by the time file_contents is called.
1635+ Return None if file not found. Optionally raise if fatal is True."""
1636+ unit_name = sentry_unit.info['unit_name']
1637+ file_contents = False
1638+ tries = 0
1639+ while not file_contents and tries < (max_wait / 4):
1640+ try:
1641+ file_contents = sentry_unit.file_contents(file_name)
1642+ except IOError:
1643+ self.log.debug('Attempt {} to open file {} from {} '
1644+ 'failed'.format(tries, file_name,
1645+ unit_name))
1646+ time.sleep(4)
1647+ tries += 1
1648+
1649+ if file_contents:
1650+ return file_contents
1651+ elif not fatal:
1652+ return None
1653+ elif fatal:
1654+ msg = 'Failed to get file contents from unit.'
1655+ amulet.raise_status(amulet.FAIL, msg)
1656+
1657+ def port_knock_tcp(self, host="localhost", port=22, timeout=15):
1658+ """Open a TCP socket to check for a listening sevice on a host.
1659+
1660+ :param host: host name or IP address, default to localhost
1661+ :param port: TCP port number, default to 22
1662+ :param timeout: Connect timeout, default to 15 seconds
1663+ :returns: True if successful, False if connect failed
1664+ """
1665+
1666+ # Resolve host name if possible
1667+ try:
1668+ connect_host = socket.gethostbyname(host)
1669+ host_human = "{} ({})".format(connect_host, host)
1670+ except socket.error as e:
1671+ self.log.warn('Unable to resolve address: '
1672+ '{} ({}) Trying anyway!'.format(host, e))
1673+ connect_host = host
1674+ host_human = connect_host
1675+
1676+ # Attempt socket connection
1677+ try:
1678+ knock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1679+ knock.settimeout(timeout)
1680+ knock.connect((connect_host, port))
1681+ knock.close()
1682+ self.log.debug('Socket connect OK for host '
1683+ '{} on port {}.'.format(host_human, port))
1684+ return True
1685+ except socket.error as e:
1686+ self.log.debug('Socket connect FAIL for'
1687+ ' {} port {} ({})'.format(host_human, port, e))
1688+ return False
1689+
1690+ def port_knock_units(self, sentry_units, port=22,
1691+ timeout=15, expect_success=True):
1692+ """Open a TCP socket to check for a listening sevice on each
1693+ listed juju unit.
1694+
1695+ :param sentry_units: list of sentry unit pointers
1696+ :param port: TCP port number, default to 22
1697+ :param timeout: Connect timeout, default to 15 seconds
1698+ :expect_success: True by default, set False to invert logic
1699+ :returns: None if successful, Failure message otherwise
1700+ """
1701+ for unit in sentry_units:
1702+ host = unit.info['public-address']
1703+ connected = self.port_knock_tcp(host, port, timeout)
1704+ if not connected and expect_success:
1705+ return 'Socket connect failed.'
1706+ elif connected and not expect_success:
1707+ return 'Socket connected unexpectedly.'
1708+
1709+ def get_uuid_epoch_stamp(self):
1710+ """Returns a stamp string based on uuid4 and epoch time. Useful in
1711+ generating test messages which need to be unique-ish."""
1712+ return '[{}-{}]'.format(uuid.uuid4(), time.time())
1713+
1714+# amulet juju action helpers:
1715 def run_action(self, unit_sentry, action,
1716 _check_output=subprocess.check_output):
1717 """Run the named action on a given unit sentry.
1718@@ -594,3 +807,12 @@
1719 output = _check_output(command, universal_newlines=True)
1720 data = json.loads(output)
1721 return data.get(u"status") == "completed"
1722+
1723+ def status_get(self, unit):
1724+ """Return the current service status of this unit."""
1725+ raw_status, return_code = unit.run(
1726+ "status-get --format=json --include-data")
1727+ if return_code != 0:
1728+ return ("unknown", "")
1729+ status = json.loads(raw_status)
1730+ return (status["status"], status["message"])
1731
1732=== modified file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py'
1733--- tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-08-19 13:50:42 +0000
1734+++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2016-01-22 22:32:27 +0000
1735@@ -14,12 +14,18 @@
1736 # You should have received a copy of the GNU Lesser General Public License
1737 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1738
1739+import logging
1740+import re
1741+import sys
1742 import six
1743 from collections import OrderedDict
1744 from charmhelpers.contrib.amulet.deployment import (
1745 AmuletDeployment
1746 )
1747
1748+DEBUG = logging.DEBUG
1749+ERROR = logging.ERROR
1750+
1751
1752 class OpenStackAmuletDeployment(AmuletDeployment):
1753 """OpenStack amulet deployment.
1754@@ -28,9 +34,12 @@
1755 that is specifically for use by OpenStack charms.
1756 """
1757
1758- def __init__(self, series=None, openstack=None, source=None, stable=True):
1759+ def __init__(self, series=None, openstack=None, source=None,
1760+ stable=True, log_level=DEBUG):
1761 """Initialize the deployment environment."""
1762 super(OpenStackAmuletDeployment, self).__init__(series)
1763+ self.log = self.get_logger(level=log_level)
1764+ self.log.info('OpenStackAmuletDeployment: init')
1765 self.openstack = openstack
1766 self.source = source
1767 self.stable = stable
1768@@ -38,26 +47,55 @@
1769 # out.
1770 self.current_next = "trusty"
1771
1772+ def get_logger(self, name="deployment-logger", level=logging.DEBUG):
1773+ """Get a logger object that will log to stdout."""
1774+ log = logging
1775+ logger = log.getLogger(name)
1776+ fmt = log.Formatter("%(asctime)s %(funcName)s "
1777+ "%(levelname)s: %(message)s")
1778+
1779+ handler = log.StreamHandler(stream=sys.stdout)
1780+ handler.setLevel(level)
1781+ handler.setFormatter(fmt)
1782+
1783+ logger.addHandler(handler)
1784+ logger.setLevel(level)
1785+
1786+ return logger
1787+
1788 def _determine_branch_locations(self, other_services):
1789 """Determine the branch locations for the other services.
1790
1791 Determine if the local branch being tested is derived from its
1792 stable or next (dev) branch, and based on this, use the corresonding
1793 stable or next branches for the other_services."""
1794+
1795+ self.log.info('OpenStackAmuletDeployment: determine branch locations')
1796+
1797+ # Charms outside the lp:~openstack-charmers namespace
1798 base_charms = ['mysql', 'mongodb', 'nrpe']
1799
1800+ # Force these charms to current series even when using an older series.
1801+ # ie. Use trusty/nrpe even when series is precise, as the P charm
1802+ # does not possess the necessary external master config and hooks.
1803+ force_series_current = ['nrpe']
1804+
1805 if self.series in ['precise', 'trusty']:
1806 base_series = self.series
1807 else:
1808 base_series = self.current_next
1809
1810- if self.stable:
1811- for svc in other_services:
1812+ for svc in other_services:
1813+ if svc['name'] in force_series_current:
1814+ base_series = self.current_next
1815+ # If a location has been explicitly set, use it
1816+ if svc.get('location'):
1817+ continue
1818+ if self.stable:
1819 temp = 'lp:charms/{}/{}'
1820 svc['location'] = temp.format(base_series,
1821 svc['name'])
1822- else:
1823- for svc in other_services:
1824+ else:
1825 if svc['name'] in base_charms:
1826 temp = 'lp:charms/{}/{}'
1827 svc['location'] = temp.format(base_series,
1828@@ -66,10 +104,13 @@
1829 temp = 'lp:~openstack-charmers/charms/{}/{}/next'
1830 svc['location'] = temp.format(self.current_next,
1831 svc['name'])
1832+
1833 return other_services
1834
1835 def _add_services(self, this_service, other_services):
1836 """Add services to the deployment and set openstack-origin/source."""
1837+ self.log.info('OpenStackAmuletDeployment: adding services')
1838+
1839 other_services = self._determine_branch_locations(other_services)
1840
1841 super(OpenStackAmuletDeployment, self)._add_services(this_service,
1842@@ -77,29 +118,103 @@
1843
1844 services = other_services
1845 services.append(this_service)
1846+
1847+ # Charms which should use the source config option
1848 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
1849 'ceph-osd', 'ceph-radosgw']
1850- # Most OpenStack subordinate charms do not expose an origin option
1851- # as that is controlled by the principle.
1852- ignore = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe']
1853+
1854+ # Charms which can not use openstack-origin, ie. many subordinates
1855+ no_origin = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe',
1856+ 'openvswitch-odl', 'neutron-api-odl', 'odl-controller',
1857+ 'cinder-backup']
1858
1859 if self.openstack:
1860 for svc in services:
1861- if svc['name'] not in use_source + ignore:
1862+ if svc['name'] not in use_source + no_origin:
1863 config = {'openstack-origin': self.openstack}
1864 self.d.configure(svc['name'], config)
1865
1866 if self.source:
1867 for svc in services:
1868- if svc['name'] in use_source and svc['name'] not in ignore:
1869+ if svc['name'] in use_source and svc['name'] not in no_origin:
1870 config = {'source': self.source}
1871 self.d.configure(svc['name'], config)
1872
1873 def _configure_services(self, configs):
1874 """Configure all of the services."""
1875+ self.log.info('OpenStackAmuletDeployment: configure services')
1876 for service, config in six.iteritems(configs):
1877 self.d.configure(service, config)
1878
1879+ def _auto_wait_for_status(self, message=None, exclude_services=None,
1880+ include_only=None, timeout=1800):
1881+ """Wait for all units to have a specific extended status, except
1882+ for any defined as excluded. Unless specified via message, any
1883+ status containing any case of 'ready' will be considered a match.
1884+
1885+ Examples of message usage:
1886+
1887+ Wait for all unit status to CONTAIN any case of 'ready' or 'ok':
1888+ message = re.compile('.*ready.*|.*ok.*', re.IGNORECASE)
1889+
1890+ Wait for all units to reach this status (exact match):
1891+ message = re.compile('^Unit is ready and clustered$')
1892+
1893+ Wait for all units to reach any one of these (exact match):
1894+ message = re.compile('Unit is ready|OK|Ready')
1895+
1896+ Wait for at least one unit to reach this status (exact match):
1897+ message = {'ready'}
1898+
1899+ See Amulet's sentry.wait_for_messages() for message usage detail.
1900+ https://github.com/juju/amulet/blob/master/amulet/sentry.py
1901+
1902+ :param message: Expected status match
1903+ :param exclude_services: List of juju service names to ignore,
1904+ not to be used in conjuction with include_only.
1905+ :param include_only: List of juju service names to exclusively check,
1906+ not to be used in conjuction with exclude_services.
1907+ :param timeout: Maximum time in seconds to wait for status match
1908+ :returns: None. Raises if timeout is hit.
1909+ """
1910+ self.log.info('Waiting for extended status on units...')
1911+
1912+ all_services = self.d.services.keys()
1913+
1914+ if exclude_services and include_only:
1915+ raise ValueError('exclude_services can not be used '
1916+ 'with include_only')
1917+
1918+ if message:
1919+ if isinstance(message, re._pattern_type):
1920+ match = message.pattern
1921+ else:
1922+ match = message
1923+
1924+ self.log.debug('Custom extended status wait match: '
1925+ '{}'.format(match))
1926+ else:
1927+ self.log.debug('Default extended status wait match: contains '
1928+ 'READY (case-insensitive)')
1929+ message = re.compile('.*ready.*', re.IGNORECASE)
1930+
1931+ if exclude_services:
1932+ self.log.debug('Excluding services from extended status match: '
1933+ '{}'.format(exclude_services))
1934+ else:
1935+ exclude_services = []
1936+
1937+ if include_only:
1938+ services = include_only
1939+ else:
1940+ services = list(set(all_services) - set(exclude_services))
1941+
1942+ self.log.debug('Waiting up to {}s for extended status on services: '
1943+ '{}'.format(timeout, services))
1944+ service_messages = {service: message for service in services}
1945+ self.d.sentry.wait_for_messages(service_messages, timeout=timeout)
1946+ self.log.info('OK')
1947+
1948 def _get_openstack_release(self):
1949 """Get openstack release.
1950
1951@@ -111,7 +226,8 @@
1952 self.precise_havana, self.precise_icehouse,
1953 self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
1954 self.trusty_kilo, self.vivid_kilo, self.trusty_liberty,
1955- self.wily_liberty) = range(12)
1956+ self.wily_liberty, self.trusty_mitaka,
1957+ self.xenial_mitaka) = range(14)
1958
1959 releases = {
1960 ('precise', None): self.precise_essex,
1961@@ -123,9 +239,11 @@
1962 ('trusty', 'cloud:trusty-juno'): self.trusty_juno,
1963 ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,
1964 ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty,
1965+ ('trusty', 'cloud:trusty-mitaka'): self.trusty_mitaka,
1966 ('utopic', None): self.utopic_juno,
1967 ('vivid', None): self.vivid_kilo,
1968- ('wily', None): self.wily_liberty}
1969+ ('wily', None): self.wily_liberty,
1970+ ('xenial', None): self.xenial_mitaka}
1971 return releases[(self.series, self.openstack)]
1972
1973 def _get_openstack_release_string(self):
1974@@ -142,6 +260,7 @@
1975 ('utopic', 'juno'),
1976 ('vivid', 'kilo'),
1977 ('wily', 'liberty'),
1978+ ('xenial', 'mitaka'),
1979 ])
1980 if self.openstack:
1981 os_origin = self.openstack.split(':')[1]
1982
1983=== modified file 'tests/charmhelpers/contrib/openstack/amulet/utils.py'
1984--- tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-06-29 14:24:05 +0000
1985+++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2016-01-22 22:32:27 +0000
1986@@ -18,6 +18,7 @@
1987 import json
1988 import logging
1989 import os
1990+import re
1991 import six
1992 import time
1993 import urllib
1994@@ -27,6 +28,7 @@
1995 import heatclient.v1.client as heat_client
1996 import keystoneclient.v2_0 as keystone_client
1997 import novaclient.v1_1.client as nova_client
1998+import pika
1999 import swiftclient
2000
2001 from charmhelpers.contrib.amulet.utils import (
2002@@ -602,3 +604,382 @@
2003 self.log.debug('Ceph {} samples (OK): '
2004 '{}'.format(sample_type, samples))
2005 return None
2006+
2007+ # rabbitmq/amqp specific helpers:
2008+
2009+ def rmq_wait_for_cluster(self, deployment, init_sleep=15, timeout=1200):
2010+ """Wait for rmq units extended status to show cluster readiness,
2011+ after an optional initial sleep period. Initial sleep is likely
2012+ necessary to be effective following a config change, as status
2013+ message may not instantly update to non-ready."""
2014+
2015+ if init_sleep:
2016+ time.sleep(init_sleep)
2017+
2018+ message = re.compile('^Unit is ready and clustered$')
2019+ deployment._auto_wait_for_status(message=message,
2020+ timeout=timeout,
2021+ include_only=['rabbitmq-server'])
2022+
2023+ def add_rmq_test_user(self, sentry_units,
2024+ username="testuser1", password="changeme"):
2025+ """Add a test user via the first rmq juju unit, check connection as
2026+ the new user against all sentry units.
2027+
2028+ :param sentry_units: list of sentry unit pointers
2029+ :param username: amqp user name, default to testuser1
2030+ :param password: amqp user password
2031+ :returns: None if successful. Raise on error.
2032+ """
2033+ self.log.debug('Adding rmq user ({})...'.format(username))
2034+
2035+ # Check that user does not already exist
2036+ cmd_user_list = 'rabbitmqctl list_users'
2037+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
2038+ if username in output:
2039+ self.log.warning('User ({}) already exists, returning '
2040+ 'gracefully.'.format(username))
2041+ return
2042+
2043+ perms = '".*" ".*" ".*"'
2044+ cmds = ['rabbitmqctl add_user {} {}'.format(username, password),
2045+ 'rabbitmqctl set_permissions {} {}'.format(username, perms)]
2046+
2047+ # Add user via first unit
2048+ for cmd in cmds:
2049+ output, _ = self.run_cmd_unit(sentry_units[0], cmd)
2050+
2051+ # Check connection against the other sentry_units
2052+ self.log.debug('Checking user connect against units...')
2053+ for sentry_unit in sentry_units:
2054+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=False,
2055+ username=username,
2056+ password=password)
2057+ connection.close()
2058+
2059+ def delete_rmq_test_user(self, sentry_units, username="testuser1"):
2060+ """Delete a rabbitmq user via the first rmq juju unit.
2061+
2062+ :param sentry_units: list of sentry unit pointers
2063+ :param username: amqp user name, default to testuser1
2064+ :param password: amqp user password
2065+ :returns: None if successful or no such user.
2066+ """
2067+ self.log.debug('Deleting rmq user ({})...'.format(username))
2068+
2069+ # Check that the user exists
2070+ cmd_user_list = 'rabbitmqctl list_users'
2071+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
2072+
2073+ if username not in output:
2074+ self.log.warning('User ({}) does not exist, returning '
2075+ 'gracefully.'.format(username))
2076+ return
2077+
2078+ # Delete the user
2079+ cmd_user_del = 'rabbitmqctl delete_user {}'.format(username)
2080+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_del)
2081+
2082+ def get_rmq_cluster_status(self, sentry_unit):
2083+ """Execute rabbitmq cluster status command on a unit and return
2084+ the full output.
2085+
2086+ :param unit: sentry unit
2087+ :returns: String containing console output of cluster status command
2088+ """
2089+ cmd = 'rabbitmqctl cluster_status'
2090+ output, _ = self.run_cmd_unit(sentry_unit, cmd)
2091+ self.log.debug('{} cluster_status:\n{}'.format(
2092+ sentry_unit.info['unit_name'], output))
2093+ return str(output)
2094+
2095+ def get_rmq_cluster_running_nodes(self, sentry_unit):
2096+ """Parse rabbitmqctl cluster_status output string, return list of
2097+ running rabbitmq cluster nodes.
2098+
2099+ :param unit: sentry unit
2100+ :returns: List containing node names of running nodes
2101+ """
2102+ # NOTE(beisner): rabbitmqctl cluster_status output is not
2103+ # json-parsable, do string chop foo, then json.loads that.
2104+ str_stat = self.get_rmq_cluster_status(sentry_unit)
2105+ if 'running_nodes' in str_stat:
2106+ pos_start = str_stat.find("{running_nodes,") + 15
2107+ pos_end = str_stat.find("]},", pos_start) + 1
2108+ str_run_nodes = str_stat[pos_start:pos_end].replace("'", '"')
2109+ run_nodes = json.loads(str_run_nodes)
2110+ return run_nodes
2111+ else:
2112+ return []
2113+
2114+ def validate_rmq_cluster_running_nodes(self, sentry_units):
2115+ """Check that all rmq unit hostnames are represented in the
2116+ cluster_status output of all units.
2117+
2118+ :param host_names: dict of juju unit names to host names
2119+ :param units: list of sentry unit pointers (all rmq units)
2120+ :returns: None if successful, otherwise return error message
2121+ """
2122+ host_names = self.get_unit_hostnames(sentry_units)
2123+ errors = []
2124+
2125+ # Query every unit for cluster_status running nodes
2126+ for query_unit in sentry_units:
2127+ query_unit_name = query_unit.info['unit_name']
2128+ running_nodes = self.get_rmq_cluster_running_nodes(query_unit)
2129+
2130+ # Confirm that every unit is represented in the queried unit's
2131+ # cluster_status running nodes output.
2132+ for validate_unit in sentry_units:
2133+ val_host_name = host_names[validate_unit.info['unit_name']]
2134+ val_node_name = 'rabbit@{}'.format(val_host_name)
2135+
2136+ if val_node_name not in running_nodes:
2137+ errors.append('Cluster member check failed on {}: {} not '
2138+ 'in {}\n'.format(query_unit_name,
2139+ val_node_name,
2140+ running_nodes))
2141+ if errors:
2142+ return ''.join(errors)
2143+
2144+ def rmq_ssl_is_enabled_on_unit(self, sentry_unit, port=None):
2145+ """Check a single juju rmq unit for ssl and port in the config file."""
2146+ host = sentry_unit.info['public-address']
2147+ unit_name = sentry_unit.info['unit_name']
2148+
2149+ conf_file = '/etc/rabbitmq/rabbitmq.config'
2150+ conf_contents = str(self.file_contents_safe(sentry_unit,
2151+ conf_file, max_wait=16))
2152+ # Checks
2153+ conf_ssl = 'ssl' in conf_contents
2154+ conf_port = str(port) in conf_contents
2155+
2156+ # Port explicitly checked in config
2157+ if port and conf_port and conf_ssl:
2158+ self.log.debug('SSL is enabled @{}:{} '
2159+ '({})'.format(host, port, unit_name))
2160+ return True
2161+ elif port and not conf_port and conf_ssl:
2162+ self.log.debug('SSL is enabled @{} but not on port {} '
2163+ '({})'.format(host, port, unit_name))
2164+ return False
2165+ # Port not checked (useful when checking that ssl is disabled)
2166+ elif not port and conf_ssl:
2167+ self.log.debug('SSL is enabled @{}:{} '
2168+ '({})'.format(host, port, unit_name))
2169+ return True
2170+ elif not conf_ssl:
2171+ self.log.debug('SSL not enabled @{}:{} '
2172+ '({})'.format(host, port, unit_name))
2173+ return False
2174+ else:
2175+ msg = ('Unknown condition when checking SSL status @{}:{} '
2176+ '({})'.format(host, port, unit_name))
2177+ amulet.raise_status(amulet.FAIL, msg)
2178+
2179+ def validate_rmq_ssl_enabled_units(self, sentry_units, port=None):
2180+ """Check that ssl is enabled on rmq juju sentry units.
2181+
2182+ :param sentry_units: list of all rmq sentry units
2183+ :param port: optional ssl port override to validate
2184+ :returns: None if successful, otherwise return error message
2185+ """
2186+ for sentry_unit in sentry_units:
2187+ if not self.rmq_ssl_is_enabled_on_unit(sentry_unit, port=port):
2188+ return ('Unexpected condition: ssl is disabled on unit '
2189+ '({})'.format(sentry_unit.info['unit_name']))
2190+ return None
2191+
2192+ def validate_rmq_ssl_disabled_units(self, sentry_units):
2193+ """Check that ssl is enabled on listed rmq juju sentry units.
2194+
2195+ :param sentry_units: list of all rmq sentry units
2196+ :returns: True if successful. Raise on error.
2197+ """
2198+ for sentry_unit in sentry_units:
2199+ if self.rmq_ssl_is_enabled_on_unit(sentry_unit):
2200+ return ('Unexpected condition: ssl is enabled on unit '
2201+ '({})'.format(sentry_unit.info['unit_name']))
2202+ return None
2203+
2204+ def configure_rmq_ssl_on(self, sentry_units, deployment,
2205+ port=None, max_wait=60):
2206+ """Turn ssl charm config option on, with optional non-default
2207+ ssl port specification. Confirm that it is enabled on every
2208+ unit.
2209+
2210+ :param sentry_units: list of sentry units
2211+ :param deployment: amulet deployment object pointer
2212+ :param port: amqp port, use defaults if None
2213+ :param max_wait: maximum time to wait in seconds to confirm
2214+ :returns: None if successful. Raise on error.
2215+ """
2216+ self.log.debug('Setting ssl charm config option: on')
2217+
2218+ # Enable RMQ SSL
2219+ config = {'ssl': 'on'}
2220+ if port:
2221+ config['ssl_port'] = port
2222+
2223+ deployment.d.configure('rabbitmq-server', config)
2224+
2225+ # Wait for unit status
2226+ self.rmq_wait_for_cluster(deployment)
2227+
2228+ # Confirm
2229+ tries = 0
2230+ ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
2231+ while ret and tries < (max_wait / 4):
2232+ time.sleep(4)
2233+ self.log.debug('Attempt {}: {}'.format(tries, ret))
2234+ ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
2235+ tries += 1
2236+
2237+ if ret:
2238+ amulet.raise_status(amulet.FAIL, ret)
2239+
2240+ def configure_rmq_ssl_off(self, sentry_units, deployment, max_wait=60):
2241+ """Turn ssl charm config option off, confirm that it is disabled
2242+ on every unit.
2243+
2244+ :param sentry_units: list of sentry units
2245+ :param deployment: amulet deployment object pointer
2246+ :param max_wait: maximum time to wait in seconds to confirm
2247+ :returns: None if successful. Raise on error.
2248+ """
2249+ self.log.debug('Setting ssl charm config option: off')
2250+
2251+ # Disable RMQ SSL
2252+ config = {'ssl': 'off'}
2253+ deployment.d.configure('rabbitmq-server', config)
2254+
2255+ # Wait for unit status
2256+ self.rmq_wait_for_cluster(deployment)
2257+
2258+ # Confirm
2259+ tries = 0
2260+ ret = self.validate_rmq_ssl_disabled_units(sentry_units)
2261+ while ret and tries < (max_wait / 4):
2262+ time.sleep(4)
2263+ self.log.debug('Attempt {}: {}'.format(tries, ret))
2264+ ret = self.validate_rmq_ssl_disabled_units(sentry_units)
2265+ tries += 1
2266+
2267+ if ret:
2268+ amulet.raise_status(amulet.FAIL, ret)
2269+
2270+ def connect_amqp_by_unit(self, sentry_unit, ssl=False,
2271+ port=None, fatal=True,
2272+ username="testuser1", password="changeme"):
2273+ """Establish and return a pika amqp connection to the rabbitmq service
2274+ running on a rmq juju unit.
2275+
2276+ :param sentry_unit: sentry unit pointer
2277+ :param ssl: boolean, default to False
2278+ :param port: amqp port, use defaults if None
2279+ :param fatal: boolean, default to True (raises on connect error)
2280+ :param username: amqp user name, default to testuser1
2281+ :param password: amqp user password
2282+ :returns: pika amqp connection pointer or None if failed and non-fatal
2283+ """
2284+ host = sentry_unit.info['public-address']
2285+ unit_name = sentry_unit.info['unit_name']
2286+
2287+ # Default port logic if port is not specified
2288+ if ssl and not port:
2289+ port = 5671
2290+ elif not ssl and not port:
2291+ port = 5672
2292+
2293+ self.log.debug('Connecting to amqp on {}:{} ({}) as '
2294+ '{}...'.format(host, port, unit_name, username))
2295+
2296+ try:
2297+ credentials = pika.PlainCredentials(username, password)
2298+ parameters = pika.ConnectionParameters(host=host, port=port,
2299+ credentials=credentials,
2300+ ssl=ssl,
2301+ connection_attempts=3,
2302+ retry_delay=5,
2303+ socket_timeout=1)
2304+ connection = pika.BlockingConnection(parameters)
2305+ assert connection.server_properties['product'] == 'RabbitMQ'
2306+ self.log.debug('Connect OK')
2307+ return connection
2308+ except Exception as e:
2309+ msg = ('amqp connection failed to {}:{} as '
2310+ '{} ({})'.format(host, port, username, str(e)))
2311+ if fatal:
2312+ amulet.raise_status(amulet.FAIL, msg)
2313+ else:
2314+ self.log.warn(msg)
2315+ return None
2316+
2317+ def publish_amqp_message_by_unit(self, sentry_unit, message,
2318+ queue="test", ssl=False,
2319+ username="testuser1",
2320+ password="changeme",
2321+ port=None):
2322+ """Publish an amqp message to a rmq juju unit.
2323+
2324+ :param sentry_unit: sentry unit pointer
2325+ :param message: amqp message string
2326+ :param queue: message queue, default to test
2327+ :param username: amqp user name, default to testuser1
2328+ :param password: amqp user password
2329+ :param ssl: boolean, default to False
2330+ :param port: amqp port, use defaults if None
2331+ :returns: None. Raises exception if publish failed.
2332+ """
2333+ self.log.debug('Publishing message to {} queue:\n{}'.format(queue,
2334+ message))
2335+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
2336+ port=port,
2337+ username=username,
2338+ password=password)
2339+
2340+ # NOTE(beisner): extra debug here re: pika hang potential:
2341+ # https://github.com/pika/pika/issues/297
2342+ # https://groups.google.com/forum/#!topic/rabbitmq-users/Ja0iyfF0Szw
2343+ self.log.debug('Defining channel...')
2344+ channel = connection.channel()
2345+ self.log.debug('Declaring queue...')
2346+ channel.queue_declare(queue=queue, auto_delete=False, durable=True)
2347+ self.log.debug('Publishing message...')
2348+ channel.basic_publish(exchange='', routing_key=queue, body=message)
2349+ self.log.debug('Closing channel...')
2350+ channel.close()
2351+ self.log.debug('Closing connection...')
2352+ connection.close()
2353+
2354+ def get_amqp_message_by_unit(self, sentry_unit, queue="test",
2355+ username="testuser1",
2356+ password="changeme",
2357+ ssl=False, port=None):
2358+ """Get an amqp message from a rmq juju unit.
2359+
2360+ :param sentry_unit: sentry unit pointer
2361+ :param queue: message queue, default to test
2362+ :param username: amqp user name, default to testuser1
2363+ :param password: amqp user password
2364+ :param ssl: boolean, default to False
2365+ :param port: amqp port, use defaults if None
2366+ :returns: amqp message body as string. Raise if get fails.
2367+ """
2368+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
2369+ port=port,
2370+ username=username,
2371+ password=password)
2372+ channel = connection.channel()
2373+ method_frame, _, body = channel.basic_get(queue)
2374+
2375+ if method_frame:
2376+ self.log.debug('Retreived message from {} queue:\n{}'.format(queue,
2377+ body))
2378+ channel.basic_ack(method_frame.delivery_tag)
2379+ channel.close()
2380+ connection.close()
2381+ return body
2382+ else:
2383+ msg = 'No message retrieved.'
2384+ amulet.raise_status(amulet.FAIL, msg)

Subscribers

People subscribed via source and target branches