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
=== modified file 'hooks/charmhelpers/cli/__init__.py'
--- hooks/charmhelpers/cli/__init__.py 2015-08-19 13:50:42 +0000
+++ hooks/charmhelpers/cli/__init__.py 2016-01-22 22:32:27 +0000
@@ -20,7 +20,7 @@
2020
21from six.moves import zip21from six.moves import zip
2222
23from charmhelpers.core import unitdata23import charmhelpers.core.unitdata
2424
2525
26class OutputFormatter(object):26class OutputFormatter(object):
@@ -163,8 +163,8 @@
163 if getattr(arguments.func, '_cli_no_output', False):163 if getattr(arguments.func, '_cli_no_output', False):
164 output = ''164 output = ''
165 self.formatter.format_output(output, arguments.format)165 self.formatter.format_output(output, arguments.format)
166 if unitdata._KV:166 if charmhelpers.core.unitdata._KV:
167 unitdata._KV.flush()167 charmhelpers.core.unitdata._KV.flush()
168168
169169
170cmdline = CommandLine()170cmdline = CommandLine()
171171
=== modified file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py'
--- hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-06-17 16:59:15 +0000
+++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 2016-01-22 22:32:27 +0000
@@ -148,6 +148,13 @@
148 self.description = description148 self.description = description
149 self.check_cmd = self._locate_cmd(check_cmd)149 self.check_cmd = self._locate_cmd(check_cmd)
150150
151 def _get_check_filename(self):
152 return os.path.join(NRPE.nrpe_confdir, '{}.cfg'.format(self.command))
153
154 def _get_service_filename(self, hostname):
155 return os.path.join(NRPE.nagios_exportdir,
156 'service__{}_{}.cfg'.format(hostname, self.command))
157
151 def _locate_cmd(self, check_cmd):158 def _locate_cmd(self, check_cmd):
152 search_path = (159 search_path = (
153 '/usr/lib/nagios/plugins',160 '/usr/lib/nagios/plugins',
@@ -163,9 +170,21 @@
163 log('Check command not found: {}'.format(parts[0]))170 log('Check command not found: {}'.format(parts[0]))
164 return ''171 return ''
165172
173 def _remove_service_files(self):
174 if not os.path.exists(NRPE.nagios_exportdir):
175 return
176 for f in os.listdir(NRPE.nagios_exportdir):
177 if f.endswith('_{}.cfg'.format(self.command)):
178 os.remove(os.path.join(NRPE.nagios_exportdir, f))
179
180 def remove(self, hostname):
181 nrpe_check_file = self._get_check_filename()
182 if os.path.exists(nrpe_check_file):
183 os.remove(nrpe_check_file)
184 self._remove_service_files()
185
166 def write(self, nagios_context, hostname, nagios_servicegroups):186 def write(self, nagios_context, hostname, nagios_servicegroups):
167 nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format(187 nrpe_check_file = self._get_check_filename()
168 self.command)
169 with open(nrpe_check_file, 'w') as nrpe_check_config:188 with open(nrpe_check_file, 'w') as nrpe_check_config:
170 nrpe_check_config.write("# check {}\n".format(self.shortname))189 nrpe_check_config.write("# check {}\n".format(self.shortname))
171 nrpe_check_config.write("command[{}]={}\n".format(190 nrpe_check_config.write("command[{}]={}\n".format(
@@ -180,9 +199,7 @@
180199
181 def write_service_config(self, nagios_context, hostname,200 def write_service_config(self, nagios_context, hostname,
182 nagios_servicegroups):201 nagios_servicegroups):
183 for f in os.listdir(NRPE.nagios_exportdir):202 self._remove_service_files()
184 if re.search('.*{}.cfg'.format(self.command), f):
185 os.remove(os.path.join(NRPE.nagios_exportdir, f))
186203
187 templ_vars = {204 templ_vars = {
188 'nagios_hostname': hostname,205 'nagios_hostname': hostname,
@@ -192,8 +209,7 @@
192 'command': self.command,209 'command': self.command,
193 }210 }
194 nrpe_service_text = Check.service_template.format(**templ_vars)211 nrpe_service_text = Check.service_template.format(**templ_vars)
195 nrpe_service_file = '{}/service__{}_{}.cfg'.format(212 nrpe_service_file = self._get_service_filename(hostname)
196 NRPE.nagios_exportdir, hostname, self.command)
197 with open(nrpe_service_file, 'w') as nrpe_service_config:213 with open(nrpe_service_file, 'w') as nrpe_service_config:
198 nrpe_service_config.write(str(nrpe_service_text))214 nrpe_service_config.write(str(nrpe_service_text))
199215
@@ -218,12 +234,32 @@
218 if hostname:234 if hostname:
219 self.hostname = hostname235 self.hostname = hostname
220 else:236 else:
221 self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)237 nagios_hostname = get_nagios_hostname()
238 if nagios_hostname:
239 self.hostname = nagios_hostname
240 else:
241 self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
222 self.checks = []242 self.checks = []
223243
224 def add_check(self, *args, **kwargs):244 def add_check(self, *args, **kwargs):
225 self.checks.append(Check(*args, **kwargs))245 self.checks.append(Check(*args, **kwargs))
226246
247 def remove_check(self, *args, **kwargs):
248 if kwargs.get('shortname') is None:
249 raise ValueError('shortname of check must be specified')
250
251 # Use sensible defaults if they're not specified - these are not
252 # actually used during removal, but they're required for constructing
253 # the Check object; check_disk is chosen because it's part of the
254 # nagios-plugins-basic package.
255 if kwargs.get('check_cmd') is None:
256 kwargs['check_cmd'] = 'check_disk'
257 if kwargs.get('description') is None:
258 kwargs['description'] = ''
259
260 check = Check(*args, **kwargs)
261 check.remove(self.hostname)
262
227 def write(self):263 def write(self):
228 try:264 try:
229 nagios_uid = pwd.getpwnam('nagios').pw_uid265 nagios_uid = pwd.getpwnam('nagios').pw_uid
@@ -260,7 +296,7 @@
260 :param str relation_name: Name of relation nrpe sub joined to296 :param str relation_name: Name of relation nrpe sub joined to
261 """297 """
262 for rel in relations_of_type(relation_name):298 for rel in relations_of_type(relation_name):
263 if 'nagios_hostname' in rel:299 if 'nagios_host_context' in rel:
264 return rel['nagios_host_context']300 return rel['nagios_host_context']
265301
266302
@@ -301,11 +337,13 @@
301 upstart_init = '/etc/init/%s.conf' % svc337 upstart_init = '/etc/init/%s.conf' % svc
302 sysv_init = '/etc/init.d/%s' % svc338 sysv_init = '/etc/init.d/%s' % svc
303 if os.path.exists(upstart_init):339 if os.path.exists(upstart_init):
304 nrpe.add_check(340 # Don't add a check for these services from neutron-gateway
305 shortname=svc,341 if svc not in ['ext-port', 'os-charm-phy-nic-mtu']:
306 description='process check {%s}' % unit_name,342 nrpe.add_check(
307 check_cmd='check_upstart_job %s' % svc343 shortname=svc,
308 )344 description='process check {%s}' % unit_name,
345 check_cmd='check_upstart_job %s' % svc
346 )
309 elif os.path.exists(sysv_init):347 elif os.path.exists(sysv_init):
310 cronpath = '/etc/cron.d/nagios-service-check-%s' % svc348 cronpath = '/etc/cron.d/nagios-service-check-%s' % svc
311 cron_file = ('*/5 * * * * root '349 cron_file = ('*/5 * * * * root '
312350
=== modified file 'hooks/charmhelpers/contrib/network/ip.py'
--- hooks/charmhelpers/contrib/network/ip.py 2015-09-03 09:42:18 +0000
+++ hooks/charmhelpers/contrib/network/ip.py 2016-01-22 22:32:27 +0000
@@ -23,7 +23,7 @@
23from functools import partial23from functools import partial
2424
25from charmhelpers.core.hookenv import unit_get25from charmhelpers.core.hookenv import unit_get
26from charmhelpers.fetch import apt_install26from charmhelpers.fetch import apt_install, apt_update
27from charmhelpers.core.hookenv import (27from charmhelpers.core.hookenv import (
28 log,28 log,
29 WARNING,29 WARNING,
@@ -32,13 +32,15 @@
32try:32try:
33 import netifaces33 import netifaces
34except ImportError:34except ImportError:
35 apt_install('python-netifaces')35 apt_update(fatal=True)
36 apt_install('python-netifaces', fatal=True)
36 import netifaces37 import netifaces
3738
38try:39try:
39 import netaddr40 import netaddr
40except ImportError:41except ImportError:
41 apt_install('python-netaddr')42 apt_update(fatal=True)
43 apt_install('python-netaddr', fatal=True)
42 import netaddr44 import netaddr
4345
4446
@@ -51,7 +53,7 @@
5153
5254
53def no_ip_found_error_out(network):55def no_ip_found_error_out(network):
54 errmsg = ("No IP address found in network: %s" % network)56 errmsg = ("No IP address found in network(s): %s" % network)
55 raise ValueError(errmsg)57 raise ValueError(errmsg)
5658
5759
@@ -59,7 +61,7 @@
59 """Get an IPv4 or IPv6 address within the network from the host.61 """Get an IPv4 or IPv6 address within the network from the host.
6062
61 :param network (str): CIDR presentation format. For example,63 :param network (str): CIDR presentation format. For example,
62 '192.168.1.0/24'.64 '192.168.1.0/24'. Supports multiple networks as a space-delimited list.
63 :param fallback (str): If no address is found, return fallback.65 :param fallback (str): If no address is found, return fallback.
64 :param fatal (boolean): If no address is found, fallback is not66 :param fatal (boolean): If no address is found, fallback is not
65 set and fatal is True then exit(1).67 set and fatal is True then exit(1).
@@ -73,24 +75,26 @@
73 else:75 else:
74 return None76 return None
7577
76 _validate_cidr(network)78 networks = network.split() or [network]
77 network = netaddr.IPNetwork(network)79 for network in networks:
78 for iface in netifaces.interfaces():80 _validate_cidr(network)
79 addresses = netifaces.ifaddresses(iface)81 network = netaddr.IPNetwork(network)
80 if network.version == 4 and netifaces.AF_INET in addresses:82 for iface in netifaces.interfaces():
81 addr = addresses[netifaces.AF_INET][0]['addr']83 addresses = netifaces.ifaddresses(iface)
82 netmask = addresses[netifaces.AF_INET][0]['netmask']84 if network.version == 4 and netifaces.AF_INET in addresses:
83 cidr = netaddr.IPNetwork("%s/%s" % (addr, netmask))85 addr = addresses[netifaces.AF_INET][0]['addr']
84 if cidr in network:86 netmask = addresses[netifaces.AF_INET][0]['netmask']
85 return str(cidr.ip)87 cidr = netaddr.IPNetwork("%s/%s" % (addr, netmask))
88 if cidr in network:
89 return str(cidr.ip)
8690
87 if network.version == 6 and netifaces.AF_INET6 in addresses:91 if network.version == 6 and netifaces.AF_INET6 in addresses:
88 for addr in addresses[netifaces.AF_INET6]:92 for addr in addresses[netifaces.AF_INET6]:
89 if not addr['addr'].startswith('fe80'):93 if not addr['addr'].startswith('fe80'):
90 cidr = netaddr.IPNetwork("%s/%s" % (addr['addr'],94 cidr = netaddr.IPNetwork("%s/%s" % (addr['addr'],
91 addr['netmask']))95 addr['netmask']))
92 if cidr in network:96 if cidr in network:
93 return str(cidr.ip)97 return str(cidr.ip)
9498
95 if fallback is not None:99 if fallback is not None:
96 return fallback100 return fallback
97101
=== modified file 'hooks/charmhelpers/core/hookenv.py'
--- hooks/charmhelpers/core/hookenv.py 2015-09-03 09:42:18 +0000
+++ hooks/charmhelpers/core/hookenv.py 2016-01-22 22:32:27 +0000
@@ -491,6 +491,19 @@
491491
492492
493@cached493@cached
494def peer_relation_id():
495 '''Get the peers relation id if a peers relation has been joined, else None.'''
496 md = metadata()
497 section = md.get('peers')
498 if section:
499 for key in section:
500 relids = relation_ids(key)
501 if relids:
502 return relids[0]
503 return None
504
505
506@cached
494def relation_to_interface(relation_name):507def relation_to_interface(relation_name):
495 """508 """
496 Given the name of a relation, return the interface that relation uses.509 Given the name of a relation, return the interface that relation uses.
@@ -504,12 +517,12 @@
504def relation_to_role_and_interface(relation_name):517def relation_to_role_and_interface(relation_name):
505 """518 """
506 Given the name of a relation, return the role and the name of the interface519 Given the name of a relation, return the role and the name of the interface
507 that relation uses (where role is one of ``provides``, ``requires``, or ``peer``).520 that relation uses (where role is one of ``provides``, ``requires``, or ``peers``).
508521
509 :returns: A tuple containing ``(role, interface)``, or ``(None, None)``.522 :returns: A tuple containing ``(role, interface)``, or ``(None, None)``.
510 """523 """
511 _metadata = metadata()524 _metadata = metadata()
512 for role in ('provides', 'requires', 'peer'):525 for role in ('provides', 'requires', 'peers'):
513 interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')526 interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')
514 if interface:527 if interface:
515 return role, interface528 return role, interface
@@ -521,7 +534,7 @@
521 """534 """
522 Given a role and interface name, return a list of relation names for the535 Given a role and interface name, return a list of relation names for the
523 current charm that use that interface under that role (where role is one536 current charm that use that interface under that role (where role is one
524 of ``provides``, ``requires``, or ``peer``).537 of ``provides``, ``requires``, or ``peers``).
525538
526 :returns: A list of relation names.539 :returns: A list of relation names.
527 """540 """
@@ -542,7 +555,7 @@
542 :returns: A list of relation names.555 :returns: A list of relation names.
543 """556 """
544 results = []557 results = []
545 for role in ('provides', 'requires', 'peer'):558 for role in ('provides', 'requires', 'peers'):
546 results.extend(role_and_interface_to_relations(role, interface_name))559 results.extend(role_and_interface_to_relations(role, interface_name))
547 return results560 return results
548561
@@ -623,6 +636,38 @@
623 return unit_get('private-address')636 return unit_get('private-address')
624637
625638
639@cached
640def storage_get(attribute=None, storage_id=None):
641 """Get storage attributes"""
642 _args = ['storage-get', '--format=json']
643 if storage_id:
644 _args.extend(('-s', storage_id))
645 if attribute:
646 _args.append(attribute)
647 try:
648 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
649 except ValueError:
650 return None
651
652
653@cached
654def storage_list(storage_name=None):
655 """List the storage IDs for the unit"""
656 _args = ['storage-list', '--format=json']
657 if storage_name:
658 _args.append(storage_name)
659 try:
660 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
661 except ValueError:
662 return None
663 except OSError as e:
664 import errno
665 if e.errno == errno.ENOENT:
666 # storage-list does not exist
667 return []
668 raise
669
670
626class UnregisteredHookError(Exception):671class UnregisteredHookError(Exception):
627 """Raised when an undefined hook is called"""672 """Raised when an undefined hook is called"""
628 pass673 pass
@@ -788,6 +833,7 @@
788833
789def translate_exc(from_exc, to_exc):834def translate_exc(from_exc, to_exc):
790 def inner_translate_exc1(f):835 def inner_translate_exc1(f):
836 @wraps(f)
791 def inner_translate_exc2(*args, **kwargs):837 def inner_translate_exc2(*args, **kwargs):
792 try:838 try:
793 return f(*args, **kwargs)839 return f(*args, **kwargs)
@@ -832,6 +878,40 @@
832 subprocess.check_call(cmd)878 subprocess.check_call(cmd)
833879
834880
881@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
882def payload_register(ptype, klass, pid):
883 """ is used while a hook is running to let Juju know that a
884 payload has been started."""
885 cmd = ['payload-register']
886 for x in [ptype, klass, pid]:
887 cmd.append(x)
888 subprocess.check_call(cmd)
889
890
891@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
892def payload_unregister(klass, pid):
893 """ is used while a hook is running to let Juju know
894 that a payload has been manually stopped. The <class> and <id> provided
895 must match a payload that has been previously registered with juju using
896 payload-register."""
897 cmd = ['payload-unregister']
898 for x in [klass, pid]:
899 cmd.append(x)
900 subprocess.check_call(cmd)
901
902
903@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
904def payload_status_set(klass, pid, status):
905 """is used to update the current status of a registered payload.
906 The <class> and <id> provided must match a payload that has been previously
907 registered with juju using payload-register. The <status> must be one of the
908 follow: starting, started, stopping, stopped"""
909 cmd = ['payload-status-set']
910 for x in [klass, pid, status]:
911 cmd.append(x)
912 subprocess.check_call(cmd)
913
914
835@cached915@cached
836def juju_version():916def juju_version():
837 """Full version string (eg. '1.23.3.1-trusty-amd64')"""917 """Full version string (eg. '1.23.3.1-trusty-amd64')"""
838918
=== modified file 'hooks/charmhelpers/core/host.py'
--- hooks/charmhelpers/core/host.py 2015-08-19 13:50:42 +0000
+++ hooks/charmhelpers/core/host.py 2016-01-22 22:32:27 +0000
@@ -63,55 +63,86 @@
63 return service_result63 return service_result
6464
6565
66def service_pause(service_name, init_dir=None):66def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):
67 """Pause a system service.67 """Pause a system service.
6868
69 Stop it, and prevent it from starting again at boot."""69 Stop it, and prevent it from starting again at boot."""
70 if init_dir is None:70 stopped = True
71 init_dir = "/etc/init"71 if service_running(service_name):
72 stopped = service_stop(service_name)72 stopped = service_stop(service_name)
73 # XXX: Support systemd too73 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
74 override_path = os.path.join(74 sysv_file = os.path.join(initd_dir, service_name)
75 init_dir, '{}.override'.format(service_name))75 if init_is_systemd():
76 with open(override_path, 'w') as fh:76 service('disable', service_name)
77 fh.write("manual\n")77 elif os.path.exists(upstart_file):
78 override_path = os.path.join(
79 init_dir, '{}.override'.format(service_name))
80 with open(override_path, 'w') as fh:
81 fh.write("manual\n")
82 elif os.path.exists(sysv_file):
83 subprocess.check_call(["update-rc.d", service_name, "disable"])
84 else:
85 raise ValueError(
86 "Unable to detect {0} as SystemD, Upstart {1} or"
87 " SysV {2}".format(
88 service_name, upstart_file, sysv_file))
78 return stopped89 return stopped
7990
8091
81def service_resume(service_name, init_dir=None):92def service_resume(service_name, init_dir="/etc/init",
93 initd_dir="/etc/init.d"):
82 """Resume a system service.94 """Resume a system service.
8395
84 Reenable starting again at boot. Start the service"""96 Reenable starting again at boot. Start the service"""
85 # XXX: Support systemd too97 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
86 if init_dir is None:98 sysv_file = os.path.join(initd_dir, service_name)
87 init_dir = "/etc/init"99 if init_is_systemd():
88 override_path = os.path.join(100 service('enable', service_name)
89 init_dir, '{}.override'.format(service_name))101 elif os.path.exists(upstart_file):
90 if os.path.exists(override_path):102 override_path = os.path.join(
91 os.unlink(override_path)103 init_dir, '{}.override'.format(service_name))
92 started = service_start(service_name)104 if os.path.exists(override_path):
105 os.unlink(override_path)
106 elif os.path.exists(sysv_file):
107 subprocess.check_call(["update-rc.d", service_name, "enable"])
108 else:
109 raise ValueError(
110 "Unable to detect {0} as SystemD, Upstart {1} or"
111 " SysV {2}".format(
112 service_name, upstart_file, sysv_file))
113
114 started = service_running(service_name)
115 if not started:
116 started = service_start(service_name)
93 return started117 return started
94118
95119
96def service(action, service_name):120def service(action, service_name):
97 """Control a system service"""121 """Control a system service"""
98 cmd = ['service', service_name, action]122 if init_is_systemd():
123 cmd = ['systemctl', action, service_name]
124 else:
125 cmd = ['service', service_name, action]
99 return subprocess.call(cmd) == 0126 return subprocess.call(cmd) == 0
100127
101128
102def service_running(service):129def service_running(service_name):
103 """Determine whether a system service is running"""130 """Determine whether a system service is running"""
104 try:131 if init_is_systemd():
105 output = subprocess.check_output(132 return service('is-active', service_name)
106 ['service', service, 'status'],
107 stderr=subprocess.STDOUT).decode('UTF-8')
108 except subprocess.CalledProcessError:
109 return False
110 else:133 else:
111 if ("start/running" in output or "is running" in output):134 try:
112 return True135 output = subprocess.check_output(
113 else:136 ['service', service_name, 'status'],
137 stderr=subprocess.STDOUT).decode('UTF-8')
138 except subprocess.CalledProcessError:
114 return False139 return False
140 else:
141 if ("start/running" in output or "is running" in output or
142 "up and running" in output):
143 return True
144 else:
145 return False
115146
116147
117def service_available(service_name):148def service_available(service_name):
@@ -126,8 +157,29 @@
126 return True157 return True
127158
128159
129def adduser(username, password=None, shell='/bin/bash', system_user=False):160SYSTEMD_SYSTEM = '/run/systemd/system'
130 """Add a user to the system"""161
162
163def init_is_systemd():
164 """Return True if the host system uses systemd, False otherwise."""
165 return os.path.isdir(SYSTEMD_SYSTEM)
166
167
168def adduser(username, password=None, shell='/bin/bash', system_user=False,
169 primary_group=None, secondary_groups=None):
170 """Add a user to the system.
171
172 Will log but otherwise succeed if the user already exists.
173
174 :param str username: Username to create
175 :param str password: Password for user; if ``None``, create a system user
176 :param str shell: The default shell for the user
177 :param bool system_user: Whether to create a login or system user
178 :param str primary_group: Primary group for user; defaults to username
179 :param list secondary_groups: Optional list of additional groups
180
181 :returns: The password database entry struct, as returned by `pwd.getpwnam`
182 """
131 try:183 try:
132 user_info = pwd.getpwnam(username)184 user_info = pwd.getpwnam(username)
133 log('user {0} already exists!'.format(username))185 log('user {0} already exists!'.format(username))
@@ -142,6 +194,16 @@
142 '--shell', shell,194 '--shell', shell,
143 '--password', password,195 '--password', password,
144 ])196 ])
197 if not primary_group:
198 try:
199 grp.getgrnam(username)
200 primary_group = username # avoid "group exists" error
201 except KeyError:
202 pass
203 if primary_group:
204 cmd.extend(['-g', primary_group])
205 if secondary_groups:
206 cmd.extend(['-G', ','.join(secondary_groups)])
145 cmd.append(username)207 cmd.append(username)
146 subprocess.check_call(cmd)208 subprocess.check_call(cmd)
147 user_info = pwd.getpwnam(username)209 user_info = pwd.getpwnam(username)
@@ -239,14 +301,12 @@
239301
240302
241def fstab_remove(mp):303def fstab_remove(mp):
242 """Remove the given mountpoint entry from /etc/fstab304 """Remove the given mountpoint entry from /etc/fstab"""
243 """
244 return Fstab.remove_by_mountpoint(mp)305 return Fstab.remove_by_mountpoint(mp)
245306
246307
247def fstab_add(dev, mp, fs, options=None):308def fstab_add(dev, mp, fs, options=None):
248 """Adds the given device entry to the /etc/fstab file309 """Adds the given device entry to the /etc/fstab file"""
249 """
250 return Fstab.add(dev, mp, fs, options=options)310 return Fstab.add(dev, mp, fs, options=options)
251311
252312
@@ -302,8 +362,7 @@
302362
303363
304def file_hash(path, hash_type='md5'):364def file_hash(path, hash_type='md5'):
305 """365 """Generate a hash checksum of the contents of 'path' or None if not found.
306 Generate a hash checksum of the contents of 'path' or None if not found.
307366
308 :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,367 :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
309 such as md5, sha1, sha256, sha512, etc.368 such as md5, sha1, sha256, sha512, etc.
@@ -318,10 +377,9 @@
318377
319378
320def path_hash(path):379def path_hash(path):
321 """380 """Generate a hash checksum of all files matching 'path'. Standard
322 Generate a hash checksum of all files matching 'path'. Standard wildcards381 wildcards like '*' and '?' are supported, see documentation for the 'glob'
323 like '*' and '?' are supported, see documentation for the 'glob' module for382 module for more information.
324 more information.
325383
326 :return: dict: A { filename: hash } dictionary for all matched files.384 :return: dict: A { filename: hash } dictionary for all matched files.
327 Empty if none found.385 Empty if none found.
@@ -333,8 +391,7 @@
333391
334392
335def check_hash(path, checksum, hash_type='md5'):393def check_hash(path, checksum, hash_type='md5'):
336 """394 """Validate a file using a cryptographic checksum.
337 Validate a file using a cryptographic checksum.
338395
339 :param str checksum: Value of the checksum used to validate the file.396 :param str checksum: Value of the checksum used to validate the file.
340 :param str hash_type: Hash algorithm used to generate `checksum`.397 :param str hash_type: Hash algorithm used to generate `checksum`.
@@ -349,6 +406,7 @@
349406
350407
351class ChecksumError(ValueError):408class ChecksumError(ValueError):
409 """A class derived from Value error to indicate the checksum failed."""
352 pass410 pass
353411
354412
@@ -454,7 +512,7 @@
454512
455513
456def list_nics(nic_type=None):514def list_nics(nic_type=None):
457 '''Return a list of nics of given type(s)'''515 """Return a list of nics of given type(s)"""
458 if isinstance(nic_type, six.string_types):516 if isinstance(nic_type, six.string_types):
459 int_types = [nic_type]517 int_types = [nic_type]
460 else:518 else:
@@ -496,12 +554,13 @@
496554
497555
498def set_nic_mtu(nic, mtu):556def set_nic_mtu(nic, mtu):
499 '''Set MTU on a network interface'''557 """Set the Maximum Transmission Unit (MTU) on a network interface."""
500 cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]558 cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
501 subprocess.check_call(cmd)559 subprocess.check_call(cmd)
502560
503561
504def get_nic_mtu(nic):562def get_nic_mtu(nic):
563 """Return the Maximum Transmission Unit (MTU) for a network interface."""
505 cmd = ['ip', 'addr', 'show', nic]564 cmd = ['ip', 'addr', 'show', nic]
506 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')565 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
507 mtu = ""566 mtu = ""
@@ -513,6 +572,7 @@
513572
514573
515def get_nic_hwaddr(nic):574def get_nic_hwaddr(nic):
575 """Return the Media Access Control (MAC) for a network interface."""
516 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]576 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
517 ip_output = subprocess.check_output(cmd).decode('UTF-8')577 ip_output = subprocess.check_output(cmd).decode('UTF-8')
518 hwaddr = ""578 hwaddr = ""
@@ -523,7 +583,7 @@
523583
524584
525def cmp_pkgrevno(package, revno, pkgcache=None):585def cmp_pkgrevno(package, revno, pkgcache=None):
526 '''Compare supplied revno with the revno of the installed package586 """Compare supplied revno with the revno of the installed package
527587
528 * 1 => Installed revno is greater than supplied arg588 * 1 => Installed revno is greater than supplied arg
529 * 0 => Installed revno is the same as supplied arg589 * 0 => Installed revno is the same as supplied arg
@@ -532,7 +592,7 @@
532 This function imports apt_cache function from charmhelpers.fetch if592 This function imports apt_cache function from charmhelpers.fetch if
533 the pkgcache argument is None. Be sure to add charmhelpers.fetch if593 the pkgcache argument is None. Be sure to add charmhelpers.fetch if
534 you call this function, or pass an apt_pkg.Cache() instance.594 you call this function, or pass an apt_pkg.Cache() instance.
535 '''595 """
536 import apt_pkg596 import apt_pkg
537 if not pkgcache:597 if not pkgcache:
538 from charmhelpers.fetch import apt_cache598 from charmhelpers.fetch import apt_cache
@@ -542,15 +602,30 @@
542602
543603
544@contextmanager604@contextmanager
545def chdir(d):605def chdir(directory):
606 """Change the current working directory to a different directory for a code
607 block and return the previous directory after the block exits. Useful to
608 run commands from a specificed directory.
609
610 :param str directory: The directory path to change to for this context.
611 """
546 cur = os.getcwd()612 cur = os.getcwd()
547 try:613 try:
548 yield os.chdir(d)614 yield os.chdir(directory)
549 finally:615 finally:
550 os.chdir(cur)616 os.chdir(cur)
551617
552618
553def chownr(path, owner, group, follow_links=True):619def chownr(path, owner, group, follow_links=True, chowntopdir=False):
620 """Recursively change user and group ownership of files and directories
621 in given path. Doesn't chown path itself by default, only its children.
622
623 :param str path: The string path to start changing ownership.
624 :param str owner: The owner string to use when looking up the uid.
625 :param str group: The group string to use when looking up the gid.
626 :param bool follow_links: Also Chown links if True
627 :param bool chowntopdir: Also chown path itself if True
628 """
554 uid = pwd.getpwnam(owner).pw_uid629 uid = pwd.getpwnam(owner).pw_uid
555 gid = grp.getgrnam(group).gr_gid630 gid = grp.getgrnam(group).gr_gid
556 if follow_links:631 if follow_links:
@@ -558,6 +633,10 @@
558 else:633 else:
559 chown = os.lchown634 chown = os.lchown
560635
636 if chowntopdir:
637 broken_symlink = os.path.lexists(path) and not os.path.exists(path)
638 if not broken_symlink:
639 chown(path, uid, gid)
561 for root, dirs, files in os.walk(path):640 for root, dirs, files in os.walk(path):
562 for name in dirs + files:641 for name in dirs + files:
563 full = os.path.join(root, name)642 full = os.path.join(root, name)
@@ -567,4 +646,28 @@
567646
568647
569def lchownr(path, owner, group):648def lchownr(path, owner, group):
649 """Recursively change user and group ownership of files and directories
650 in a given path, not following symbolic links. See the documentation for
651 'os.lchown' for more information.
652
653 :param str path: The string path to start changing ownership.
654 :param str owner: The owner string to use when looking up the uid.
655 :param str group: The group string to use when looking up the gid.
656 """
570 chownr(path, owner, group, follow_links=False)657 chownr(path, owner, group, follow_links=False)
658
659
660def get_total_ram():
661 """The total amount of system RAM in bytes.
662
663 This is what is reported by the OS, and may be overcommitted when
664 there are multiple containers hosted on the same machine.
665 """
666 with open('/proc/meminfo', 'r') as f:
667 for line in f.readlines():
668 if line:
669 key, value, unit = line.split()
670 if key == 'MemTotal:':
671 assert unit == 'kB', 'Unknown unit'
672 return int(value) * 1024 # Classic, not KiB.
673 raise NotImplementedError()
571674
=== modified file 'hooks/charmhelpers/core/hugepage.py'
--- hooks/charmhelpers/core/hugepage.py 2015-08-19 13:50:42 +0000
+++ hooks/charmhelpers/core/hugepage.py 2016-01-22 22:32:27 +0000
@@ -25,11 +25,13 @@
25 fstab_mount,25 fstab_mount,
26 mkdir,26 mkdir,
27)27)
28from charmhelpers.core.strutils import bytes_from_string
29from subprocess import check_output
2830
2931
30def hugepage_support(user, group='hugetlb', nr_hugepages=256,32def hugepage_support(user, group='hugetlb', nr_hugepages=256,
31 max_map_count=65536, mnt_point='/run/hugepages/kvm',33 max_map_count=65536, mnt_point='/run/hugepages/kvm',
32 pagesize='2MB', mount=True):34 pagesize='2MB', mount=True, set_shmmax=False):
33 """Enable hugepages on system.35 """Enable hugepages on system.
3436
35 Args:37 Args:
@@ -44,11 +46,18 @@
44 group_info = add_group(group)46 group_info = add_group(group)
45 gid = group_info.gr_gid47 gid = group_info.gr_gid
46 add_user_to_group(user, group)48 add_user_to_group(user, group)
49 if max_map_count < 2 * nr_hugepages:
50 max_map_count = 2 * nr_hugepages
47 sysctl_settings = {51 sysctl_settings = {
48 'vm.nr_hugepages': nr_hugepages,52 'vm.nr_hugepages': nr_hugepages,
49 'vm.max_map_count': max_map_count,53 'vm.max_map_count': max_map_count,
50 'vm.hugetlb_shm_group': gid,54 'vm.hugetlb_shm_group': gid,
51 }55 }
56 if set_shmmax:
57 shmmax_current = int(check_output(['sysctl', '-n', 'kernel.shmmax']))
58 shmmax_minsize = bytes_from_string(pagesize) * nr_hugepages
59 if shmmax_minsize > shmmax_current:
60 sysctl_settings['kernel.shmmax'] = shmmax_minsize
52 sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf')61 sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf')
53 mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False)62 mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False)
54 lfstab = fstab.Fstab()63 lfstab = fstab.Fstab()
5564
=== added file 'hooks/charmhelpers/core/kernel.py'
--- hooks/charmhelpers/core/kernel.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/kernel.py 2016-01-22 22:32:27 +0000
@@ -0,0 +1,68 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# Copyright 2014-2015 Canonical Limited.
5#
6# This file is part of charm-helpers.
7#
8# charm-helpers is free software: you can redistribute it and/or modify
9# it under the terms of the GNU Lesser General Public License version 3 as
10# published by the Free Software Foundation.
11#
12# charm-helpers is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Lesser General Public License for more details.
16#
17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
19
20__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
21
22from charmhelpers.core.hookenv import (
23 log,
24 INFO
25)
26
27from subprocess import check_call, check_output
28import re
29
30
31def modprobe(module, persist=True):
32 """Load a kernel module and configure for auto-load on reboot."""
33 cmd = ['modprobe', module]
34
35 log('Loading kernel module %s' % module, level=INFO)
36
37 check_call(cmd)
38 if persist:
39 with open('/etc/modules', 'r+') as modules:
40 if module not in modules.read():
41 modules.write(module)
42
43
44def rmmod(module, force=False):
45 """Remove a module from the linux kernel"""
46 cmd = ['rmmod']
47 if force:
48 cmd.append('-f')
49 cmd.append(module)
50 log('Removing kernel module %s' % module, level=INFO)
51 return check_call(cmd)
52
53
54def lsmod():
55 """Shows what kernel modules are currently loaded"""
56 return check_output(['lsmod'],
57 universal_newlines=True)
58
59
60def is_module_loaded(module):
61 """Checks if a kernel module is already loaded"""
62 matches = re.findall('^%s[ ]+' % module, lsmod(), re.M)
63 return len(matches) > 0
64
65
66def update_initramfs(version='all'):
67 """Updates an initramfs image"""
68 return check_call(["update-initramfs", "-k", version, "-u"])
069
=== modified file 'hooks/charmhelpers/core/services/helpers.py'
--- hooks/charmhelpers/core/services/helpers.py 2015-08-19 13:50:42 +0000
+++ hooks/charmhelpers/core/services/helpers.py 2016-01-22 22:32:27 +0000
@@ -243,33 +243,40 @@
243 :param str source: The template source file, relative to243 :param str source: The template source file, relative to
244 `$CHARM_DIR/templates`244 `$CHARM_DIR/templates`
245245
246 :param str target: The target to write the rendered template to246 :param str target: The target to write the rendered template to (or None)
247 :param str owner: The owner of the rendered file247 :param str owner: The owner of the rendered file
248 :param str group: The group of the rendered file248 :param str group: The group of the rendered file
249 :param int perms: The permissions of the rendered file249 :param int perms: The permissions of the rendered file
250 :param partial on_change_action: functools partial to be executed when250 :param partial on_change_action: functools partial to be executed when
251 rendered file changes251 rendered file changes
252 :param jinja2 loader template_loader: A jinja2 template loader
253
254 :return str: The rendered template
252 """255 """
253 def __init__(self, source, target,256 def __init__(self, source, target,
254 owner='root', group='root', perms=0o444,257 owner='root', group='root', perms=0o444,
255 on_change_action=None):258 on_change_action=None, template_loader=None):
256 self.source = source259 self.source = source
257 self.target = target260 self.target = target
258 self.owner = owner261 self.owner = owner
259 self.group = group262 self.group = group
260 self.perms = perms263 self.perms = perms
261 self.on_change_action = on_change_action264 self.on_change_action = on_change_action
265 self.template_loader = template_loader
262266
263 def __call__(self, manager, service_name, event_name):267 def __call__(self, manager, service_name, event_name):
264 pre_checksum = ''268 pre_checksum = ''
265 if self.on_change_action and os.path.isfile(self.target):269 if self.on_change_action and os.path.isfile(self.target):
266 pre_checksum = host.file_hash(self.target)270 pre_checksum = host.file_hash(self.target)
267 service = manager.get_service(service_name)271 service = manager.get_service(service_name)
268 context = {}272 context = {'ctx': {}}
269 for ctx in service.get('required_data', []):273 for ctx in service.get('required_data', []):
270 context.update(ctx)274 context.update(ctx)
271 templating.render(self.source, self.target, context,275 context['ctx'].update(ctx)
272 self.owner, self.group, self.perms)276
277 result = templating.render(self.source, self.target, context,
278 self.owner, self.group, self.perms,
279 template_loader=self.template_loader)
273 if self.on_change_action:280 if self.on_change_action:
274 if pre_checksum == host.file_hash(self.target):281 if pre_checksum == host.file_hash(self.target):
275 hookenv.log(282 hookenv.log(
@@ -278,6 +285,8 @@
278 else:285 else:
279 self.on_change_action()286 self.on_change_action()
280287
288 return result
289
281290
282# Convenience aliases for templates291# Convenience aliases for templates
283render_template = template = TemplateCallback292render_template = template = TemplateCallback
284293
=== modified file 'hooks/charmhelpers/core/strutils.py'
--- hooks/charmhelpers/core/strutils.py 2015-04-16 21:32:48 +0000
+++ hooks/charmhelpers/core/strutils.py 2016-01-22 22:32:27 +0000
@@ -18,6 +18,7 @@
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1919
20import six20import six
21import re
2122
2223
23def bool_from_string(value):24def bool_from_string(value):
@@ -40,3 +41,32 @@
4041
41 msg = "Unable to interpret string value '%s' as boolean" % (value)42 msg = "Unable to interpret string value '%s' as boolean" % (value)
42 raise ValueError(msg)43 raise ValueError(msg)
44
45
46def bytes_from_string(value):
47 """Interpret human readable string value as bytes.
48
49 Returns int
50 """
51 BYTE_POWER = {
52 'K': 1,
53 'KB': 1,
54 'M': 2,
55 'MB': 2,
56 'G': 3,
57 'GB': 3,
58 'T': 4,
59 'TB': 4,
60 'P': 5,
61 'PB': 5,
62 }
63 if isinstance(value, six.string_types):
64 value = six.text_type(value)
65 else:
66 msg = "Unable to interpret non-string value '%s' as boolean" % (value)
67 raise ValueError(msg)
68 matches = re.match("([0-9]+)([a-zA-Z]+)", value)
69 if not matches:
70 msg = "Unable to interpret string value '%s' as bytes" % (value)
71 raise ValueError(msg)
72 return int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)])
4373
=== modified file 'hooks/charmhelpers/core/templating.py'
--- hooks/charmhelpers/core/templating.py 2015-02-26 13:37:18 +0000
+++ hooks/charmhelpers/core/templating.py 2016-01-22 22:32:27 +0000
@@ -21,13 +21,14 @@
2121
2222
23def render(source, target, context, owner='root', group='root',23def render(source, target, context, owner='root', group='root',
24 perms=0o444, templates_dir=None, encoding='UTF-8'):24 perms=0o444, templates_dir=None, encoding='UTF-8', template_loader=None):
25 """25 """
26 Render a template.26 Render a template.
2727
28 The `source` path, if not absolute, is relative to the `templates_dir`.28 The `source` path, if not absolute, is relative to the `templates_dir`.
2929
30 The `target` path should be absolute.30 The `target` path should be absolute. It can also be `None`, in which
31 case no file will be written.
3132
32 The context should be a dict containing the values to be replaced in the33 The context should be a dict containing the values to be replaced in the
33 template.34 template.
@@ -36,6 +37,9 @@
3637
37 If omitted, `templates_dir` defaults to the `templates` folder in the charm.38 If omitted, `templates_dir` defaults to the `templates` folder in the charm.
3839
40 The rendered template will be written to the file as well as being returned
41 as a string.
42
39 Note: Using this requires python-jinja2; if it is not installed, calling43 Note: Using this requires python-jinja2; if it is not installed, calling
40 this will attempt to use charmhelpers.fetch.apt_install to install it.44 this will attempt to use charmhelpers.fetch.apt_install to install it.
41 """45 """
@@ -52,17 +56,26 @@
52 apt_install('python-jinja2', fatal=True)56 apt_install('python-jinja2', fatal=True)
53 from jinja2 import FileSystemLoader, Environment, exceptions57 from jinja2 import FileSystemLoader, Environment, exceptions
5458
55 if templates_dir is None:59 if template_loader:
56 templates_dir = os.path.join(hookenv.charm_dir(), 'templates')60 template_env = Environment(loader=template_loader)
57 loader = Environment(loader=FileSystemLoader(templates_dir))61 else:
62 if templates_dir is None:
63 templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
64 template_env = Environment(loader=FileSystemLoader(templates_dir))
58 try:65 try:
59 source = source66 source = source
60 template = loader.get_template(source)67 template = template_env.get_template(source)
61 except exceptions.TemplateNotFound as e:68 except exceptions.TemplateNotFound as e:
62 hookenv.log('Could not load template %s from %s.' %69 hookenv.log('Could not load template %s from %s.' %
63 (source, templates_dir),70 (source, templates_dir),
64 level=hookenv.ERROR)71 level=hookenv.ERROR)
65 raise e72 raise e
66 content = template.render(context)73 content = template.render(context)
67 host.mkdir(os.path.dirname(target), owner, group, perms=0o755)74 if target is not None:
68 host.write_file(target, content.encode(encoding), owner, group, perms)75 target_dir = os.path.dirname(target)
76 if not os.path.exists(target_dir):
77 # This is a terrible default directory permission, as the file
78 # or its siblings will often contain secrets.
79 host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
80 host.write_file(target, content.encode(encoding), owner, group, perms)
81 return content
6982
=== modified file 'hooks/charmhelpers/fetch/__init__.py'
--- hooks/charmhelpers/fetch/__init__.py 2015-08-19 13:50:42 +0000
+++ hooks/charmhelpers/fetch/__init__.py 2016-01-22 22:32:27 +0000
@@ -98,6 +98,14 @@
98 'liberty/proposed': 'trusty-proposed/liberty',98 'liberty/proposed': 'trusty-proposed/liberty',
99 'trusty-liberty/proposed': 'trusty-proposed/liberty',99 'trusty-liberty/proposed': 'trusty-proposed/liberty',
100 'trusty-proposed/liberty': 'trusty-proposed/liberty',100 'trusty-proposed/liberty': 'trusty-proposed/liberty',
101 # Mitaka
102 'mitaka': 'trusty-updates/mitaka',
103 'trusty-mitaka': 'trusty-updates/mitaka',
104 'trusty-mitaka/updates': 'trusty-updates/mitaka',
105 'trusty-updates/mitaka': 'trusty-updates/mitaka',
106 'mitaka/proposed': 'trusty-proposed/mitaka',
107 'trusty-mitaka/proposed': 'trusty-proposed/mitaka',
108 'trusty-proposed/mitaka': 'trusty-proposed/mitaka',
101}109}
102110
103# The order of this list is very important. Handlers should be listed in from111# The order of this list is very important. Handlers should be listed in from
@@ -225,12 +233,12 @@
225233
226def apt_mark(packages, mark, fatal=False):234def apt_mark(packages, mark, fatal=False):
227 """Flag one or more packages using apt-mark"""235 """Flag one or more packages using apt-mark"""
236 log("Marking {} as {}".format(packages, mark))
228 cmd = ['apt-mark', mark]237 cmd = ['apt-mark', mark]
229 if isinstance(packages, six.string_types):238 if isinstance(packages, six.string_types):
230 cmd.append(packages)239 cmd.append(packages)
231 else:240 else:
232 cmd.extend(packages)241 cmd.extend(packages)
233 log("Holding {}".format(packages))
234242
235 if fatal:243 if fatal:
236 subprocess.check_call(cmd, universal_newlines=True)244 subprocess.check_call(cmd, universal_newlines=True)
@@ -411,7 +419,7 @@
411 importlib.import_module(package),419 importlib.import_module(package),
412 classname)420 classname)
413 plugin_list.append(handler_class())421 plugin_list.append(handler_class())
414 except (ImportError, AttributeError):422 except NotImplementedError:
415 # Skip missing plugins so that they can be ommitted from423 # Skip missing plugins so that they can be ommitted from
416 # installation if desired424 # installation if desired
417 log("FetchHandler {} not found, skipping plugin".format(425 log("FetchHandler {} not found, skipping plugin".format(
418426
=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
--- hooks/charmhelpers/fetch/archiveurl.py 2015-08-03 14:53:05 +0000
+++ hooks/charmhelpers/fetch/archiveurl.py 2016-01-22 22:32:27 +0000
@@ -108,7 +108,7 @@
108 install_opener(opener)108 install_opener(opener)
109 response = urlopen(source)109 response = urlopen(source)
110 try:110 try:
111 with open(dest, 'w') as dest_file:111 with open(dest, 'wb') as dest_file:
112 dest_file.write(response.read())112 dest_file.write(response.read())
113 except Exception as e:113 except Exception as e:
114 if os.path.isfile(dest):114 if os.path.isfile(dest):
115115
=== modified file 'hooks/charmhelpers/fetch/bzrurl.py'
--- hooks/charmhelpers/fetch/bzrurl.py 2015-01-26 11:51:28 +0000
+++ hooks/charmhelpers/fetch/bzrurl.py 2016-01-22 22:32:27 +0000
@@ -15,60 +15,50 @@
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1616
17import os17import os
18from subprocess import check_call
18from charmhelpers.fetch import (19from charmhelpers.fetch import (
19 BaseFetchHandler,20 BaseFetchHandler,
20 UnhandledSource21 UnhandledSource,
22 filter_installed_packages,
23 apt_install,
21)24)
22from charmhelpers.core.host import mkdir25from charmhelpers.core.host import mkdir
2326
24import six
25if six.PY3:
26 raise ImportError('bzrlib does not support Python3')
2727
28try:28if filter_installed_packages(['bzr']) != []:
29 from bzrlib.branch import Branch29 apt_install(['bzr'])
30 from bzrlib import bzrdir, workingtree, errors30 if filter_installed_packages(['bzr']) != []:
31except ImportError:31 raise NotImplementedError('Unable to install bzr')
32 from charmhelpers.fetch import apt_install
33 apt_install("python-bzrlib")
34 from bzrlib.branch import Branch
35 from bzrlib import bzrdir, workingtree, errors
3632
3733
38class BzrUrlFetchHandler(BaseFetchHandler):34class BzrUrlFetchHandler(BaseFetchHandler):
39 """Handler for bazaar branches via generic and lp URLs"""35 """Handler for bazaar branches via generic and lp URLs"""
40 def can_handle(self, source):36 def can_handle(self, source):
41 url_parts = self.parse_url(source)37 url_parts = self.parse_url(source)
42 if url_parts.scheme not in ('bzr+ssh', 'lp'):38 if url_parts.scheme not in ('bzr+ssh', 'lp', ''):
43 return False39 return False
40 elif not url_parts.scheme:
41 return os.path.exists(os.path.join(source, '.bzr'))
44 else:42 else:
45 return True43 return True
4644
47 def branch(self, source, dest):45 def branch(self, source, dest):
48 url_parts = self.parse_url(source)
49 # If we use lp:branchname scheme we need to load plugins
50 if not self.can_handle(source):46 if not self.can_handle(source):
51 raise UnhandledSource("Cannot handle {}".format(source))47 raise UnhandledSource("Cannot handle {}".format(source))
52 if url_parts.scheme == "lp":48 if os.path.exists(dest):
53 from bzrlib.plugin import load_plugins49 check_call(['bzr', 'pull', '--overwrite', '-d', dest, source])
54 load_plugins()50 else:
55 try:51 check_call(['bzr', 'branch', source, dest])
56 local_branch = bzrdir.BzrDir.create_branch_convenience(dest)
57 except errors.AlreadyControlDirError:
58 local_branch = Branch.open(dest)
59 try:
60 remote_branch = Branch.open(source)
61 remote_branch.push(local_branch)
62 tree = workingtree.WorkingTree.open(dest)
63 tree.update()
64 except Exception as e:
65 raise e
6652
67 def install(self, source):53 def install(self, source, dest=None):
68 url_parts = self.parse_url(source)54 url_parts = self.parse_url(source)
69 branch_name = url_parts.path.strip("/").split("/")[-1]55 branch_name = url_parts.path.strip("/").split("/")[-1]
70 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",56 if dest:
71 branch_name)57 dest_dir = os.path.join(dest, branch_name)
58 else:
59 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
60 branch_name)
61
72 if not os.path.exists(dest_dir):62 if not os.path.exists(dest_dir):
73 mkdir(dest_dir, perms=0o755)63 mkdir(dest_dir, perms=0o755)
74 try:64 try:
7565
=== modified file 'hooks/charmhelpers/fetch/giturl.py'
--- hooks/charmhelpers/fetch/giturl.py 2015-08-03 14:53:05 +0000
+++ hooks/charmhelpers/fetch/giturl.py 2016-01-22 22:32:27 +0000
@@ -15,24 +15,18 @@
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1616
17import os17import os
18from subprocess import check_call, CalledProcessError
18from charmhelpers.fetch import (19from charmhelpers.fetch import (
19 BaseFetchHandler,20 BaseFetchHandler,
20 UnhandledSource21 UnhandledSource,
22 filter_installed_packages,
23 apt_install,
21)24)
22from charmhelpers.core.host import mkdir25
2326if filter_installed_packages(['git']) != []:
24import six27 apt_install(['git'])
25if six.PY3:28 if filter_installed_packages(['git']) != []:
26 raise ImportError('GitPython does not support Python 3')29 raise NotImplementedError('Unable to install git')
27
28try:
29 from git import Repo
30except ImportError:
31 from charmhelpers.fetch import apt_install
32 apt_install("python-git")
33 from git import Repo
34
35from git.exc import GitCommandError # noqa E402
3630
3731
38class GitUrlFetchHandler(BaseFetchHandler):32class GitUrlFetchHandler(BaseFetchHandler):
@@ -40,19 +34,24 @@
40 def can_handle(self, source):34 def can_handle(self, source):
41 url_parts = self.parse_url(source)35 url_parts = self.parse_url(source)
42 # TODO (mattyw) no support for ssh git@ yet36 # TODO (mattyw) no support for ssh git@ yet
43 if url_parts.scheme not in ('http', 'https', 'git'):37 if url_parts.scheme not in ('http', 'https', 'git', ''):
44 return False38 return False
39 elif not url_parts.scheme:
40 return os.path.exists(os.path.join(source, '.git'))
45 else:41 else:
46 return True42 return True
4743
48 def clone(self, source, dest, branch, depth=None):44 def clone(self, source, dest, branch="master", depth=None):
49 if not self.can_handle(source):45 if not self.can_handle(source):
50 raise UnhandledSource("Cannot handle {}".format(source))46 raise UnhandledSource("Cannot handle {}".format(source))
5147
52 if depth:48 if os.path.exists(dest):
53 Repo.clone_from(source, dest, branch=branch, depth=depth)49 cmd = ['git', '-C', dest, 'pull', source, branch]
54 else:50 else:
55 Repo.clone_from(source, dest, branch=branch)51 cmd = ['git', 'clone', source, dest, '--branch', branch]
52 if depth:
53 cmd.extend(['--depth', depth])
54 check_call(cmd)
5655
57 def install(self, source, branch="master", dest=None, depth=None):56 def install(self, source, branch="master", dest=None, depth=None):
58 url_parts = self.parse_url(source)57 url_parts = self.parse_url(source)
@@ -62,11 +61,9 @@
62 else:61 else:
63 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",62 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
64 branch_name)63 branch_name)
65 if not os.path.exists(dest_dir):
66 mkdir(dest_dir, perms=0o755)
67 try:64 try:
68 self.clone(source, dest_dir, branch, depth)65 self.clone(source, dest_dir, branch, depth)
69 except GitCommandError as e:66 except CalledProcessError as e:
70 raise UnhandledSource(e)67 raise UnhandledSource(e)
71 except OSError as e:68 except OSError as e:
72 raise UnhandledSource(e.strerror)69 raise UnhandledSource(e.strerror)
7370
=== modified file 'tests/018-basic-trusty-liberty' (properties changed: -x to +x)
=== modified file 'tests/020-basic-wily-liberty' (properties changed: -x to +x)
=== modified file 'tests/charmhelpers/contrib/amulet/deployment.py'
--- tests/charmhelpers/contrib/amulet/deployment.py 2015-01-26 11:51:28 +0000
+++ tests/charmhelpers/contrib/amulet/deployment.py 2016-01-22 22:32:27 +0000
@@ -51,7 +51,8 @@
51 if 'units' not in this_service:51 if 'units' not in this_service:
52 this_service['units'] = 152 this_service['units'] = 1
5353
54 self.d.add(this_service['name'], units=this_service['units'])54 self.d.add(this_service['name'], units=this_service['units'],
55 constraints=this_service.get('constraints'))
5556
56 for svc in other_services:57 for svc in other_services:
57 if 'location' in svc:58 if 'location' in svc:
@@ -64,7 +65,8 @@
64 if 'units' not in svc:65 if 'units' not in svc:
65 svc['units'] = 166 svc['units'] = 1
6667
67 self.d.add(svc['name'], charm=branch_location, units=svc['units'])68 self.d.add(svc['name'], charm=branch_location, units=svc['units'],
69 constraints=svc.get('constraints'))
6870
69 def _add_relations(self, relations):71 def _add_relations(self, relations):
70 """Add all of the relations for the services."""72 """Add all of the relations for the services."""
7173
=== modified file 'tests/charmhelpers/contrib/amulet/utils.py'
--- tests/charmhelpers/contrib/amulet/utils.py 2015-08-19 13:50:42 +0000
+++ tests/charmhelpers/contrib/amulet/utils.py 2016-01-22 22:32:27 +0000
@@ -19,9 +19,11 @@
19import logging19import logging
20import os20import os
21import re21import re
22import socket
22import subprocess23import subprocess
23import sys24import sys
24import time25import time
26import uuid
2527
26import amulet28import amulet
27import distro_info29import distro_info
@@ -114,7 +116,7 @@
114 # /!\ DEPRECATION WARNING (beisner):116 # /!\ DEPRECATION WARNING (beisner):
115 # New and existing tests should be rewritten to use117 # New and existing tests should be rewritten to use
116 # validate_services_by_name() as it is aware of init systems.118 # validate_services_by_name() as it is aware of init systems.
117 self.log.warn('/!\\ DEPRECATION WARNING: use '119 self.log.warn('DEPRECATION WARNING: use '
118 'validate_services_by_name instead of validate_services '120 'validate_services_by_name instead of validate_services '
119 'due to init system differences.')121 'due to init system differences.')
120122
@@ -269,33 +271,52 @@
269 """Get last modification time of directory."""271 """Get last modification time of directory."""
270 return sentry_unit.directory_stat(directory)['mtime']272 return sentry_unit.directory_stat(directory)['mtime']
271273
272 def _get_proc_start_time(self, sentry_unit, service, pgrep_full=False):274 def _get_proc_start_time(self, sentry_unit, service, pgrep_full=None):
273 """Get process' start time.275 """Get start time of a process based on the last modification time
274276 of the /proc/pid directory.
275 Determine start time of the process based on the last modification277
276 time of the /proc/pid directory. If pgrep_full is True, the process278 :sentry_unit: The sentry unit to check for the service on
277 name is matched against the full command line.279 :service: service name to look for in process table
278 """280 :pgrep_full: [Deprecated] Use full command line search mode with pgrep
279 if pgrep_full:281 :returns: epoch time of service process start
280 cmd = 'pgrep -o -f {}'.format(service)282 :param commands: list of bash commands
281 else:283 :param sentry_units: list of sentry unit pointers
282 cmd = 'pgrep -o {}'.format(service)284 :returns: None if successful; Failure message otherwise
283 cmd = cmd + ' | grep -v pgrep || exit 0'285 """
284 cmd_out = sentry_unit.run(cmd)286 if pgrep_full is not None:
285 self.log.debug('CMDout: ' + str(cmd_out))287 # /!\ DEPRECATION WARNING (beisner):
286 if cmd_out[0]:288 # No longer implemented, as pidof is now used instead of pgrep.
287 self.log.debug('Pid for %s %s' % (service, str(cmd_out[0])))289 # https://bugs.launchpad.net/charm-helpers/+bug/1474030
288 proc_dir = '/proc/{}'.format(cmd_out[0].strip())290 self.log.warn('DEPRECATION WARNING: pgrep_full bool is no '
289 return self._get_dir_mtime(sentry_unit, proc_dir)291 'longer implemented re: lp 1474030.')
292
293 pid_list = self.get_process_id_list(sentry_unit, service)
294 pid = pid_list[0]
295 proc_dir = '/proc/{}'.format(pid)
296 self.log.debug('Pid for {} on {}: {}'.format(
297 service, sentry_unit.info['unit_name'], pid))
298
299 return self._get_dir_mtime(sentry_unit, proc_dir)
290300
291 def service_restarted(self, sentry_unit, service, filename,301 def service_restarted(self, sentry_unit, service, filename,
292 pgrep_full=False, sleep_time=20):302 pgrep_full=None, sleep_time=20):
293 """Check if service was restarted.303 """Check if service was restarted.
294304
295 Compare a service's start time vs a file's last modification time305 Compare a service's start time vs a file's last modification time
296 (such as a config file for that service) to determine if the service306 (such as a config file for that service) to determine if the service
297 has been restarted.307 has been restarted.
298 """308 """
309 # /!\ DEPRECATION WARNING (beisner):
310 # This method is prone to races in that no before-time is known.
311 # Use validate_service_config_changed instead.
312
313 # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
314 # used instead of pgrep. pgrep_full is still passed through to ensure
315 # deprecation WARNS. lp1474030
316 self.log.warn('DEPRECATION WARNING: use '
317 'validate_service_config_changed instead of '
318 'service_restarted due to known races.')
319
299 time.sleep(sleep_time)320 time.sleep(sleep_time)
300 if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >=321 if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >=
301 self._get_file_mtime(sentry_unit, filename)):322 self._get_file_mtime(sentry_unit, filename)):
@@ -304,78 +325,122 @@
304 return False325 return False
305326
306 def service_restarted_since(self, sentry_unit, mtime, service,327 def service_restarted_since(self, sentry_unit, mtime, service,
307 pgrep_full=False, sleep_time=20,328 pgrep_full=None, sleep_time=20,
308 retry_count=2):329 retry_count=30, retry_sleep_time=10):
309 """Check if service was been started after a given time.330 """Check if service was been started after a given time.
310331
311 Args:332 Args:
312 sentry_unit (sentry): The sentry unit to check for the service on333 sentry_unit (sentry): The sentry unit to check for the service on
313 mtime (float): The epoch time to check against334 mtime (float): The epoch time to check against
314 service (string): service name to look for in process table335 service (string): service name to look for in process table
315 pgrep_full (boolean): Use full command line search mode with pgrep336 pgrep_full: [Deprecated] Use full command line search mode with pgrep
316 sleep_time (int): Seconds to sleep before looking for process337 sleep_time (int): Initial sleep time (s) before looking for file
317 retry_count (int): If service is not found, how many times to retry338 retry_sleep_time (int): Time (s) to sleep between retries
339 retry_count (int): If file is not found, how many times to retry
318340
319 Returns:341 Returns:
320 bool: True if service found and its start time it newer than mtime,342 bool: True if service found and its start time it newer than mtime,
321 False if service is older than mtime or if service was343 False if service is older than mtime or if service was
322 not found.344 not found.
323 """345 """
324 self.log.debug('Checking %s restarted since %s' % (service, mtime))346 # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
347 # used instead of pgrep. pgrep_full is still passed through to ensure
348 # deprecation WARNS. lp1474030
349
350 unit_name = sentry_unit.info['unit_name']
351 self.log.debug('Checking that %s service restarted since %s on '
352 '%s' % (service, mtime, unit_name))
325 time.sleep(sleep_time)353 time.sleep(sleep_time)
326 proc_start_time = self._get_proc_start_time(sentry_unit, service,354 proc_start_time = None
327 pgrep_full)355 tries = 0
328 while retry_count > 0 and not proc_start_time:356 while tries <= retry_count and not proc_start_time:
329 self.log.debug('No pid file found for service %s, will retry %i '357 try:
330 'more times' % (service, retry_count))358 proc_start_time = self._get_proc_start_time(sentry_unit,
331 time.sleep(30)359 service,
332 proc_start_time = self._get_proc_start_time(sentry_unit, service,360 pgrep_full)
333 pgrep_full)361 self.log.debug('Attempt {} to get {} proc start time on {} '
334 retry_count = retry_count - 1362 'OK'.format(tries, service, unit_name))
363 except IOError as e:
364 # NOTE(beisner) - race avoidance, proc may not exist yet.
365 # https://bugs.launchpad.net/charm-helpers/+bug/1474030
366 self.log.debug('Attempt {} to get {} proc start time on {} '
367 'failed\n{}'.format(tries, service,
368 unit_name, e))
369 time.sleep(retry_sleep_time)
370 tries += 1
335371
336 if not proc_start_time:372 if not proc_start_time:
337 self.log.warn('No proc start time found, assuming service did '373 self.log.warn('No proc start time found, assuming service did '
338 'not start')374 'not start')
339 return False375 return False
340 if proc_start_time >= mtime:376 if proc_start_time >= mtime:
341 self.log.debug('proc start time is newer than provided mtime'377 self.log.debug('Proc start time is newer than provided mtime'
342 '(%s >= %s)' % (proc_start_time, mtime))378 '(%s >= %s) on %s (OK)' % (proc_start_time,
379 mtime, unit_name))
343 return True380 return True
344 else:381 else:
345 self.log.warn('proc start time (%s) is older than provided mtime '382 self.log.warn('Proc start time (%s) is older than provided mtime '
346 '(%s), service did not restart' % (proc_start_time,383 '(%s) on %s, service did not '
347 mtime))384 'restart' % (proc_start_time, mtime, unit_name))
348 return False385 return False
349386
350 def config_updated_since(self, sentry_unit, filename, mtime,387 def config_updated_since(self, sentry_unit, filename, mtime,
351 sleep_time=20):388 sleep_time=20, retry_count=30,
389 retry_sleep_time=10):
352 """Check if file was modified after a given time.390 """Check if file was modified after a given time.
353391
354 Args:392 Args:
355 sentry_unit (sentry): The sentry unit to check the file mtime on393 sentry_unit (sentry): The sentry unit to check the file mtime on
356 filename (string): The file to check mtime of394 filename (string): The file to check mtime of
357 mtime (float): The epoch time to check against395 mtime (float): The epoch time to check against
358 sleep_time (int): Seconds to sleep before looking for process396 sleep_time (int): Initial sleep time (s) before looking for file
397 retry_sleep_time (int): Time (s) to sleep between retries
398 retry_count (int): If file is not found, how many times to retry
359399
360 Returns:400 Returns:
361 bool: True if file was modified more recently than mtime, False if401 bool: True if file was modified more recently than mtime, False if
362 file was modified before mtime,402 file was modified before mtime, or if file not found.
363 """403 """
364 self.log.debug('Checking %s updated since %s' % (filename, mtime))404 unit_name = sentry_unit.info['unit_name']
405 self.log.debug('Checking that %s updated since %s on '
406 '%s' % (filename, mtime, unit_name))
365 time.sleep(sleep_time)407 time.sleep(sleep_time)
366 file_mtime = self._get_file_mtime(sentry_unit, filename)408 file_mtime = None
409 tries = 0
410 while tries <= retry_count and not file_mtime:
411 try:
412 file_mtime = self._get_file_mtime(sentry_unit, filename)
413 self.log.debug('Attempt {} to get {} file mtime on {} '
414 'OK'.format(tries, filename, unit_name))
415 except IOError as e:
416 # NOTE(beisner) - race avoidance, file may not exist yet.
417 # https://bugs.launchpad.net/charm-helpers/+bug/1474030
418 self.log.debug('Attempt {} to get {} file mtime on {} '
419 'failed\n{}'.format(tries, filename,
420 unit_name, e))
421 time.sleep(retry_sleep_time)
422 tries += 1
423
424 if not file_mtime:
425 self.log.warn('Could not determine file mtime, assuming '
426 'file does not exist')
427 return False
428
367 if file_mtime >= mtime:429 if file_mtime >= mtime:
368 self.log.debug('File mtime is newer than provided mtime '430 self.log.debug('File mtime is newer than provided mtime '
369 '(%s >= %s)' % (file_mtime, mtime))431 '(%s >= %s) on %s (OK)' % (file_mtime,
432 mtime, unit_name))
370 return True433 return True
371 else:434 else:
372 self.log.warn('File mtime %s is older than provided mtime %s'435 self.log.warn('File mtime is older than provided mtime'
373 % (file_mtime, mtime))436 '(%s < on %s) on %s' % (file_mtime,
437 mtime, unit_name))
374 return False438 return False
375439
376 def validate_service_config_changed(self, sentry_unit, mtime, service,440 def validate_service_config_changed(self, sentry_unit, mtime, service,
377 filename, pgrep_full=False,441 filename, pgrep_full=None,
378 sleep_time=20, retry_count=2):442 sleep_time=20, retry_count=30,
443 retry_sleep_time=10):
379 """Check service and file were updated after mtime444 """Check service and file were updated after mtime
380445
381 Args:446 Args:
@@ -383,9 +448,10 @@
383 mtime (float): The epoch time to check against448 mtime (float): The epoch time to check against
384 service (string): service name to look for in process table449 service (string): service name to look for in process table
385 filename (string): The file to check mtime of450 filename (string): The file to check mtime of
386 pgrep_full (boolean): Use full command line search mode with pgrep451 pgrep_full: [Deprecated] Use full command line search mode with pgrep
387 sleep_time (int): Seconds to sleep before looking for process452 sleep_time (int): Initial sleep in seconds to pass to test helpers
388 retry_count (int): If service is not found, how many times to retry453 retry_count (int): If service is not found, how many times to retry
454 retry_sleep_time (int): Time in seconds to wait between retries
389455
390 Typical Usage:456 Typical Usage:
391 u = OpenStackAmuletUtils(ERROR)457 u = OpenStackAmuletUtils(ERROR)
@@ -402,15 +468,27 @@
402 mtime, False if service is older than mtime or if service was468 mtime, False if service is older than mtime or if service was
403 not found or if filename was modified before mtime.469 not found or if filename was modified before mtime.
404 """470 """
405 self.log.debug('Checking %s restarted since %s' % (service, mtime))471
406 time.sleep(sleep_time)472 # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
407 service_restart = self.service_restarted_since(sentry_unit, mtime,473 # used instead of pgrep. pgrep_full is still passed through to ensure
408 service,474 # deprecation WARNS. lp1474030
409 pgrep_full=pgrep_full,475
410 sleep_time=0,476 service_restart = self.service_restarted_since(
411 retry_count=retry_count)477 sentry_unit, mtime,
412 config_update = self.config_updated_since(sentry_unit, filename, mtime,478 service,
413 sleep_time=0)479 pgrep_full=pgrep_full,
480 sleep_time=sleep_time,
481 retry_count=retry_count,
482 retry_sleep_time=retry_sleep_time)
483
484 config_update = self.config_updated_since(
485 sentry_unit,
486 filename,
487 mtime,
488 sleep_time=sleep_time,
489 retry_count=retry_count,
490 retry_sleep_time=retry_sleep_time)
491
414 return service_restart and config_update492 return service_restart and config_update
415493
416 def get_sentry_time(self, sentry_unit):494 def get_sentry_time(self, sentry_unit):
@@ -428,7 +506,6 @@
428 """Return a list of all Ubuntu releases in order of release."""506 """Return a list of all Ubuntu releases in order of release."""
429 _d = distro_info.UbuntuDistroInfo()507 _d = distro_info.UbuntuDistroInfo()
430 _release_list = _d.all508 _release_list = _d.all
431 self.log.debug('Ubuntu release list: {}'.format(_release_list))
432 return _release_list509 return _release_list
433510
434 def file_to_url(self, file_rel_path):511 def file_to_url(self, file_rel_path):
@@ -568,6 +645,142 @@
568645
569 return None646 return None
570647
648 def validate_sectionless_conf(self, file_contents, expected):
649 """A crude conf parser. Useful to inspect configuration files which
650 do not have section headers (as would be necessary in order to use
651 the configparser). Such as openstack-dashboard or rabbitmq confs."""
652 for line in file_contents.split('\n'):
653 if '=' in line:
654 args = line.split('=')
655 if len(args) <= 1:
656 continue
657 key = args[0].strip()
658 value = args[1].strip()
659 if key in expected.keys():
660 if expected[key] != value:
661 msg = ('Config mismatch. Expected, actual: {}, '
662 '{}'.format(expected[key], value))
663 amulet.raise_status(amulet.FAIL, msg=msg)
664
665 def get_unit_hostnames(self, units):
666 """Return a dict of juju unit names to hostnames."""
667 host_names = {}
668 for unit in units:
669 host_names[unit.info['unit_name']] = \
670 str(unit.file_contents('/etc/hostname').strip())
671 self.log.debug('Unit host names: {}'.format(host_names))
672 return host_names
673
674 def run_cmd_unit(self, sentry_unit, cmd):
675 """Run a command on a unit, return the output and exit code."""
676 output, code = sentry_unit.run(cmd)
677 if code == 0:
678 self.log.debug('{} `{}` command returned {} '
679 '(OK)'.format(sentry_unit.info['unit_name'],
680 cmd, code))
681 else:
682 msg = ('{} `{}` command returned {} '
683 '{}'.format(sentry_unit.info['unit_name'],
684 cmd, code, output))
685 amulet.raise_status(amulet.FAIL, msg=msg)
686 return str(output), code
687
688 def file_exists_on_unit(self, sentry_unit, file_name):
689 """Check if a file exists on a unit."""
690 try:
691 sentry_unit.file_stat(file_name)
692 return True
693 except IOError:
694 return False
695 except Exception as e:
696 msg = 'Error checking file {}: {}'.format(file_name, e)
697 amulet.raise_status(amulet.FAIL, msg=msg)
698
699 def file_contents_safe(self, sentry_unit, file_name,
700 max_wait=60, fatal=False):
701 """Get file contents from a sentry unit. Wrap amulet file_contents
702 with retry logic to address races where a file checks as existing,
703 but no longer exists by the time file_contents is called.
704 Return None if file not found. Optionally raise if fatal is True."""
705 unit_name = sentry_unit.info['unit_name']
706 file_contents = False
707 tries = 0
708 while not file_contents and tries < (max_wait / 4):
709 try:
710 file_contents = sentry_unit.file_contents(file_name)
711 except IOError:
712 self.log.debug('Attempt {} to open file {} from {} '
713 'failed'.format(tries, file_name,
714 unit_name))
715 time.sleep(4)
716 tries += 1
717
718 if file_contents:
719 return file_contents
720 elif not fatal:
721 return None
722 elif fatal:
723 msg = 'Failed to get file contents from unit.'
724 amulet.raise_status(amulet.FAIL, msg)
725
726 def port_knock_tcp(self, host="localhost", port=22, timeout=15):
727 """Open a TCP socket to check for a listening sevice on a host.
728
729 :param host: host name or IP address, default to localhost
730 :param port: TCP port number, default to 22
731 :param timeout: Connect timeout, default to 15 seconds
732 :returns: True if successful, False if connect failed
733 """
734
735 # Resolve host name if possible
736 try:
737 connect_host = socket.gethostbyname(host)
738 host_human = "{} ({})".format(connect_host, host)
739 except socket.error as e:
740 self.log.warn('Unable to resolve address: '
741 '{} ({}) Trying anyway!'.format(host, e))
742 connect_host = host
743 host_human = connect_host
744
745 # Attempt socket connection
746 try:
747 knock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
748 knock.settimeout(timeout)
749 knock.connect((connect_host, port))
750 knock.close()
751 self.log.debug('Socket connect OK for host '
752 '{} on port {}.'.format(host_human, port))
753 return True
754 except socket.error as e:
755 self.log.debug('Socket connect FAIL for'
756 ' {} port {} ({})'.format(host_human, port, e))
757 return False
758
759 def port_knock_units(self, sentry_units, port=22,
760 timeout=15, expect_success=True):
761 """Open a TCP socket to check for a listening sevice on each
762 listed juju unit.
763
764 :param sentry_units: list of sentry unit pointers
765 :param port: TCP port number, default to 22
766 :param timeout: Connect timeout, default to 15 seconds
767 :expect_success: True by default, set False to invert logic
768 :returns: None if successful, Failure message otherwise
769 """
770 for unit in sentry_units:
771 host = unit.info['public-address']
772 connected = self.port_knock_tcp(host, port, timeout)
773 if not connected and expect_success:
774 return 'Socket connect failed.'
775 elif connected and not expect_success:
776 return 'Socket connected unexpectedly.'
777
778 def get_uuid_epoch_stamp(self):
779 """Returns a stamp string based on uuid4 and epoch time. Useful in
780 generating test messages which need to be unique-ish."""
781 return '[{}-{}]'.format(uuid.uuid4(), time.time())
782
783# amulet juju action helpers:
571 def run_action(self, unit_sentry, action,784 def run_action(self, unit_sentry, action,
572 _check_output=subprocess.check_output):785 _check_output=subprocess.check_output):
573 """Run the named action on a given unit sentry.786 """Run the named action on a given unit sentry.
@@ -594,3 +807,12 @@
594 output = _check_output(command, universal_newlines=True)807 output = _check_output(command, universal_newlines=True)
595 data = json.loads(output)808 data = json.loads(output)
596 return data.get(u"status") == "completed"809 return data.get(u"status") == "completed"
810
811 def status_get(self, unit):
812 """Return the current service status of this unit."""
813 raw_status, return_code = unit.run(
814 "status-get --format=json --include-data")
815 if return_code != 0:
816 return ("unknown", "")
817 status = json.loads(raw_status)
818 return (status["status"], status["message"])
597819
=== modified file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py'
--- tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-08-19 13:50:42 +0000
+++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2016-01-22 22:32:27 +0000
@@ -14,12 +14,18 @@
14# You should have received a copy of the GNU Lesser General Public License14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1616
17import logging
18import re
19import sys
17import six20import six
18from collections import OrderedDict21from collections import OrderedDict
19from charmhelpers.contrib.amulet.deployment import (22from charmhelpers.contrib.amulet.deployment import (
20 AmuletDeployment23 AmuletDeployment
21)24)
2225
26DEBUG = logging.DEBUG
27ERROR = logging.ERROR
28
2329
24class OpenStackAmuletDeployment(AmuletDeployment):30class OpenStackAmuletDeployment(AmuletDeployment):
25 """OpenStack amulet deployment.31 """OpenStack amulet deployment.
@@ -28,9 +34,12 @@
28 that is specifically for use by OpenStack charms.34 that is specifically for use by OpenStack charms.
29 """35 """
3036
31 def __init__(self, series=None, openstack=None, source=None, stable=True):37 def __init__(self, series=None, openstack=None, source=None,
38 stable=True, log_level=DEBUG):
32 """Initialize the deployment environment."""39 """Initialize the deployment environment."""
33 super(OpenStackAmuletDeployment, self).__init__(series)40 super(OpenStackAmuletDeployment, self).__init__(series)
41 self.log = self.get_logger(level=log_level)
42 self.log.info('OpenStackAmuletDeployment: init')
34 self.openstack = openstack43 self.openstack = openstack
35 self.source = source44 self.source = source
36 self.stable = stable45 self.stable = stable
@@ -38,26 +47,55 @@
38 # out.47 # out.
39 self.current_next = "trusty"48 self.current_next = "trusty"
4049
50 def get_logger(self, name="deployment-logger", level=logging.DEBUG):
51 """Get a logger object that will log to stdout."""
52 log = logging
53 logger = log.getLogger(name)
54 fmt = log.Formatter("%(asctime)s %(funcName)s "
55 "%(levelname)s: %(message)s")
56
57 handler = log.StreamHandler(stream=sys.stdout)
58 handler.setLevel(level)
59 handler.setFormatter(fmt)
60
61 logger.addHandler(handler)
62 logger.setLevel(level)
63
64 return logger
65
41 def _determine_branch_locations(self, other_services):66 def _determine_branch_locations(self, other_services):
42 """Determine the branch locations for the other services.67 """Determine the branch locations for the other services.
4368
44 Determine if the local branch being tested is derived from its69 Determine if the local branch being tested is derived from its
45 stable or next (dev) branch, and based on this, use the corresonding70 stable or next (dev) branch, and based on this, use the corresonding
46 stable or next branches for the other_services."""71 stable or next branches for the other_services."""
72
73 self.log.info('OpenStackAmuletDeployment: determine branch locations')
74
75 # Charms outside the lp:~openstack-charmers namespace
47 base_charms = ['mysql', 'mongodb', 'nrpe']76 base_charms = ['mysql', 'mongodb', 'nrpe']
4877
78 # Force these charms to current series even when using an older series.
79 # ie. Use trusty/nrpe even when series is precise, as the P charm
80 # does not possess the necessary external master config and hooks.
81 force_series_current = ['nrpe']
82
49 if self.series in ['precise', 'trusty']:83 if self.series in ['precise', 'trusty']:
50 base_series = self.series84 base_series = self.series
51 else:85 else:
52 base_series = self.current_next86 base_series = self.current_next
5387
54 if self.stable:88 for svc in other_services:
55 for svc in other_services:89 if svc['name'] in force_series_current:
90 base_series = self.current_next
91 # If a location has been explicitly set, use it
92 if svc.get('location'):
93 continue
94 if self.stable:
56 temp = 'lp:charms/{}/{}'95 temp = 'lp:charms/{}/{}'
57 svc['location'] = temp.format(base_series,96 svc['location'] = temp.format(base_series,
58 svc['name'])97 svc['name'])
59 else:98 else:
60 for svc in other_services:
61 if svc['name'] in base_charms:99 if svc['name'] in base_charms:
62 temp = 'lp:charms/{}/{}'100 temp = 'lp:charms/{}/{}'
63 svc['location'] = temp.format(base_series,101 svc['location'] = temp.format(base_series,
@@ -66,10 +104,13 @@
66 temp = 'lp:~openstack-charmers/charms/{}/{}/next'104 temp = 'lp:~openstack-charmers/charms/{}/{}/next'
67 svc['location'] = temp.format(self.current_next,105 svc['location'] = temp.format(self.current_next,
68 svc['name'])106 svc['name'])
107
69 return other_services108 return other_services
70109
71 def _add_services(self, this_service, other_services):110 def _add_services(self, this_service, other_services):
72 """Add services to the deployment and set openstack-origin/source."""111 """Add services to the deployment and set openstack-origin/source."""
112 self.log.info('OpenStackAmuletDeployment: adding services')
113
73 other_services = self._determine_branch_locations(other_services)114 other_services = self._determine_branch_locations(other_services)
74115
75 super(OpenStackAmuletDeployment, self)._add_services(this_service,116 super(OpenStackAmuletDeployment, self)._add_services(this_service,
@@ -77,29 +118,103 @@
77118
78 services = other_services119 services = other_services
79 services.append(this_service)120 services.append(this_service)
121
122 # Charms which should use the source config option
80 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',123 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
81 'ceph-osd', 'ceph-radosgw']124 'ceph-osd', 'ceph-radosgw']
82 # Most OpenStack subordinate charms do not expose an origin option125
83 # as that is controlled by the principle.126 # Charms which can not use openstack-origin, ie. many subordinates
84 ignore = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe']127 no_origin = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe',
128 'openvswitch-odl', 'neutron-api-odl', 'odl-controller',
129 'cinder-backup']
85130
86 if self.openstack:131 if self.openstack:
87 for svc in services:132 for svc in services:
88 if svc['name'] not in use_source + ignore:133 if svc['name'] not in use_source + no_origin:
89 config = {'openstack-origin': self.openstack}134 config = {'openstack-origin': self.openstack}
90 self.d.configure(svc['name'], config)135 self.d.configure(svc['name'], config)
91136
92 if self.source:137 if self.source:
93 for svc in services:138 for svc in services:
94 if svc['name'] in use_source and svc['name'] not in ignore:139 if svc['name'] in use_source and svc['name'] not in no_origin:
95 config = {'source': self.source}140 config = {'source': self.source}
96 self.d.configure(svc['name'], config)141 self.d.configure(svc['name'], config)
97142
98 def _configure_services(self, configs):143 def _configure_services(self, configs):
99 """Configure all of the services."""144 """Configure all of the services."""
145 self.log.info('OpenStackAmuletDeployment: configure services')
100 for service, config in six.iteritems(configs):146 for service, config in six.iteritems(configs):
101 self.d.configure(service, config)147 self.d.configure(service, config)
102148
149 def _auto_wait_for_status(self, message=None, exclude_services=None,
150 include_only=None, timeout=1800):
151 """Wait for all units to have a specific extended status, except
152 for any defined as excluded. Unless specified via message, any
153 status containing any case of 'ready' will be considered a match.
154
155 Examples of message usage:
156
157 Wait for all unit status to CONTAIN any case of 'ready' or 'ok':
158 message = re.compile('.*ready.*|.*ok.*', re.IGNORECASE)
159
160 Wait for all units to reach this status (exact match):
161 message = re.compile('^Unit is ready and clustered$')
162
163 Wait for all units to reach any one of these (exact match):
164 message = re.compile('Unit is ready|OK|Ready')
165
166 Wait for at least one unit to reach this status (exact match):
167 message = {'ready'}
168
169 See Amulet's sentry.wait_for_messages() for message usage detail.
170 https://github.com/juju/amulet/blob/master/amulet/sentry.py
171
172 :param message: Expected status match
173 :param exclude_services: List of juju service names to ignore,
174 not to be used in conjuction with include_only.
175 :param include_only: List of juju service names to exclusively check,
176 not to be used in conjuction with exclude_services.
177 :param timeout: Maximum time in seconds to wait for status match
178 :returns: None. Raises if timeout is hit.
179 """
180 self.log.info('Waiting for extended status on units...')
181
182 all_services = self.d.services.keys()
183
184 if exclude_services and include_only:
185 raise ValueError('exclude_services can not be used '
186 'with include_only')
187
188 if message:
189 if isinstance(message, re._pattern_type):
190 match = message.pattern
191 else:
192 match = message
193
194 self.log.debug('Custom extended status wait match: '
195 '{}'.format(match))
196 else:
197 self.log.debug('Default extended status wait match: contains '
198 'READY (case-insensitive)')
199 message = re.compile('.*ready.*', re.IGNORECASE)
200
201 if exclude_services:
202 self.log.debug('Excluding services from extended status match: '
203 '{}'.format(exclude_services))
204 else:
205 exclude_services = []
206
207 if include_only:
208 services = include_only
209 else:
210 services = list(set(all_services) - set(exclude_services))
211
212 self.log.debug('Waiting up to {}s for extended status on services: '
213 '{}'.format(timeout, services))
214 service_messages = {service: message for service in services}
215 self.d.sentry.wait_for_messages(service_messages, timeout=timeout)
216 self.log.info('OK')
217
103 def _get_openstack_release(self):218 def _get_openstack_release(self):
104 """Get openstack release.219 """Get openstack release.
105220
@@ -111,7 +226,8 @@
111 self.precise_havana, self.precise_icehouse,226 self.precise_havana, self.precise_icehouse,
112 self.trusty_icehouse, self.trusty_juno, self.utopic_juno,227 self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
113 self.trusty_kilo, self.vivid_kilo, self.trusty_liberty,228 self.trusty_kilo, self.vivid_kilo, self.trusty_liberty,
114 self.wily_liberty) = range(12)229 self.wily_liberty, self.trusty_mitaka,
230 self.xenial_mitaka) = range(14)
115231
116 releases = {232 releases = {
117 ('precise', None): self.precise_essex,233 ('precise', None): self.precise_essex,
@@ -123,9 +239,11 @@
123 ('trusty', 'cloud:trusty-juno'): self.trusty_juno,239 ('trusty', 'cloud:trusty-juno'): self.trusty_juno,
124 ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,240 ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,
125 ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty,241 ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty,
242 ('trusty', 'cloud:trusty-mitaka'): self.trusty_mitaka,
126 ('utopic', None): self.utopic_juno,243 ('utopic', None): self.utopic_juno,
127 ('vivid', None): self.vivid_kilo,244 ('vivid', None): self.vivid_kilo,
128 ('wily', None): self.wily_liberty}245 ('wily', None): self.wily_liberty,
246 ('xenial', None): self.xenial_mitaka}
129 return releases[(self.series, self.openstack)]247 return releases[(self.series, self.openstack)]
130248
131 def _get_openstack_release_string(self):249 def _get_openstack_release_string(self):
@@ -142,6 +260,7 @@
142 ('utopic', 'juno'),260 ('utopic', 'juno'),
143 ('vivid', 'kilo'),261 ('vivid', 'kilo'),
144 ('wily', 'liberty'),262 ('wily', 'liberty'),
263 ('xenial', 'mitaka'),
145 ])264 ])
146 if self.openstack:265 if self.openstack:
147 os_origin = self.openstack.split(':')[1]266 os_origin = self.openstack.split(':')[1]
148267
=== modified file 'tests/charmhelpers/contrib/openstack/amulet/utils.py'
--- tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-06-29 14:24:05 +0000
+++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2016-01-22 22:32:27 +0000
@@ -18,6 +18,7 @@
18import json18import json
19import logging19import logging
20import os20import os
21import re
21import six22import six
22import time23import time
23import urllib24import urllib
@@ -27,6 +28,7 @@
27import heatclient.v1.client as heat_client28import heatclient.v1.client as heat_client
28import keystoneclient.v2_0 as keystone_client29import keystoneclient.v2_0 as keystone_client
29import novaclient.v1_1.client as nova_client30import novaclient.v1_1.client as nova_client
31import pika
30import swiftclient32import swiftclient
3133
32from charmhelpers.contrib.amulet.utils import (34from charmhelpers.contrib.amulet.utils import (
@@ -602,3 +604,382 @@
602 self.log.debug('Ceph {} samples (OK): '604 self.log.debug('Ceph {} samples (OK): '
603 '{}'.format(sample_type, samples))605 '{}'.format(sample_type, samples))
604 return None606 return None
607
608 # rabbitmq/amqp specific helpers:
609
610 def rmq_wait_for_cluster(self, deployment, init_sleep=15, timeout=1200):
611 """Wait for rmq units extended status to show cluster readiness,
612 after an optional initial sleep period. Initial sleep is likely
613 necessary to be effective following a config change, as status
614 message may not instantly update to non-ready."""
615
616 if init_sleep:
617 time.sleep(init_sleep)
618
619 message = re.compile('^Unit is ready and clustered$')
620 deployment._auto_wait_for_status(message=message,
621 timeout=timeout,
622 include_only=['rabbitmq-server'])
623
624 def add_rmq_test_user(self, sentry_units,
625 username="testuser1", password="changeme"):
626 """Add a test user via the first rmq juju unit, check connection as
627 the new user against all sentry units.
628
629 :param sentry_units: list of sentry unit pointers
630 :param username: amqp user name, default to testuser1
631 :param password: amqp user password
632 :returns: None if successful. Raise on error.
633 """
634 self.log.debug('Adding rmq user ({})...'.format(username))
635
636 # Check that user does not already exist
637 cmd_user_list = 'rabbitmqctl list_users'
638 output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
639 if username in output:
640 self.log.warning('User ({}) already exists, returning '
641 'gracefully.'.format(username))
642 return
643
644 perms = '".*" ".*" ".*"'
645 cmds = ['rabbitmqctl add_user {} {}'.format(username, password),
646 'rabbitmqctl set_permissions {} {}'.format(username, perms)]
647
648 # Add user via first unit
649 for cmd in cmds:
650 output, _ = self.run_cmd_unit(sentry_units[0], cmd)
651
652 # Check connection against the other sentry_units
653 self.log.debug('Checking user connect against units...')
654 for sentry_unit in sentry_units:
655 connection = self.connect_amqp_by_unit(sentry_unit, ssl=False,
656 username=username,
657 password=password)
658 connection.close()
659
660 def delete_rmq_test_user(self, sentry_units, username="testuser1"):
661 """Delete a rabbitmq user via the first rmq juju unit.
662
663 :param sentry_units: list of sentry unit pointers
664 :param username: amqp user name, default to testuser1
665 :param password: amqp user password
666 :returns: None if successful or no such user.
667 """
668 self.log.debug('Deleting rmq user ({})...'.format(username))
669
670 # Check that the user exists
671 cmd_user_list = 'rabbitmqctl list_users'
672 output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
673
674 if username not in output:
675 self.log.warning('User ({}) does not exist, returning '
676 'gracefully.'.format(username))
677 return
678
679 # Delete the user
680 cmd_user_del = 'rabbitmqctl delete_user {}'.format(username)
681 output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_del)
682
683 def get_rmq_cluster_status(self, sentry_unit):
684 """Execute rabbitmq cluster status command on a unit and return
685 the full output.
686
687 :param unit: sentry unit
688 :returns: String containing console output of cluster status command
689 """
690 cmd = 'rabbitmqctl cluster_status'
691 output, _ = self.run_cmd_unit(sentry_unit, cmd)
692 self.log.debug('{} cluster_status:\n{}'.format(
693 sentry_unit.info['unit_name'], output))
694 return str(output)
695
696 def get_rmq_cluster_running_nodes(self, sentry_unit):
697 """Parse rabbitmqctl cluster_status output string, return list of
698 running rabbitmq cluster nodes.
699
700 :param unit: sentry unit
701 :returns: List containing node names of running nodes
702 """
703 # NOTE(beisner): rabbitmqctl cluster_status output is not
704 # json-parsable, do string chop foo, then json.loads that.
705 str_stat = self.get_rmq_cluster_status(sentry_unit)
706 if 'running_nodes' in str_stat:
707 pos_start = str_stat.find("{running_nodes,") + 15
708 pos_end = str_stat.find("]},", pos_start) + 1
709 str_run_nodes = str_stat[pos_start:pos_end].replace("'", '"')
710 run_nodes = json.loads(str_run_nodes)
711 return run_nodes
712 else:
713 return []
714
715 def validate_rmq_cluster_running_nodes(self, sentry_units):
716 """Check that all rmq unit hostnames are represented in the
717 cluster_status output of all units.
718
719 :param host_names: dict of juju unit names to host names
720 :param units: list of sentry unit pointers (all rmq units)
721 :returns: None if successful, otherwise return error message
722 """
723 host_names = self.get_unit_hostnames(sentry_units)
724 errors = []
725
726 # Query every unit for cluster_status running nodes
727 for query_unit in sentry_units:
728 query_unit_name = query_unit.info['unit_name']
729 running_nodes = self.get_rmq_cluster_running_nodes(query_unit)
730
731 # Confirm that every unit is represented in the queried unit's
732 # cluster_status running nodes output.
733 for validate_unit in sentry_units:
734 val_host_name = host_names[validate_unit.info['unit_name']]
735 val_node_name = 'rabbit@{}'.format(val_host_name)
736
737 if val_node_name not in running_nodes:
738 errors.append('Cluster member check failed on {}: {} not '
739 'in {}\n'.format(query_unit_name,
740 val_node_name,
741 running_nodes))
742 if errors:
743 return ''.join(errors)
744
745 def rmq_ssl_is_enabled_on_unit(self, sentry_unit, port=None):
746 """Check a single juju rmq unit for ssl and port in the config file."""
747 host = sentry_unit.info['public-address']
748 unit_name = sentry_unit.info['unit_name']
749
750 conf_file = '/etc/rabbitmq/rabbitmq.config'
751 conf_contents = str(self.file_contents_safe(sentry_unit,
752 conf_file, max_wait=16))
753 # Checks
754 conf_ssl = 'ssl' in conf_contents
755 conf_port = str(port) in conf_contents
756
757 # Port explicitly checked in config
758 if port and conf_port and conf_ssl:
759 self.log.debug('SSL is enabled @{}:{} '
760 '({})'.format(host, port, unit_name))
761 return True
762 elif port and not conf_port and conf_ssl:
763 self.log.debug('SSL is enabled @{} but not on port {} '
764 '({})'.format(host, port, unit_name))
765 return False
766 # Port not checked (useful when checking that ssl is disabled)
767 elif not port and conf_ssl:
768 self.log.debug('SSL is enabled @{}:{} '
769 '({})'.format(host, port, unit_name))
770 return True
771 elif not conf_ssl:
772 self.log.debug('SSL not enabled @{}:{} '
773 '({})'.format(host, port, unit_name))
774 return False
775 else:
776 msg = ('Unknown condition when checking SSL status @{}:{} '
777 '({})'.format(host, port, unit_name))
778 amulet.raise_status(amulet.FAIL, msg)
779
780 def validate_rmq_ssl_enabled_units(self, sentry_units, port=None):
781 """Check that ssl is enabled on rmq juju sentry units.
782
783 :param sentry_units: list of all rmq sentry units
784 :param port: optional ssl port override to validate
785 :returns: None if successful, otherwise return error message
786 """
787 for sentry_unit in sentry_units:
788 if not self.rmq_ssl_is_enabled_on_unit(sentry_unit, port=port):
789 return ('Unexpected condition: ssl is disabled on unit '
790 '({})'.format(sentry_unit.info['unit_name']))
791 return None
792
793 def validate_rmq_ssl_disabled_units(self, sentry_units):
794 """Check that ssl is enabled on listed rmq juju sentry units.
795
796 :param sentry_units: list of all rmq sentry units
797 :returns: True if successful. Raise on error.
798 """
799 for sentry_unit in sentry_units:
800 if self.rmq_ssl_is_enabled_on_unit(sentry_unit):
801 return ('Unexpected condition: ssl is enabled on unit '
802 '({})'.format(sentry_unit.info['unit_name']))
803 return None
804
805 def configure_rmq_ssl_on(self, sentry_units, deployment,
806 port=None, max_wait=60):
807 """Turn ssl charm config option on, with optional non-default
808 ssl port specification. Confirm that it is enabled on every
809 unit.
810
811 :param sentry_units: list of sentry units
812 :param deployment: amulet deployment object pointer
813 :param port: amqp port, use defaults if None
814 :param max_wait: maximum time to wait in seconds to confirm
815 :returns: None if successful. Raise on error.
816 """
817 self.log.debug('Setting ssl charm config option: on')
818
819 # Enable RMQ SSL
820 config = {'ssl': 'on'}
821 if port:
822 config['ssl_port'] = port
823
824 deployment.d.configure('rabbitmq-server', config)
825
826 # Wait for unit status
827 self.rmq_wait_for_cluster(deployment)
828
829 # Confirm
830 tries = 0
831 ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
832 while ret and tries < (max_wait / 4):
833 time.sleep(4)
834 self.log.debug('Attempt {}: {}'.format(tries, ret))
835 ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
836 tries += 1
837
838 if ret:
839 amulet.raise_status(amulet.FAIL, ret)
840
841 def configure_rmq_ssl_off(self, sentry_units, deployment, max_wait=60):
842 """Turn ssl charm config option off, confirm that it is disabled
843 on every unit.
844
845 :param sentry_units: list of sentry units
846 :param deployment: amulet deployment object pointer
847 :param max_wait: maximum time to wait in seconds to confirm
848 :returns: None if successful. Raise on error.
849 """
850 self.log.debug('Setting ssl charm config option: off')
851
852 # Disable RMQ SSL
853 config = {'ssl': 'off'}
854 deployment.d.configure('rabbitmq-server', config)
855
856 # Wait for unit status
857 self.rmq_wait_for_cluster(deployment)
858
859 # Confirm
860 tries = 0
861 ret = self.validate_rmq_ssl_disabled_units(sentry_units)
862 while ret and tries < (max_wait / 4):
863 time.sleep(4)
864 self.log.debug('Attempt {}: {}'.format(tries, ret))
865 ret = self.validate_rmq_ssl_disabled_units(sentry_units)
866 tries += 1
867
868 if ret:
869 amulet.raise_status(amulet.FAIL, ret)
870
871 def connect_amqp_by_unit(self, sentry_unit, ssl=False,
872 port=None, fatal=True,
873 username="testuser1", password="changeme"):
874 """Establish and return a pika amqp connection to the rabbitmq service
875 running on a rmq juju unit.
876
877 :param sentry_unit: sentry unit pointer
878 :param ssl: boolean, default to False
879 :param port: amqp port, use defaults if None
880 :param fatal: boolean, default to True (raises on connect error)
881 :param username: amqp user name, default to testuser1
882 :param password: amqp user password
883 :returns: pika amqp connection pointer or None if failed and non-fatal
884 """
885 host = sentry_unit.info['public-address']
886 unit_name = sentry_unit.info['unit_name']
887
888 # Default port logic if port is not specified
889 if ssl and not port:
890 port = 5671
891 elif not ssl and not port:
892 port = 5672
893
894 self.log.debug('Connecting to amqp on {}:{} ({}) as '
895 '{}...'.format(host, port, unit_name, username))
896
897 try:
898 credentials = pika.PlainCredentials(username, password)
899 parameters = pika.ConnectionParameters(host=host, port=port,
900 credentials=credentials,
901 ssl=ssl,
902 connection_attempts=3,
903 retry_delay=5,
904 socket_timeout=1)
905 connection = pika.BlockingConnection(parameters)
906 assert connection.server_properties['product'] == 'RabbitMQ'
907 self.log.debug('Connect OK')
908 return connection
909 except Exception as e:
910 msg = ('amqp connection failed to {}:{} as '
911 '{} ({})'.format(host, port, username, str(e)))
912 if fatal:
913 amulet.raise_status(amulet.FAIL, msg)
914 else:
915 self.log.warn(msg)
916 return None
917
918 def publish_amqp_message_by_unit(self, sentry_unit, message,
919 queue="test", ssl=False,
920 username="testuser1",
921 password="changeme",
922 port=None):
923 """Publish an amqp message to a rmq juju unit.
924
925 :param sentry_unit: sentry unit pointer
926 :param message: amqp message string
927 :param queue: message queue, default to test
928 :param username: amqp user name, default to testuser1
929 :param password: amqp user password
930 :param ssl: boolean, default to False
931 :param port: amqp port, use defaults if None
932 :returns: None. Raises exception if publish failed.
933 """
934 self.log.debug('Publishing message to {} queue:\n{}'.format(queue,
935 message))
936 connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
937 port=port,
938 username=username,
939 password=password)
940
941 # NOTE(beisner): extra debug here re: pika hang potential:
942 # https://github.com/pika/pika/issues/297
943 # https://groups.google.com/forum/#!topic/rabbitmq-users/Ja0iyfF0Szw
944 self.log.debug('Defining channel...')
945 channel = connection.channel()
946 self.log.debug('Declaring queue...')
947 channel.queue_declare(queue=queue, auto_delete=False, durable=True)
948 self.log.debug('Publishing message...')
949 channel.basic_publish(exchange='', routing_key=queue, body=message)
950 self.log.debug('Closing channel...')
951 channel.close()
952 self.log.debug('Closing connection...')
953 connection.close()
954
955 def get_amqp_message_by_unit(self, sentry_unit, queue="test",
956 username="testuser1",
957 password="changeme",
958 ssl=False, port=None):
959 """Get an amqp message from a rmq juju unit.
960
961 :param sentry_unit: sentry unit pointer
962 :param queue: message queue, default to test
963 :param username: amqp user name, default to testuser1
964 :param password: amqp user password
965 :param ssl: boolean, default to False
966 :param port: amqp port, use defaults if None
967 :returns: amqp message body as string. Raise if get fails.
968 """
969 connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
970 port=port,
971 username=username,
972 password=password)
973 channel = connection.channel()
974 method_frame, _, body = channel.basic_get(queue)
975
976 if method_frame:
977 self.log.debug('Retreived message from {} queue:\n{}'.format(queue,
978 body))
979 channel.basic_ack(method_frame.delivery_tag)
980 channel.close()
981 connection.close()
982 return body
983 else:
984 msg = 'No message retrieved.'
985 amulet.raise_status(amulet.FAIL, msg)

Subscribers

People subscribed via source and target branches