Merge lp:~ajkavanagh/charms/trusty/memcached/add-spaces-support into lp:charms/trusty/memcached

Proposed by Alex Kavanagh
Status: Merged
Merged at revision: 77
Proposed branch: lp:~ajkavanagh/charms/trusty/memcached/add-spaces-support
Merge into: lp:charms/trusty/memcached
Diff against target: 1570 lines (+842/-136)
16 files modified
README.md (+23/-0)
hooks/charmhelpers/contrib/network/ip.py (+122/-28)
hooks/charmhelpers/contrib/network/ovs/__init__.py (+66/-1)
hooks/charmhelpers/core/hookenv.py (+47/-0)
hooks/charmhelpers/core/host.py (+225/-37)
hooks/charmhelpers/core/host_factory/centos.py (+16/-0)
hooks/charmhelpers/core/host_factory/ubuntu.py (+32/-0)
hooks/charmhelpers/core/kernel_factory/ubuntu.py (+1/-1)
hooks/charmhelpers/core/strutils.py (+53/-0)
hooks/charmhelpers/fetch/__init__.py (+1/-0)
hooks/charmhelpers/fetch/snap.py (+122/-0)
hooks/charmhelpers/fetch/ubuntu.py (+87/-36)
hooks/charmhelpers/osplatform.py (+6/-0)
hooks/memcached_hooks.py (+20/-13)
hooks/replication.py (+6/-5)
unit_tests/test_memcached_hooks.py (+15/-15)
To merge this branch: bzr merge lp:~ajkavanagh/charms/trusty/memcached/add-spaces-support
Reviewer Review Type Date Requested Status
David Ames (community) Approve
Review via email: mp+322844@code.launchpad.net

Description of the change

Add Juju space support to the interfaces so that Juju 2.0+ bindings work with the charm.

To post a comment you must log in.
Revision history for this message
David Ames (thedac) wrote :

Alex,

It feels like we are re-inventing the wheel with space_aware_unit_address.

We should use charmhelprs.contrib.network.ip.get_relation_ip just as we have in other charms. Though it does not look like it, if get_relation_ip is missing some functionality let's add it there.

review: Needs Fixing
Revision history for this message
Alex Kavanagh (ajkavanagh) wrote :

Good catch David. I wrote the space_aware_unit_address() function before the get_relation_ip() came into existence. I'll re-work the changes to use that.

78. By Alex Kavanagh

Removed the 'space_aware_unit_address()' function and replaced it with
the (new) charmhelpers.contrib.network.ip.get_relation_ip() function.

Updated the README.md file to indicate what space name strings exist in
the charm and what interfaces they are used for.

Synced charmhelpers to get the 'get_relation_ip()' function.

Revision history for this message
David Ames (thedac) wrote :

This looks good. Has it been tested?

Revision history for this message
David Ames (thedac) wrote :

Verbal confirmation from Alex that the amulet tests were run.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'README.md'
2--- README.md 2015-03-26 01:01:14 +0000
3+++ README.md 2017-05-05 10:32:43 +0000
4@@ -83,6 +83,29 @@
5
6 ## Known Limitations and Issues
7
8+# Juju Network Space support
9+
10+The charm supports Juju network space bindings on its interfaces. This is
11+activated in Juju 2.0+ by either using a --bind during a deploy, or using
12+--constraints. If an interface is not bound to a space then the charm falls
13+back to the default behaviour of using the unit's private address.
14+
15+The following network space strings are used in the charm to support binding to
16+specific interfaces:
17+
18+ - 'cluster' for the interface: memcached-replication
19+ - 'cache' for the interface: memcache
20+ - 'munin' for the interface: munin-node
21+ - 'monitors' for the interface: monitors
22+ - 'nrpe-external-master' for the interface: nrpe-external-master
23+
24+Thus, for a Juju network space called 'memcache-space', the binding would be
25+
26+juju ... --bind='cache:memcache-space' ...
27+
28+
29+Juju 1.25.x continues to use the private address of the unit.
30+
31 # Configuration
32
33 Standard configuration options are provided, we recommend scanning the [Memcached documentation](https://code.google.com/p/memcached/wiki/NewConfiguringServer) before tweaking the default configuration.
34
35=== modified file 'hooks/charmhelpers/contrib/network/ip.py'
36--- hooks/charmhelpers/contrib/network/ip.py 2016-09-09 12:18:22 +0000
37+++ hooks/charmhelpers/contrib/network/ip.py 2017-05-05 10:32:43 +0000
38@@ -20,25 +20,38 @@
39
40 from functools import partial
41
42-from charmhelpers.core.hookenv import unit_get
43 from charmhelpers.fetch import apt_install, apt_update
44 from charmhelpers.core.hookenv import (
45+ config,
46 log,
47+ network_get_primary_address,
48+ unit_get,
49 WARNING,
50 )
51
52+from charmhelpers.core.host import (
53+ lsb_release,
54+ CompareHostReleases,
55+)
56+
57 try:
58 import netifaces
59 except ImportError:
60 apt_update(fatal=True)
61- apt_install('python-netifaces', fatal=True)
62+ if six.PY2:
63+ apt_install('python-netifaces', fatal=True)
64+ else:
65+ apt_install('python3-netifaces', fatal=True)
66 import netifaces
67
68 try:
69 import netaddr
70 except ImportError:
71 apt_update(fatal=True)
72- apt_install('python-netaddr', fatal=True)
73+ if six.PY2:
74+ apt_install('python-netaddr', fatal=True)
75+ else:
76+ apt_install('python3-netaddr', fatal=True)
77 import netaddr
78
79
80@@ -55,6 +68,24 @@
81 raise ValueError(errmsg)
82
83
84+def _get_ipv6_network_from_address(address):
85+ """Get an netaddr.IPNetwork for the given IPv6 address
86+ :param address: a dict as returned by netifaces.ifaddresses
87+ :returns netaddr.IPNetwork: None if the address is a link local or loopback
88+ address
89+ """
90+ if address['addr'].startswith('fe80') or address['addr'] == "::1":
91+ return None
92+
93+ prefix = address['netmask'].split("/")
94+ if len(prefix) > 1:
95+ netmask = prefix[1]
96+ else:
97+ netmask = address['netmask']
98+ return netaddr.IPNetwork("%s/%s" % (address['addr'],
99+ netmask))
100+
101+
102 def get_address_in_network(network, fallback=None, fatal=False):
103 """Get an IPv4 or IPv6 address within the network from the host.
104
105@@ -80,19 +111,17 @@
106 for iface in netifaces.interfaces():
107 addresses = netifaces.ifaddresses(iface)
108 if network.version == 4 and netifaces.AF_INET in addresses:
109- addr = addresses[netifaces.AF_INET][0]['addr']
110- netmask = addresses[netifaces.AF_INET][0]['netmask']
111- cidr = netaddr.IPNetwork("%s/%s" % (addr, netmask))
112- if cidr in network:
113- return str(cidr.ip)
114+ for addr in addresses[netifaces.AF_INET]:
115+ cidr = netaddr.IPNetwork("%s/%s" % (addr['addr'],
116+ addr['netmask']))
117+ if cidr in network:
118+ return str(cidr.ip)
119
120 if network.version == 6 and netifaces.AF_INET6 in addresses:
121 for addr in addresses[netifaces.AF_INET6]:
122- if not addr['addr'].startswith('fe80'):
123- cidr = netaddr.IPNetwork("%s/%s" % (addr['addr'],
124- addr['netmask']))
125- if cidr in network:
126- return str(cidr.ip)
127+ cidr = _get_ipv6_network_from_address(addr)
128+ if cidr and cidr in network:
129+ return str(cidr.ip)
130
131 if fallback is not None:
132 return fallback
133@@ -168,18 +197,18 @@
134
135 if address.version == 6 and netifaces.AF_INET6 in addresses:
136 for addr in addresses[netifaces.AF_INET6]:
137- if not addr['addr'].startswith('fe80'):
138- network = netaddr.IPNetwork("%s/%s" % (addr['addr'],
139- addr['netmask']))
140- cidr = network.cidr
141- if address in cidr:
142- if key == 'iface':
143- return iface
144- elif key == 'netmask' and cidr:
145- return str(cidr).split('/')[1]
146- else:
147- return addr[key]
148+ network = _get_ipv6_network_from_address(addr)
149+ if not network:
150+ continue
151
152+ cidr = network.cidr
153+ if address in cidr:
154+ if key == 'iface':
155+ return iface
156+ elif key == 'netmask' and cidr:
157+ return str(cidr).split('/')[1]
158+ else:
159+ return addr[key]
160 return None
161
162
163@@ -210,6 +239,16 @@
164 return None
165
166
167+def is_ipv6_disabled():
168+ try:
169+ result = subprocess.check_output(
170+ ['sysctl', 'net.ipv6.conf.all.disable_ipv6'],
171+ stderr=subprocess.STDOUT)
172+ return "net.ipv6.conf.all.disable_ipv6 = 1" in result
173+ except subprocess.CalledProcessError:
174+ return True
175+
176+
177 def get_iface_addr(iface='eth0', inet_type='AF_INET', inc_aliases=False,
178 fatal=True, exc_list=None):
179 """Return the assigned IP address for a given interface, if any.
180@@ -406,7 +445,7 @@
181 # Test to see if already an IPv4/IPv6 address
182 address = netaddr.IPAddress(address)
183 return True
184- except netaddr.AddrFormatError:
185+ except (netaddr.AddrFormatError, ValueError):
186 return False
187
188
189@@ -414,7 +453,10 @@
190 try:
191 import dns.resolver
192 except ImportError:
193- apt_install('python-dnspython', fatal=True)
194+ if six.PY2:
195+ apt_install('python-dnspython', fatal=True)
196+ else:
197+ apt_install('python3-dnspython', fatal=True)
198 import dns.resolver
199
200 if isinstance(address, dns.name.Name):
201@@ -424,7 +466,11 @@
202 else:
203 return None
204
205- answers = dns.resolver.query(address, rtype)
206+ try:
207+ answers = dns.resolver.query(address, rtype)
208+ except dns.resolver.NXDOMAIN:
209+ return None
210+
211 if answers:
212 return str(answers[0])
213 return None
214@@ -458,7 +504,10 @@
215 try:
216 import dns.reversename
217 except ImportError:
218- apt_install("python-dnspython", fatal=True)
219+ if six.PY2:
220+ apt_install("python-dnspython", fatal=True)
221+ else:
222+ apt_install("python3-dnspython", fatal=True)
223 import dns.reversename
224
225 rev = dns.reversename.from_address(address)
226@@ -495,3 +544,48 @@
227 cmd = ['nc', '-z', address, str(port)]
228 result = subprocess.call(cmd)
229 return not(bool(result))
230+
231+
232+def assert_charm_supports_ipv6():
233+ """Check whether we are able to support charms ipv6."""
234+ release = lsb_release()['DISTRIB_CODENAME'].lower()
235+ if CompareHostReleases(release) < "trusty":
236+ raise Exception("IPv6 is not supported in the charms for Ubuntu "
237+ "versions less than Trusty 14.04")
238+
239+
240+def get_relation_ip(interface, cidr_network=None):
241+ """Return this unit's IP for the given interface.
242+
243+ Allow for an arbitrary interface to use with network-get to select an IP.
244+ Handle all address selection options including passed cidr network and
245+ IPv6.
246+
247+ Usage: get_relation_ip('amqp', cidr_network='10.0.0.0/8')
248+
249+ @param interface: string name of the relation.
250+ @param cidr_network: string CIDR Network to select an address from.
251+ @raises Exception if prefer-ipv6 is configured but IPv6 unsupported.
252+ @returns IPv6 or IPv4 address
253+ """
254+ # Select the interface address first
255+ # For possible use as a fallback bellow with get_address_in_network
256+ try:
257+ # Get the interface specific IP
258+ address = network_get_primary_address(interface)
259+ except NotImplementedError:
260+ # If network-get is not available
261+ address = get_host_ip(unit_get('private-address'))
262+
263+ if config('prefer-ipv6'):
264+ # Currently IPv6 has priority, eventually we want IPv6 to just be
265+ # another network space.
266+ assert_charm_supports_ipv6()
267+ return get_ipv6_addr()[0]
268+ elif cidr_network:
269+ # If a specific CIDR network is passed get the address from that
270+ # network.
271+ return get_address_in_network(cidr_network, address)
272+
273+ # Return the interface address
274+ return address
275
276=== modified file 'hooks/charmhelpers/contrib/network/ovs/__init__.py'
277--- hooks/charmhelpers/contrib/network/ovs/__init__.py 2016-09-09 12:18:22 +0000
278+++ hooks/charmhelpers/contrib/network/ovs/__init__.py 2017-05-05 10:32:43 +0000
279@@ -15,13 +15,30 @@
280 ''' Helpers for interacting with OpenvSwitch '''
281 import subprocess
282 import os
283+import six
284+
285+from charmhelpers.fetch import apt_install
286+
287+
288 from charmhelpers.core.hookenv import (
289- log, WARNING
290+ log, WARNING, INFO, DEBUG
291 )
292 from charmhelpers.core.host import (
293 service
294 )
295
296+BRIDGE_TEMPLATE = """\
297+# This veth pair is required when neutron data-port is mapped to an existing linux bridge. lp:1635067
298+
299+auto {linuxbridge_port}
300+iface {linuxbridge_port} inet manual
301+ pre-up ip link add name {linuxbridge_port} type veth peer name {ovsbridge_port}
302+ pre-up ip link set {ovsbridge_port} master {bridge}
303+ pre-up ip link set {ovsbridge_port} up
304+ up ip link set {linuxbridge_port} up
305+ down ip link del {linuxbridge_port}
306+"""
307+
308
309 def add_bridge(name, datapath_type=None):
310 ''' Add the named bridge to openvswitch '''
311@@ -60,6 +77,54 @@
312 subprocess.check_call(["ip", "link", "set", port, "promisc", "off"])
313
314
315+def add_ovsbridge_linuxbridge(name, bridge):
316+ ''' Add linux bridge to the named openvswitch bridge
317+ :param name: Name of ovs bridge to be added to Linux bridge
318+ :param bridge: Name of Linux bridge to be added to ovs bridge
319+ :returns: True if veth is added between ovs bridge and linux bridge,
320+ False otherwise'''
321+ try:
322+ import netifaces
323+ except ImportError:
324+ if six.PY2:
325+ apt_install('python-netifaces', fatal=True)
326+ else:
327+ apt_install('python3-netifaces', fatal=True)
328+ import netifaces
329+
330+ ovsbridge_port = "veth-" + name
331+ linuxbridge_port = "veth-" + bridge
332+ log('Adding linuxbridge {} to ovsbridge {}'.format(bridge, name),
333+ level=INFO)
334+ interfaces = netifaces.interfaces()
335+ for interface in interfaces:
336+ if interface == ovsbridge_port or interface == linuxbridge_port:
337+ log('Interface {} already exists'.format(interface), level=INFO)
338+ return
339+
340+ with open('/etc/network/interfaces.d/{}.cfg'.format(
341+ linuxbridge_port), 'w') as config:
342+ config.write(BRIDGE_TEMPLATE.format(linuxbridge_port=linuxbridge_port,
343+ ovsbridge_port=ovsbridge_port,
344+ bridge=bridge))
345+
346+ subprocess.check_call(["ifup", linuxbridge_port])
347+ add_bridge_port(name, linuxbridge_port)
348+
349+
350+def is_linuxbridge_interface(port):
351+ ''' Check if the interface is a linuxbridge bridge
352+ :param port: Name of an interface to check whether it is a Linux bridge
353+ :returns: True if port is a Linux bridge'''
354+
355+ if os.path.exists('/sys/class/net/' + port + '/bridge'):
356+ log('Interface {} is a Linux bridge'.format(port), level=DEBUG)
357+ return True
358+ else:
359+ log('Interface {} is not a Linux bridge'.format(port), level=DEBUG)
360+ return False
361+
362+
363 def set_manager(manager):
364 ''' Set the controller for the local openvswitch '''
365 log('Setting manager for local ovs to {}'.format(manager))
366
367=== modified file 'hooks/charmhelpers/core/hookenv.py'
368--- hooks/charmhelpers/core/hookenv.py 2016-09-09 12:18:22 +0000
369+++ hooks/charmhelpers/core/hookenv.py 2017-05-05 10:32:43 +0000
370@@ -332,6 +332,8 @@
371 config_cmd_line = ['config-get']
372 if scope is not None:
373 config_cmd_line.append(scope)
374+ else:
375+ config_cmd_line.append('--all')
376 config_cmd_line.append('--format=json')
377 try:
378 config_data = json.loads(
379@@ -614,6 +616,20 @@
380 subprocess.check_call(_args)
381
382
383+def open_ports(start, end, protocol="TCP"):
384+ """Opens a range of service network ports"""
385+ _args = ['open-port']
386+ _args.append('{}-{}/{}'.format(start, end, protocol))
387+ subprocess.check_call(_args)
388+
389+
390+def close_ports(start, end, protocol="TCP"):
391+ """Close a range of service network ports"""
392+ _args = ['close-port']
393+ _args.append('{}-{}/{}'.format(start, end, protocol))
394+ subprocess.check_call(_args)
395+
396+
397 @cached
398 def unit_get(attribute):
399 """Get the unit ID for the remote unit"""
400@@ -1019,3 +1035,34 @@
401 '''
402 cmd = ['network-get', '--primary-address', binding]
403 return subprocess.check_output(cmd).decode('UTF-8').strip()
404+
405+
406+def add_metric(*args, **kwargs):
407+ """Add metric values. Values may be expressed with keyword arguments. For
408+ metric names containing dashes, these may be expressed as one or more
409+ 'key=value' positional arguments. May only be called from the collect-metrics
410+ hook."""
411+ _args = ['add-metric']
412+ _kvpairs = []
413+ _kvpairs.extend(args)
414+ _kvpairs.extend(['{}={}'.format(k, v) for k, v in kwargs.items()])
415+ _args.extend(sorted(_kvpairs))
416+ try:
417+ subprocess.check_call(_args)
418+ return
419+ except EnvironmentError as e:
420+ if e.errno != errno.ENOENT:
421+ raise
422+ log_message = 'add-metric failed: {}'.format(' '.join(_kvpairs))
423+ log(log_message, level='INFO')
424+
425+
426+def meter_status():
427+ """Get the meter status, if running in the meter-status-changed hook."""
428+ return os.environ.get('JUJU_METER_STATUS')
429+
430+
431+def meter_info():
432+ """Get the meter status information, if running in the meter-status-changed
433+ hook."""
434+ return os.environ.get('JUJU_METER_INFO')
435
436=== modified file 'hooks/charmhelpers/core/host.py'
437--- hooks/charmhelpers/core/host.py 2016-09-09 12:18:22 +0000
438+++ hooks/charmhelpers/core/host.py 2017-05-05 10:32:43 +0000
439@@ -45,6 +45,7 @@
440 add_new_group,
441 lsb_release,
442 cmp_pkgrevno,
443+ CompareHostReleases,
444 ) # flake8: noqa -- ignore F401 for this import
445 elif __platform__ == "centos":
446 from charmhelpers.core.host_factory.centos import (
447@@ -52,44 +53,145 @@
448 add_new_group,
449 lsb_release,
450 cmp_pkgrevno,
451+ CompareHostReleases,
452 ) # flake8: noqa -- ignore F401 for this import
453
454-
455-def service_start(service_name):
456- """Start a system service"""
457- return service('start', service_name)
458-
459-
460-def service_stop(service_name):
461- """Stop a system service"""
462- return service('stop', service_name)
463-
464-
465-def service_restart(service_name):
466- """Restart a system service"""
467+UPDATEDB_PATH = '/etc/updatedb.conf'
468+
469+def service_start(service_name, **kwargs):
470+ """Start a system service.
471+
472+ The specified service name is managed via the system level init system.
473+ Some init systems (e.g. upstart) require that additional arguments be
474+ provided in order to directly control service instances whereas other init
475+ systems allow for addressing instances of a service directly by name (e.g.
476+ systemd).
477+
478+ The kwargs allow for the additional parameters to be passed to underlying
479+ init systems for those systems which require/allow for them. For example,
480+ the ceph-osd upstart script requires the id parameter to be passed along
481+ in order to identify which running daemon should be reloaded. The follow-
482+ ing example stops the ceph-osd service for instance id=4:
483+
484+ service_stop('ceph-osd', id=4)
485+
486+ :param service_name: the name of the service to stop
487+ :param **kwargs: additional parameters to pass to the init system when
488+ managing services. These will be passed as key=value
489+ parameters to the init system's commandline. kwargs
490+ are ignored for systemd enabled systems.
491+ """
492+ return service('start', service_name, **kwargs)
493+
494+
495+def service_stop(service_name, **kwargs):
496+ """Stop a system service.
497+
498+ The specified service name is managed via the system level init system.
499+ Some init systems (e.g. upstart) require that additional arguments be
500+ provided in order to directly control service instances whereas other init
501+ systems allow for addressing instances of a service directly by name (e.g.
502+ systemd).
503+
504+ The kwargs allow for the additional parameters to be passed to underlying
505+ init systems for those systems which require/allow for them. For example,
506+ the ceph-osd upstart script requires the id parameter to be passed along
507+ in order to identify which running daemon should be reloaded. The follow-
508+ ing example stops the ceph-osd service for instance id=4:
509+
510+ service_stop('ceph-osd', id=4)
511+
512+ :param service_name: the name of the service to stop
513+ :param **kwargs: additional parameters to pass to the init system when
514+ managing services. These will be passed as key=value
515+ parameters to the init system's commandline. kwargs
516+ are ignored for systemd enabled systems.
517+ """
518+ return service('stop', service_name, **kwargs)
519+
520+
521+def service_restart(service_name, **kwargs):
522+ """Restart a system service.
523+
524+ The specified service name is managed via the system level init system.
525+ Some init systems (e.g. upstart) require that additional arguments be
526+ provided in order to directly control service instances whereas other init
527+ systems allow for addressing instances of a service directly by name (e.g.
528+ systemd).
529+
530+ The kwargs allow for the additional parameters to be passed to underlying
531+ init systems for those systems which require/allow for them. For example,
532+ the ceph-osd upstart script requires the id parameter to be passed along
533+ in order to identify which running daemon should be restarted. The follow-
534+ ing example restarts the ceph-osd service for instance id=4:
535+
536+ service_restart('ceph-osd', id=4)
537+
538+ :param service_name: the name of the service to restart
539+ :param **kwargs: additional parameters to pass to the init system when
540+ managing services. These will be passed as key=value
541+ parameters to the init system's commandline. kwargs
542+ are ignored for init systems not allowing additional
543+ parameters via the commandline (systemd).
544+ """
545 return service('restart', service_name)
546
547
548-def service_reload(service_name, restart_on_failure=False):
549+def service_reload(service_name, restart_on_failure=False, **kwargs):
550 """Reload a system service, optionally falling back to restart if
551- reload fails"""
552- service_result = service('reload', service_name)
553+ reload fails.
554+
555+ The specified service name is managed via the system level init system.
556+ Some init systems (e.g. upstart) require that additional arguments be
557+ provided in order to directly control service instances whereas other init
558+ systems allow for addressing instances of a service directly by name (e.g.
559+ systemd).
560+
561+ The kwargs allow for the additional parameters to be passed to underlying
562+ init systems for those systems which require/allow for them. For example,
563+ the ceph-osd upstart script requires the id parameter to be passed along
564+ in order to identify which running daemon should be reloaded. The follow-
565+ ing example restarts the ceph-osd service for instance id=4:
566+
567+ service_reload('ceph-osd', id=4)
568+
569+ :param service_name: the name of the service to reload
570+ :param restart_on_failure: boolean indicating whether to fallback to a
571+ restart if the reload fails.
572+ :param **kwargs: additional parameters to pass to the init system when
573+ managing services. These will be passed as key=value
574+ parameters to the init system's commandline. kwargs
575+ are ignored for init systems not allowing additional
576+ parameters via the commandline (systemd).
577+ """
578+ service_result = service('reload', service_name, **kwargs)
579 if not service_result and restart_on_failure:
580- service_result = service('restart', service_name)
581+ service_result = service('restart', service_name, **kwargs)
582 return service_result
583
584
585-def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):
586+def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d",
587+ **kwargs):
588 """Pause a system service.
589
590- Stop it, and prevent it from starting again at boot."""
591+ Stop it, and prevent it from starting again at boot.
592+
593+ :param service_name: the name of the service to pause
594+ :param init_dir: path to the upstart init directory
595+ :param initd_dir: path to the sysv init directory
596+ :param **kwargs: additional parameters to pass to the init system when
597+ managing services. These will be passed as key=value
598+ parameters to the init system's commandline. kwargs
599+ are ignored for init systems which do not support
600+ key=value arguments via the commandline.
601+ """
602 stopped = True
603- if service_running(service_name):
604- stopped = service_stop(service_name)
605+ if service_running(service_name, **kwargs):
606+ stopped = service_stop(service_name, **kwargs)
607 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
608 sysv_file = os.path.join(initd_dir, service_name)
609 if init_is_systemd():
610- service('disable', service_name)
611+ service('mask', service_name)
612 elif os.path.exists(upstart_file):
613 override_path = os.path.join(
614 init_dir, '{}.override'.format(service_name))
615@@ -106,14 +208,23 @@
616
617
618 def service_resume(service_name, init_dir="/etc/init",
619- initd_dir="/etc/init.d"):
620+ initd_dir="/etc/init.d", **kwargs):
621 """Resume a system service.
622
623- Reenable starting again at boot. Start the service"""
624+ Reenable starting again at boot. Start the service.
625+
626+ :param service_name: the name of the service to resume
627+ :param init_dir: the path to the init dir
628+ :param initd dir: the path to the initd dir
629+ :param **kwargs: additional parameters to pass to the init system when
630+ managing services. These will be passed as key=value
631+ parameters to the init system's commandline. kwargs
632+ are ignored for systemd enabled systems.
633+ """
634 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
635 sysv_file = os.path.join(initd_dir, service_name)
636 if init_is_systemd():
637- service('enable', service_name)
638+ service('unmask', service_name)
639 elif os.path.exists(upstart_file):
640 override_path = os.path.join(
641 init_dir, '{}.override'.format(service_name))
642@@ -126,19 +237,28 @@
643 "Unable to detect {0} as SystemD, Upstart {1} or"
644 " SysV {2}".format(
645 service_name, upstart_file, sysv_file))
646+ started = service_running(service_name, **kwargs)
647
648- started = service_running(service_name)
649 if not started:
650- started = service_start(service_name)
651+ started = service_start(service_name, **kwargs)
652 return started
653
654
655-def service(action, service_name):
656- """Control a system service"""
657+def service(action, service_name, **kwargs):
658+ """Control a system service.
659+
660+ :param action: the action to take on the service
661+ :param service_name: the name of the service to perform th action on
662+ :param **kwargs: additional params to be passed to the service command in
663+ the form of key=value.
664+ """
665 if init_is_systemd():
666 cmd = ['systemctl', action, service_name]
667 else:
668 cmd = ['service', service_name, action]
669+ for key, value in six.iteritems(kwargs):
670+ parameter = '%s=%s' % (key, value)
671+ cmd.append(parameter)
672 return subprocess.call(cmd) == 0
673
674
675@@ -146,15 +266,26 @@
676 _INIT_D_CONF = "/etc/init.d/{}"
677
678
679-def service_running(service_name):
680- """Determine whether a system service is running"""
681+def service_running(service_name, **kwargs):
682+ """Determine whether a system service is running.
683+
684+ :param service_name: the name of the service
685+ :param **kwargs: additional args to pass to the service command. This is
686+ used to pass additional key=value arguments to the
687+ service command line for managing specific instance
688+ units (e.g. service ceph-osd status id=2). The kwargs
689+ are ignored in systemd services.
690+ """
691 if init_is_systemd():
692 return service('is-active', service_name)
693 else:
694 if os.path.exists(_UPSTART_CONF.format(service_name)):
695 try:
696- output = subprocess.check_output(
697- ['status', service_name],
698+ cmd = ['status', service_name]
699+ for key, value in six.iteritems(kwargs):
700+ parameter = '%s=%s' % (key, value)
701+ cmd.append(parameter)
702+ output = subprocess.check_output(cmd,
703 stderr=subprocess.STDOUT).decode('UTF-8')
704 except subprocess.CalledProcessError:
705 return False
706@@ -177,6 +308,8 @@
707
708 def init_is_systemd():
709 """Return True if the host system uses systemd, False otherwise."""
710+ if lsb_release()['DISTRIB_CODENAME'] == 'trusty':
711+ return False
712 return os.path.isdir(SYSTEMD_SYSTEM)
713
714
715@@ -306,15 +439,17 @@
716 subprocess.check_call(cmd)
717
718
719-def rsync(from_path, to_path, flags='-r', options=None):
720+def rsync(from_path, to_path, flags='-r', options=None, timeout=None):
721 """Replicate the contents of a path"""
722 options = options or ['--delete', '--executability']
723 cmd = ['/usr/bin/rsync', flags]
724+ if timeout:
725+ cmd = ['timeout', str(timeout)] + cmd
726 cmd.extend(options)
727 cmd.append(from_path)
728 cmd.append(to_path)
729 log(" ".join(cmd))
730- return subprocess.check_output(cmd).decode('UTF-8').strip()
731+ return subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode('UTF-8').strip()
732
733
734 def symlink(source, destination):
735@@ -684,7 +819,7 @@
736 :param str path: The string path to start changing ownership.
737 :param str owner: The owner string to use when looking up the uid.
738 :param str group: The group string to use when looking up the gid.
739- :param bool follow_links: Also Chown links if True
740+ :param bool follow_links: Also follow and chown links if True
741 :param bool chowntopdir: Also chown path itself if True
742 """
743 uid = pwd.getpwnam(owner).pw_uid
744@@ -698,7 +833,7 @@
745 broken_symlink = os.path.lexists(path) and not os.path.exists(path)
746 if not broken_symlink:
747 chown(path, uid, gid)
748- for root, dirs, files in os.walk(path):
749+ for root, dirs, files in os.walk(path, followlinks=follow_links):
750 for name in dirs + files:
751 full = os.path.join(root, name)
752 broken_symlink = os.path.lexists(full) and not os.path.exists(full)
753@@ -718,6 +853,20 @@
754 chownr(path, owner, group, follow_links=False)
755
756
757+def owner(path):
758+ """Returns a tuple containing the username & groupname owning the path.
759+
760+ :param str path: the string path to retrieve the ownership
761+ :return tuple(str, str): A (username, groupname) tuple containing the
762+ name of the user and group owning the path.
763+ :raises OSError: if the specified path does not exist
764+ """
765+ stat = os.stat(path)
766+ username = pwd.getpwuid(stat.st_uid)[0]
767+ groupname = grp.getgrgid(stat.st_gid)[0]
768+ return username, groupname
769+
770+
771 def get_total_ram():
772 """The total amount of system RAM in bytes.
773
774@@ -732,3 +881,42 @@
775 assert unit == 'kB', 'Unknown unit'
776 return int(value) * 1024 # Classic, not KiB.
777 raise NotImplementedError()
778+
779+
780+UPSTART_CONTAINER_TYPE = '/run/container_type'
781+
782+
783+def is_container():
784+ """Determine whether unit is running in a container
785+
786+ @return: boolean indicating if unit is in a container
787+ """
788+ if init_is_systemd():
789+ # Detect using systemd-detect-virt
790+ return subprocess.call(['systemd-detect-virt',
791+ '--container']) == 0
792+ else:
793+ # Detect using upstart container file marker
794+ return os.path.exists(UPSTART_CONTAINER_TYPE)
795+
796+
797+def add_to_updatedb_prunepath(path, updatedb_path=UPDATEDB_PATH):
798+ with open(updatedb_path, 'r+') as f_id:
799+ updatedb_text = f_id.read()
800+ output = updatedb(updatedb_text, path)
801+ f_id.seek(0)
802+ f_id.write(output)
803+ f_id.truncate()
804+
805+
806+def updatedb(updatedb_text, new_path):
807+ lines = [line for line in updatedb_text.split("\n")]
808+ for i, line in enumerate(lines):
809+ if line.startswith("PRUNEPATHS="):
810+ paths_line = line.split("=")[1].replace('"', '')
811+ paths = paths_line.split(" ")
812+ if new_path not in paths:
813+ paths.append(new_path)
814+ lines[i] = 'PRUNEPATHS="{}"'.format(' '.join(paths))
815+ output = "\n".join(lines)
816+ return output
817
818=== modified file 'hooks/charmhelpers/core/host_factory/centos.py'
819--- hooks/charmhelpers/core/host_factory/centos.py 2016-09-09 12:18:22 +0000
820+++ hooks/charmhelpers/core/host_factory/centos.py 2017-05-05 10:32:43 +0000
821@@ -2,6 +2,22 @@
822 import yum
823 import os
824
825+from charmhelpers.core.strutils import BasicStringComparator
826+
827+
828+class CompareHostReleases(BasicStringComparator):
829+ """Provide comparisons of Host releases.
830+
831+ Use in the form of
832+
833+ if CompareHostReleases(release) > 'trusty':
834+ # do something with mitaka
835+ """
836+
837+ def __init__(self, item):
838+ raise NotImplementedError(
839+ "CompareHostReleases() is not implemented for CentOS")
840+
841
842 def service_available(service_name):
843 # """Determine whether a system service is available."""
844
845=== modified file 'hooks/charmhelpers/core/host_factory/ubuntu.py'
846--- hooks/charmhelpers/core/host_factory/ubuntu.py 2016-09-09 12:18:22 +0000
847+++ hooks/charmhelpers/core/host_factory/ubuntu.py 2017-05-05 10:32:43 +0000
848@@ -1,5 +1,37 @@
849 import subprocess
850
851+from charmhelpers.core.strutils import BasicStringComparator
852+
853+
854+UBUNTU_RELEASES = (
855+ 'lucid',
856+ 'maverick',
857+ 'natty',
858+ 'oneiric',
859+ 'precise',
860+ 'quantal',
861+ 'raring',
862+ 'saucy',
863+ 'trusty',
864+ 'utopic',
865+ 'vivid',
866+ 'wily',
867+ 'xenial',
868+ 'yakkety',
869+ 'zesty',
870+)
871+
872+
873+class CompareHostReleases(BasicStringComparator):
874+ """Provide comparisons of Ubuntu releases.
875+
876+ Use in the form of
877+
878+ if CompareHostReleases(release) > 'trusty':
879+ # do something with mitaka
880+ """
881+ _list = UBUNTU_RELEASES
882+
883
884 def service_available(service_name):
885 """Determine whether a system service is available"""
886
887=== modified file 'hooks/charmhelpers/core/kernel_factory/ubuntu.py'
888--- hooks/charmhelpers/core/kernel_factory/ubuntu.py 2016-09-09 12:18:22 +0000
889+++ hooks/charmhelpers/core/kernel_factory/ubuntu.py 2017-05-05 10:32:43 +0000
890@@ -5,7 +5,7 @@
891 """Load a kernel module and configure for auto-load on reboot."""
892 with open('/etc/modules', 'r+') as modules:
893 if module not in modules.read():
894- modules.write(module)
895+ modules.write(module + "\n")
896
897
898 def update_initramfs(version='all'):
899
900=== modified file 'hooks/charmhelpers/core/strutils.py'
901--- hooks/charmhelpers/core/strutils.py 2016-09-09 12:18:22 +0000
902+++ hooks/charmhelpers/core/strutils.py 2017-05-05 10:32:43 +0000
903@@ -68,3 +68,56 @@
904 msg = "Unable to interpret string value '%s' as bytes" % (value)
905 raise ValueError(msg)
906 return int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)])
907+
908+
909+class BasicStringComparator(object):
910+ """Provides a class that will compare strings from an iterator type object.
911+ Used to provide > and < comparisons on strings that may not necessarily be
912+ alphanumerically ordered. e.g. OpenStack or Ubuntu releases AFTER the
913+ z-wrap.
914+ """
915+
916+ _list = None
917+
918+ def __init__(self, item):
919+ if self._list is None:
920+ raise Exception("Must define the _list in the class definition!")
921+ try:
922+ self.index = self._list.index(item)
923+ except Exception:
924+ raise KeyError("Item '{}' is not in list '{}'"
925+ .format(item, self._list))
926+
927+ def __eq__(self, other):
928+ assert isinstance(other, str) or isinstance(other, self.__class__)
929+ return self.index == self._list.index(other)
930+
931+ def __ne__(self, other):
932+ return not self.__eq__(other)
933+
934+ def __lt__(self, other):
935+ assert isinstance(other, str) or isinstance(other, self.__class__)
936+ return self.index < self._list.index(other)
937+
938+ def __ge__(self, other):
939+ return not self.__lt__(other)
940+
941+ def __gt__(self, other):
942+ assert isinstance(other, str) or isinstance(other, self.__class__)
943+ return self.index > self._list.index(other)
944+
945+ def __le__(self, other):
946+ return not self.__gt__(other)
947+
948+ def __str__(self):
949+ """Always give back the item at the index so it can be used in
950+ comparisons like:
951+
952+ s_mitaka = CompareOpenStack('mitaka')
953+ s_newton = CompareOpenstack('newton')
954+
955+ assert s_newton > s_mitaka
956+
957+ @returns: <string>
958+ """
959+ return self._list[self.index]
960
961=== modified file 'hooks/charmhelpers/fetch/__init__.py'
962--- hooks/charmhelpers/fetch/__init__.py 2016-09-09 12:18:22 +0000
963+++ hooks/charmhelpers/fetch/__init__.py 2017-05-05 10:32:43 +0000
964@@ -92,6 +92,7 @@
965 apt_mark = fetch.apt_mark
966 apt_hold = fetch.apt_hold
967 apt_unhold = fetch.apt_unhold
968+ get_upstream_version = fetch.get_upstream_version
969 elif __platform__ == "centos":
970 yum_search = fetch.yum_search
971
972
973=== added file 'hooks/charmhelpers/fetch/snap.py'
974--- hooks/charmhelpers/fetch/snap.py 1970-01-01 00:00:00 +0000
975+++ hooks/charmhelpers/fetch/snap.py 2017-05-05 10:32:43 +0000
976@@ -0,0 +1,122 @@
977+# Copyright 2014-2017 Canonical Limited.
978+#
979+# Licensed under the Apache License, Version 2.0 (the "License");
980+# you may not use this file except in compliance with the License.
981+# You may obtain a copy of the License at
982+#
983+# http://www.apache.org/licenses/LICENSE-2.0
984+#
985+# Unless required by applicable law or agreed to in writing, software
986+# distributed under the License is distributed on an "AS IS" BASIS,
987+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
988+# See the License for the specific language governing permissions and
989+# limitations under the License.
990+"""
991+Charm helpers snap for classic charms.
992+
993+If writing reactive charms, use the snap layer:
994+https://lists.ubuntu.com/archives/snapcraft/2016-September/001114.html
995+"""
996+import subprocess
997+from os import environ
998+from time import sleep
999+from charmhelpers.core.hookenv import log
1000+
1001+__author__ = 'Joseph Borg <joseph.borg@canonical.com>'
1002+
1003+SNAP_NO_LOCK = 1 # The return code for "couldn't acquire lock" in Snap (hopefully this will be improved).
1004+SNAP_NO_LOCK_RETRY_DELAY = 10 # Wait X seconds between Snap lock checks.
1005+SNAP_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
1006+
1007+
1008+class CouldNotAcquireLockException(Exception):
1009+ pass
1010+
1011+
1012+def _snap_exec(commands):
1013+ """
1014+ Execute snap commands.
1015+
1016+ :param commands: List commands
1017+ :return: Integer exit code
1018+ """
1019+ assert type(commands) == list
1020+
1021+ retry_count = 0
1022+ return_code = None
1023+
1024+ while return_code is None or return_code == SNAP_NO_LOCK:
1025+ try:
1026+ return_code = subprocess.check_call(['snap'] + commands, env=environ)
1027+ except subprocess.CalledProcessError as e:
1028+ retry_count += + 1
1029+ if retry_count > SNAP_NO_LOCK_RETRY_COUNT:
1030+ raise CouldNotAcquireLockException('Could not aquire lock after %s attempts' % SNAP_NO_LOCK_RETRY_COUNT)
1031+ return_code = e.returncode
1032+ log('Snap failed to acquire lock, trying again in %s seconds.' % SNAP_NO_LOCK_RETRY_DELAY, level='WARN')
1033+ sleep(SNAP_NO_LOCK_RETRY_DELAY)
1034+
1035+ return return_code
1036+
1037+
1038+def snap_install(packages, *flags):
1039+ """
1040+ Install a snap package.
1041+
1042+ :param packages: String or List String package name
1043+ :param flags: List String flags to pass to install command
1044+ :return: Integer return code from snap
1045+ """
1046+ if type(packages) is not list:
1047+ packages = [packages]
1048+
1049+ flags = list(flags)
1050+
1051+ message = 'Installing snap(s) "%s"' % ', '.join(packages)
1052+ if flags:
1053+ message += ' with option(s) "%s"' % ', '.join(flags)
1054+
1055+ log(message, level='INFO')
1056+ return _snap_exec(['install'] + flags + packages)
1057+
1058+
1059+def snap_remove(packages, *flags):
1060+ """
1061+ Remove a snap package.
1062+
1063+ :param packages: String or List String package name
1064+ :param flags: List String flags to pass to remove command
1065+ :return: Integer return code from snap
1066+ """
1067+ if type(packages) is not list:
1068+ packages = [packages]
1069+
1070+ flags = list(flags)
1071+
1072+ message = 'Removing snap(s) "%s"' % ', '.join(packages)
1073+ if flags:
1074+ message += ' with options "%s"' % ', '.join(flags)
1075+
1076+ log(message, level='INFO')
1077+ return _snap_exec(['remove'] + flags + packages)
1078+
1079+
1080+def snap_refresh(packages, *flags):
1081+ """
1082+ Refresh / Update snap package.
1083+
1084+ :param packages: String or List String package name
1085+ :param flags: List String flags to pass to refresh command
1086+ :return: Integer return code from snap
1087+ """
1088+ if type(packages) is not list:
1089+ packages = [packages]
1090+
1091+ flags = list(flags)
1092+
1093+ message = 'Refreshing snap(s) "%s"' % ', '.join(packages)
1094+ if flags:
1095+ message += ' with options "%s"' % ', '.join(flags)
1096+
1097+ log(message, level='INFO')
1098+ return _snap_exec(['refresh'] + flags + packages)
1099
1100=== modified file 'hooks/charmhelpers/fetch/ubuntu.py'
1101--- hooks/charmhelpers/fetch/ubuntu.py 2016-09-09 12:18:22 +0000
1102+++ hooks/charmhelpers/fetch/ubuntu.py 2017-05-05 10:32:43 +0000
1103@@ -24,11 +24,14 @@
1104 from charmhelpers.core.hookenv import log
1105 from charmhelpers.fetch import SourceConfigError
1106
1107-CLOUD_ARCHIVE = ('# Ubuntu Cloud Archive deb'
1108- ' http://ubuntu-cloud.archive.canonical.com/ubuntu'
1109- ' {} main')
1110-PROPOSED_POCKET = ('# Proposed deb http://archive.ubuntu.com/ubuntu'
1111- ' {}-proposed main universe multiverse restricted')
1112+CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
1113+deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
1114+"""
1115+
1116+PROPOSED_POCKET = """# Proposed
1117+deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
1118+"""
1119+
1120 CLOUD_ARCHIVE_POCKETS = {
1121 # Folsom
1122 'folsom': 'precise-updates/folsom',
1123@@ -102,11 +105,19 @@
1124 'newton/proposed': 'xenial-proposed/newton',
1125 'xenial-newton/proposed': 'xenial-proposed/newton',
1126 'xenial-proposed/newton': 'xenial-proposed/newton',
1127+ # Ocata
1128+ 'ocata': 'xenial-updates/ocata',
1129+ 'xenial-ocata': 'xenial-updates/ocata',
1130+ 'xenial-ocata/updates': 'xenial-updates/ocata',
1131+ 'xenial-updates/ocata': 'xenial-updates/ocata',
1132+ 'ocata/proposed': 'xenial-proposed/ocata',
1133+ 'xenial-ocata/proposed': 'xenial-proposed/ocata',
1134+ 'xenial-ocata/newton': 'xenial-proposed/ocata',
1135 }
1136
1137 APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
1138-APT_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks.
1139-APT_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
1140+CMD_RETRY_DELAY = 10 # Wait 10 seconds between command retries.
1141+CMD_RETRY_COUNT = 30 # Retry a failing fatal command X times.
1142
1143
1144 def filter_installed_packages(packages):
1145@@ -238,7 +249,8 @@
1146 source.startswith('http') or
1147 source.startswith('deb ') or
1148 source.startswith('cloud-archive:')):
1149- subprocess.check_call(['add-apt-repository', '--yes', source])
1150+ cmd = ['add-apt-repository', '--yes', source]
1151+ _run_with_retries(cmd)
1152 elif source.startswith('cloud:'):
1153 install(filter_installed_packages(['ubuntu-cloud-keyring']),
1154 fatal=True)
1155@@ -275,39 +287,78 @@
1156 key])
1157
1158
1159+def _run_with_retries(cmd, max_retries=CMD_RETRY_COUNT, retry_exitcodes=(1,),
1160+ retry_message="", cmd_env=None):
1161+ """Run a command and retry until success or max_retries is reached.
1162+
1163+ :param: cmd: str: The apt command to run.
1164+ :param: max_retries: int: The number of retries to attempt on a fatal
1165+ command. Defaults to CMD_RETRY_COUNT.
1166+ :param: retry_exitcodes: tuple: Optional additional exit codes to retry.
1167+ Defaults to retry on exit code 1.
1168+ :param: retry_message: str: Optional log prefix emitted during retries.
1169+ :param: cmd_env: dict: Environment variables to add to the command run.
1170+ """
1171+
1172+ env = os.environ.copy()
1173+ if cmd_env:
1174+ env.update(cmd_env)
1175+
1176+ if not retry_message:
1177+ retry_message = "Failed executing '{}'".format(" ".join(cmd))
1178+ retry_message += ". Will retry in {} seconds".format(CMD_RETRY_DELAY)
1179+
1180+ retry_count = 0
1181+ result = None
1182+
1183+ retry_results = (None,) + retry_exitcodes
1184+ while result in retry_results:
1185+ try:
1186+ result = subprocess.check_call(cmd, env=env)
1187+ except subprocess.CalledProcessError as e:
1188+ retry_count = retry_count + 1
1189+ if retry_count > max_retries:
1190+ raise
1191+ result = e.returncode
1192+ log(retry_message)
1193+ time.sleep(CMD_RETRY_DELAY)
1194+
1195+
1196 def _run_apt_command(cmd, fatal=False):
1197- """Run an APT command.
1198-
1199- Checks the output and retries if the fatal flag is set
1200- to True.
1201-
1202- :param: cmd: str: The apt command to run.
1203+ """Run an apt command with optional retries.
1204+
1205 :param: fatal: bool: Whether the command's output should be checked and
1206 retried.
1207 """
1208- env = os.environ.copy()
1209-
1210- if 'DEBIAN_FRONTEND' not in env:
1211- env['DEBIAN_FRONTEND'] = 'noninteractive'
1212+ # Provide DEBIAN_FRONTEND=noninteractive if not present in the environment.
1213+ cmd_env = {
1214+ 'DEBIAN_FRONTEND': os.environ.get('DEBIAN_FRONTEND', 'noninteractive')}
1215
1216 if fatal:
1217- retry_count = 0
1218- result = None
1219-
1220- # If the command is considered "fatal", we need to retry if the apt
1221- # lock was not acquired.
1222-
1223- while result is None or result == APT_NO_LOCK:
1224- try:
1225- result = subprocess.check_call(cmd, env=env)
1226- except subprocess.CalledProcessError as e:
1227- retry_count = retry_count + 1
1228- if retry_count > APT_NO_LOCK_RETRY_COUNT:
1229- raise
1230- result = e.returncode
1231- log("Couldn't acquire DPKG lock. Will retry in {} seconds."
1232- "".format(APT_NO_LOCK_RETRY_DELAY))
1233- time.sleep(APT_NO_LOCK_RETRY_DELAY)
1234-
1235+ _run_with_retries(
1236+ cmd, cmd_env=cmd_env, retry_exitcodes=(1, APT_NO_LOCK,),
1237+ retry_message="Couldn't acquire DPKG lock")
1238 else:
1239+ env = os.environ.copy()
1240+ env.update(cmd_env)
1241 subprocess.call(cmd, env=env)
1242+
1243+
1244+def get_upstream_version(package):
1245+ """Determine upstream version based on installed package
1246+
1247+ @returns None (if not installed) or the upstream version
1248+ """
1249+ import apt_pkg
1250+ cache = apt_cache()
1251+ try:
1252+ pkg = cache[package]
1253+ except:
1254+ # the package is unknown to the current apt cache.
1255+ return None
1256+
1257+ if not pkg.current_ver:
1258+ # package is known, but no version is currently installed.
1259+ return None
1260+
1261+ return apt_pkg.upstream_version(pkg.current_ver.ver_str)
1262
1263=== modified file 'hooks/charmhelpers/osplatform.py'
1264--- hooks/charmhelpers/osplatform.py 2016-09-09 13:00:43 +0000
1265+++ hooks/charmhelpers/osplatform.py 2017-05-05 10:32:43 +0000
1266@@ -8,12 +8,18 @@
1267 will be returned (which is the name of the module).
1268 This string is used to decide which platform module should be imported.
1269 """
1270+ # linux_distribution is deprecated and will be removed in Python 3.7
1271+ # Warings *not* disabled, as we certainly need to fix this.
1272 tuple_platform = platform.linux_distribution()
1273 current_platform = tuple_platform[0]
1274 if "Ubuntu" in current_platform:
1275 return "ubuntu"
1276 elif "CentOS" in current_platform:
1277 return "centos"
1278+ elif "debian" in current_platform:
1279+ # Stock Python does not detect Ubuntu and instead returns debian.
1280+ # Or at least it does in some build environments like Travis CI
1281+ return "ubuntu"
1282 else:
1283 raise RuntimeError("This module is not supported on {}."
1284 .format(current_platform))
1285
1286=== modified file 'hooks/memcached_hooks.py'
1287--- hooks/memcached_hooks.py 2016-09-20 18:01:20 +0000
1288+++ hooks/memcached_hooks.py 2017-05-05 10:32:43 +0000
1289@@ -10,10 +10,10 @@
1290 config,
1291 local_unit,
1292 log,
1293+ open_port,
1294 relation_ids,
1295 relation_get,
1296 relation_set,
1297- unit_get,
1298 hook_name,
1299 Hooks,
1300 UnregisteredHookError,
1301@@ -31,11 +31,16 @@
1302 peer_units,
1303 )
1304
1305-from charmhelpers.core.hookenv import open_port
1306 from charmhelpers.fetch import apt_install, apt_update, add_source
1307 from charmhelpers.contrib.network import ufw
1308+from charmhelpers.contrib.network.ip import get_relation_ip
1309
1310-import memcached_utils
1311+from memcached_utils import (
1312+ dpkg_info_contains,
1313+ grant_access,
1314+ munin_format_ip,
1315+ revoke_access,
1316+)
1317 import replication
1318
1319 __author__ = 'Felipe Reyes <felipe.reyes@canonical.com>'
1320@@ -127,7 +132,8 @@
1321 level='INFO')
1322 secondary = relation_get('replica')
1323 try:
1324- if secondary == unit_get('private-address'):
1325+ this_address = get_relation_ip('cluster')
1326+ if secondary == this_address:
1327 replication.store_replica(relation_get('master'), secondary)
1328 return config_changed(replica=relation_get('master'))
1329 elif secondary is None:
1330@@ -148,7 +154,9 @@
1331 @hooks.hook('cache-relation-joined')
1332 def cache_relation_joined():
1333
1334- settings = {'host': unit_get('private-address'),
1335+ # advertise our network space bind address, if set, otherwise fall back to
1336+ # the unit_get
1337+ settings = {'host': get_relation_ip('cache'),
1338 'port': config('tcp-port'),
1339 'udp-port': config('udp-port')}
1340
1341@@ -158,14 +166,14 @@
1342 addr = relation_get('private-address')
1343 if addr:
1344 log('Granting memcached access to {}'.format(addr), level='INFO')
1345- memcached_utils.grant_access(addr)
1346+ grant_access(addr)
1347
1348
1349 @hooks.hook('cache-relation-departed')
1350 def cache_relation_departed():
1351 addr = relation_get('private-address')
1352 log('Revoking memcached access to {}'.format(addr))
1353- memcached_utils.revoke_access(addr)
1354+ revoke_access(addr)
1355
1356
1357 @hooks.hook('config-changed')
1358@@ -182,9 +190,7 @@
1359 # If repcached was enabled after install, we need
1360 # to make sure to install the memcached package with replication
1361 # enabled.
1362- if not memcached_utils.dpkg_info_contains('memcached',
1363- 'version',
1364- 'repcache'):
1365+ if not dpkg_info_contains('memcached', 'version', 'repcache'):
1366 add_source(config('repcached_origin'))
1367 apt_update(fatal=True)
1368 apt_install(["memcached"], fatal=True)
1369@@ -273,7 +279,7 @@
1370 log('Remote node must provide IP', level='INFO')
1371 sys.exit(0)
1372
1373- munin_server_ip = memcached_utils.munin_format_ip(remote_ip)
1374+ munin_server_ip = munin_format_ip(remote_ip)
1375
1376 # make sure munin port is open, the access is restricted by munin
1377 ufw.service('4949', 'open')
1378@@ -284,7 +290,7 @@
1379 apt_install(['munin-node'], fatal=True)
1380 configs = {'munin_server': munin_server_ip}
1381 templating.render('munin-node.conf', ETC_MUNIN_NODE_CONF, configs)
1382- relation_set(ip=unit_get('private-address'))
1383+ relation_set(ip=get_relation_ip('munin'))
1384
1385
1386 @hooks.hook('nrpe-external-master-relation-changed')
1387@@ -334,8 +340,9 @@
1388 with open('monitors.yaml') as mon_file:
1389 mon_data = mon_file.read()
1390
1391+ target_address = get_relation_ip('monitors')
1392 relation_set(monitors=mon_data, target_id=nagios_hostname,
1393- target_address=unit_get('private-address'))
1394+ target_address=target_address)
1395
1396 service_reload('nagios-nrpe-server')
1397
1398
1399=== modified file 'hooks/replication.py'
1400--- hooks/replication.py 2016-11-03 17:07:37 +0000
1401+++ hooks/replication.py 2017-05-05 10:32:43 +0000
1402@@ -2,7 +2,6 @@
1403 log,
1404 relation_id,
1405 relation_set,
1406- unit_get,
1407 )
1408
1409 from charmhelpers.contrib.hahelpers.cluster import (
1410@@ -10,6 +9,8 @@
1411 peer_units,
1412 peer_ips,
1413 )
1414+from charmhelpers.contrib.network.ip import get_relation_ip
1415+
1416
1417 REPCACHED_REPLICA_FILE = "/var/run/repcached_replica"
1418 REPCACHED_MASTER_WAIT = 10
1419@@ -25,7 +26,7 @@
1420
1421 if replica:
1422 master, secondary = replica
1423- ip_addr = unit_get('private-address')
1424+ ip_addr = get_relation_ip('cluster')
1425 if ip_addr == master:
1426 replica = secondary
1427 elif ip_addr == secondary:
1428@@ -44,12 +45,12 @@
1429 peer_unit = list(peer_ips().values())[0]
1430
1431 if oldest_peer(peers):
1432- master, secondary = (unit_get('private-address'),
1433+ master, secondary = (get_relation_ip('cluster'),
1434 peer_unit)
1435 replica = secondary
1436 else:
1437 master, secondary = (peer_unit,
1438- unit_get('private-address'))
1439+ get_relation_ip('cluster'))
1440 replica = master
1441
1442 store_replica(master, secondary)
1443@@ -73,7 +74,7 @@
1444 replica = list(peer_ips().values())[0]
1445 log("Setting replica unit: %s" % replica)
1446
1447- master = unit_get('private-address')
1448+ master = get_relation_ip('cluster')
1449 relation_set(relation_id(), {
1450 'master': master,
1451 'replica': replica,
1452
1453=== modified file 'unit_tests/test_memcached_hooks.py'
1454--- unit_tests/test_memcached_hooks.py 2016-11-23 21:14:14 +0000
1455+++ unit_tests/test_memcached_hooks.py 2017-05-05 10:32:43 +0000
1456@@ -17,7 +17,9 @@
1457 'relation_ids',
1458 'relation_set',
1459 'relation_get',
1460- 'unit_get',
1461+ 'get_relation_ip',
1462+ 'grant_access',
1463+ 'dpkg_info_contains',
1464 'config',
1465 'log',
1466 'oldest_peer',
1467@@ -93,10 +95,9 @@
1468
1469 @mock.patch('subprocess.check_output')
1470 @mock.patch('memcached_hooks.relation_get')
1471- @mock.patch('memcached_utils.grant_access')
1472 @mock.patch('memcached_utils.config')
1473 @mock.patch('memcached_utils.log')
1474- def test_cache_relation_joined(self, log, config, grant_access,
1475+ def test_cache_relation_joined(self, log, config,
1476 relation_get, check_output):
1477 configs = {'tcp-port': '1234', 'udp-port': '3456'}
1478
1479@@ -106,7 +107,7 @@
1480 self.config.side_effect = f
1481 config.side_effect = f
1482 relation_get.return_value = '127.0.1.1'
1483- self.unit_get.return_value = '127.0.0.1'
1484+ self.get_relation_ip.return_value = '127.0.0.1'
1485 self.relation_ids.return_value = ['cache:1', 'cache:2']
1486 memcached_hooks.cache_relation_joined()
1487 self.relation_set.assert_any_call('cache:1', **{'host': '127.0.0.1',
1488@@ -115,7 +116,7 @@
1489 self.relation_set.assert_any_call('cache:2', **{'host': '127.0.0.1',
1490 'port': '1234',
1491 'udp-port': '3456'})
1492- grant_access.assert_called_with('127.0.1.1')
1493+ self.grant_access.assert_called_with('127.0.1.1')
1494
1495 @mock.patch('subprocess.check_output')
1496 @mock.patch('memcached_hooks.relation_get')
1497@@ -287,7 +288,6 @@
1498 @mock.patch('os.fchown')
1499 @mock.patch('os.chown')
1500 @mock.patch('memcached_utils.log')
1501- @mock.patch('memcached_utils.dpkg_info_contains')
1502 @mock.patch('subprocess.Popen')
1503 @mock.patch('memcached_utils.config')
1504 @mock.patch('subprocess.check_output')
1505@@ -299,7 +299,7 @@
1506 @mock.patch('memcached_hooks.config_changed')
1507 def test_upgrade_charm(self, config_changed, add_source, log,
1508 charm_dir, service,
1509- enable, check_output, config, popen, dpkg, *args):
1510+ enable, check_output, config, popen, *args):
1511 configs = {
1512 'tcp-port': '1234', 'udp-port': '3456',
1513 'allow-ufw-ip6-softfail': False,
1514@@ -323,7 +323,7 @@
1515 p.configure_mock(**{'communicate.return_value': ('stdout', 'stderr'),
1516 'returncode': 0})
1517 popen.return_value = p
1518- dpkg.return_value = True
1519+ self.dpkg_info_contains.return_value = True
1520
1521 charm_dir.return_value = os.path.join(DOT, '..')
1522 config.return_value = 12111
1523@@ -406,7 +406,7 @@
1524 return relations.get(k, None)
1525
1526 self.relation_get.side_effect = r
1527- self.unit_get.return_value = '10.0.0.2'
1528+ self.get_relation_ip.return_value = '10.0.0.2'
1529
1530 self.peer_units.return_value = [0, 0]
1531 self.oldest_peer.return_value = False
1532@@ -433,7 +433,7 @@
1533 return relations.get(k, None)
1534
1535 self.relation_get.side_effect = r
1536- self.unit_get.return_value = '10.0.0.3'
1537+ self.get_relation_ip.return_value = '10.0.0.3'
1538
1539 self.peer_units.return_value = [0, 0]
1540 self.oldest_peer.return_value = False
1541@@ -442,16 +442,16 @@
1542
1543 @mock.patch('memcached_hooks.open_port')
1544 @mock.patch('replication.get_current_replica')
1545- @mock.patch('replication.unit_get')
1546+ @mock.patch('replication.get_relation_ip')
1547 @mock.patch('memcached_hooks.ufw.modify_access')
1548 @mock.patch('memcached_hooks.ufw.service')
1549 @mock.patch('memcached_hooks.cache_relation_joined')
1550 @mock.patch('charmhelpers.core.templating.render')
1551 @mock.patch('replication.get_repcached_replica')
1552- @mock.patch('memcached_utils.dpkg_info_contains')
1553- def test_config_changed_replica(self, dpkg, get_replica, render,
1554+ def test_config_changed_replica(self, get_replica, render,
1555 cache_joined, ufw_svc, ufw_modify,
1556- unit_get, replica, open_port):
1557+ get_relation_ip,
1558+ replica, open_port):
1559 params = {
1560 'tcp_port': 11211,
1561 'disable_cas': False,
1562@@ -469,7 +469,7 @@
1563 'disable_auto_cleanup': False,
1564 }
1565
1566- dpkg.return_value = True
1567+ self.dpkg_info_contains.return_value = True
1568 replica.return_value = ('10.0.0.2')
1569
1570 configs = get_default_config()

Subscribers

People subscribed via source and target branches