Merge lp:~ajkavanagh/charms/trusty/memcached/add-spaces-support into lp:charms/trusty/memcached
- Trusty Tahr (14.04)
- add-spaces-support
- Merge into trunk
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 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
David Ames (community) | Approve | ||
Review via email: mp+322844@code.launchpad.net |
Commit message
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
Alex Kavanagh (ajkavanagh) wrote : | # |
Good catch David. I wrote the space_aware_
- 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() |
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.