Merge lp:~gnuoy/charms/trusty/nova-cloud-controller/next-charm-sync into lp:~openstack-charmers-archive/charms/trusty/nova-cloud-controller/next

Proposed by Liam Young
Status: Merged
Merged at revision: 94
Proposed branch: lp:~gnuoy/charms/trusty/nova-cloud-controller/next-charm-sync
Merge into: lp:~openstack-charmers-archive/charms/trusty/nova-cloud-controller/next
Diff against target: 727 lines (+570/-12)
11 files modified
hooks/charmhelpers/contrib/network/ip.py (+19/-1)
hooks/charmhelpers/contrib/openstack/context.py (+20/-4)
hooks/charmhelpers/contrib/openstack/ip.py (+7/-3)
hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg (+3/-3)
hooks/charmhelpers/contrib/storage/linux/utils.py (+3/-0)
hooks/charmhelpers/core/host.py (+34/-1)
hooks/charmhelpers/core/services/__init__.py (+2/-0)
hooks/charmhelpers/core/services/base.py (+305/-0)
hooks/charmhelpers/core/services/helpers.py (+125/-0)
hooks/charmhelpers/core/templating.py (+51/-0)
hooks/charmhelpers/fetch/__init__.py (+1/-0)
To merge this branch: bzr merge lp:~gnuoy/charms/trusty/nova-cloud-controller/next-charm-sync
Reviewer Review Type Date Requested Status
Liam Young (community) Approve
Review via email: mp+230629@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Liam Young (gnuoy) wrote :

Approved by jamespage

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'hooks/charmhelpers/contrib/network/ip.py'
--- hooks/charmhelpers/contrib/network/ip.py 2014-07-28 12:05:42 +0000
+++ hooks/charmhelpers/contrib/network/ip.py 2014-08-13 13:55:05 +0000
@@ -4,7 +4,7 @@
44
5from charmhelpers.fetch import apt_install5from charmhelpers.fetch import apt_install
6from charmhelpers.core.hookenv import (6from charmhelpers.core.hookenv import (
7 ERROR, log,7 ERROR, log, config,
8)8)
99
10try:10try:
@@ -154,3 +154,21 @@
154get_iface_for_address = partial(_get_for_address, key='iface')154get_iface_for_address = partial(_get_for_address, key='iface')
155155
156get_netmask_for_address = partial(_get_for_address, key='netmask')156get_netmask_for_address = partial(_get_for_address, key='netmask')
157
158
159def get_ipv6_addr(iface="eth0"):
160 try:
161 iface_addrs = netifaces.ifaddresses(iface)
162 if netifaces.AF_INET6 not in iface_addrs:
163 raise Exception("Interface '%s' doesn't have an ipv6 address." % iface)
164
165 addresses = netifaces.ifaddresses(iface)[netifaces.AF_INET6]
166 ipv6_addr = [a['addr'] for a in addresses if not a['addr'].startswith('fe80')
167 and config('vip') != a['addr']]
168 if not ipv6_addr:
169 raise Exception("Interface '%s' doesn't have global ipv6 address." % iface)
170
171 return ipv6_addr[0]
172
173 except ValueError:
174 raise ValueError("Invalid interface '%s'" % iface)
157175
=== modified file 'hooks/charmhelpers/contrib/openstack/context.py'
--- hooks/charmhelpers/contrib/openstack/context.py 2014-07-28 14:41:41 +0000
+++ hooks/charmhelpers/contrib/openstack/context.py 2014-08-13 13:55:05 +0000
@@ -44,7 +44,10 @@
44 neutron_plugin_attribute,44 neutron_plugin_attribute,
45)45)
4646
47from charmhelpers.contrib.network.ip import get_address_in_network47from charmhelpers.contrib.network.ip import (
48 get_address_in_network,
49 get_ipv6_addr,
50)
4851
49CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'52CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
5053
@@ -401,9 +404,12 @@
401404
402 cluster_hosts = {}405 cluster_hosts = {}
403 l_unit = local_unit().replace('/', '-')406 l_unit = local_unit().replace('/', '-')
404 cluster_hosts[l_unit] = \407 if config('prefer-ipv6'):
405 get_address_in_network(config('os-internal-network'),408 addr = get_ipv6_addr()
406 unit_get('private-address'))409 else:
410 addr = unit_get('private-address')
411 cluster_hosts[l_unit] = get_address_in_network(config('os-internal-network'),
412 addr)
407413
408 for rid in relation_ids('cluster'):414 for rid in relation_ids('cluster'):
409 for unit in related_units(rid):415 for unit in related_units(rid):
@@ -414,6 +420,16 @@
414 ctxt = {420 ctxt = {
415 'units': cluster_hosts,421 'units': cluster_hosts,
416 }422 }
423
424 if config('prefer-ipv6'):
425 ctxt['local_host'] = 'ip6-localhost'
426 ctxt['haproxy_host'] = '::'
427 ctxt['stat_port'] = ':::8888'
428 else:
429 ctxt['local_host'] = '127.0.0.1'
430 ctxt['haproxy_host'] = '0.0.0.0'
431 ctxt['stat_port'] = ':8888'
432
417 if len(cluster_hosts.keys()) > 1:433 if len(cluster_hosts.keys()) > 1:
418 # Enable haproxy when we have enough peers.434 # Enable haproxy when we have enough peers.
419 log('Ensuring haproxy enabled in /etc/default/haproxy.')435 log('Ensuring haproxy enabled in /etc/default/haproxy.')
420436
=== modified file 'hooks/charmhelpers/contrib/openstack/ip.py'
--- hooks/charmhelpers/contrib/openstack/ip.py 2014-07-28 11:39:11 +0000
+++ hooks/charmhelpers/contrib/openstack/ip.py 2014-08-13 13:55:05 +0000
@@ -7,6 +7,7 @@
7 get_address_in_network,7 get_address_in_network,
8 is_address_in_network,8 is_address_in_network,
9 is_ipv6,9 is_ipv6,
10 get_ipv6_addr,
10)11)
1112
12from charmhelpers.contrib.hahelpers.cluster import is_clustered13from charmhelpers.contrib.hahelpers.cluster import is_clustered
@@ -64,10 +65,13 @@
64 vip):65 vip):
65 resolved_address = vip66 resolved_address = vip
66 else:67 else:
68 if config('prefer-ipv6'):
69 fallback_addr = get_ipv6_addr()
70 else:
71 fallback_addr = unit_get(_address_map[endpoint_type]['fallback'])
67 resolved_address = get_address_in_network(72 resolved_address = get_address_in_network(
68 config(_address_map[endpoint_type]['config']),73 config(_address_map[endpoint_type]['config']), fallback_addr)
69 unit_get(_address_map[endpoint_type]['fallback'])74
70 )
71 if resolved_address is None:75 if resolved_address is None:
72 raise ValueError('Unable to resolve a suitable IP address'76 raise ValueError('Unable to resolve a suitable IP address'
73 ' based on charm state and configuration')77 ' based on charm state and configuration')
7478
=== modified file 'hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg'
--- hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg 2014-07-28 14:41:41 +0000
+++ hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg 2014-08-13 13:55:05 +0000
@@ -1,6 +1,6 @@
1global1global
2 log 127.0.0.1 local02 log {{ local_host }} local0
3 log 127.0.0.1 local1 notice3 log {{ local_host }} local1 notice
4 maxconn 200004 maxconn 20000
5 user haproxy5 user haproxy
6 group haproxy6 group haproxy
@@ -17,7 +17,7 @@
17 timeout client 3000017 timeout client 30000
18 timeout server 3000018 timeout server 30000
1919
20listen stats :888820listen stats {{ stat_port }}
21 mode http21 mode http
22 stats enable22 stats enable
23 stats hide-version23 stats hide-version
2424
=== modified file 'hooks/charmhelpers/contrib/storage/linux/utils.py'
--- hooks/charmhelpers/contrib/storage/linux/utils.py 2014-07-28 14:41:41 +0000
+++ hooks/charmhelpers/contrib/storage/linux/utils.py 2014-08-13 13:55:05 +0000
@@ -46,5 +46,8 @@
46 :returns: boolean: True if the path represents a mounted device, False if46 :returns: boolean: True if the path represents a mounted device, False if
47 it doesn't.47 it doesn't.
48 '''48 '''
49 is_partition = bool(re.search(r".*[0-9]+\b", device))
49 out = check_output(['mount'])50 out = check_output(['mount'])
51 if is_partition:
52 return bool(re.search(device + r"\b", out))
50 return bool(re.search(device + r"[0-9]+\b", out))53 return bool(re.search(device + r"[0-9]+\b", out))
5154
=== modified file 'hooks/charmhelpers/core/host.py'
--- hooks/charmhelpers/core/host.py 2014-07-28 14:41:41 +0000
+++ hooks/charmhelpers/core/host.py 2014-08-13 13:55:05 +0000
@@ -12,6 +12,8 @@
12import string12import string
13import subprocess13import subprocess
14import hashlib14import hashlib
15import shutil
16from contextlib import contextmanager
1517
16from collections import OrderedDict18from collections import OrderedDict
1719
@@ -52,7 +54,7 @@
52def service_running(service):54def service_running(service):
53 """Determine whether a system service is running"""55 """Determine whether a system service is running"""
54 try:56 try:
55 output = subprocess.check_output(['service', service, 'status'])57 output = subprocess.check_output(['service', service, 'status'], stderr=subprocess.STDOUT)
56 except subprocess.CalledProcessError:58 except subprocess.CalledProcessError:
57 return False59 return False
58 else:60 else:
@@ -62,6 +64,16 @@
62 return False64 return False
6365
6466
67def service_available(service_name):
68 """Determine whether a system service is available"""
69 try:
70 subprocess.check_output(['service', service_name, 'status'], stderr=subprocess.STDOUT)
71 except subprocess.CalledProcessError:
72 return False
73 else:
74 return True
75
76
65def adduser(username, password=None, shell='/bin/bash', system_user=False):77def adduser(username, password=None, shell='/bin/bash', system_user=False):
66 """Add a user to the system"""78 """Add a user to the system"""
67 try:79 try:
@@ -329,3 +341,24 @@
329 pkgcache = apt_pkg.Cache()341 pkgcache = apt_pkg.Cache()
330 pkg = pkgcache[package]342 pkg = pkgcache[package]
331 return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)343 return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
344
345
346@contextmanager
347def chdir(d):
348 cur = os.getcwd()
349 try:
350 yield os.chdir(d)
351 finally:
352 os.chdir(cur)
353
354
355def chownr(path, owner, group):
356 uid = pwd.getpwnam(owner).pw_uid
357 gid = grp.getgrnam(group).gr_gid
358
359 for root, dirs, files in os.walk(path):
360 for name in dirs + files:
361 full = os.path.join(root, name)
362 broken_symlink = os.path.lexists(full) and not os.path.exists(full)
363 if not broken_symlink:
364 os.chown(full, uid, gid)
332365
=== added directory 'hooks/charmhelpers/core/services'
=== added file 'hooks/charmhelpers/core/services/__init__.py'
--- hooks/charmhelpers/core/services/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/services/__init__.py 2014-08-13 13:55:05 +0000
@@ -0,0 +1,2 @@
1from .base import *
2from .helpers import *
03
=== added file 'hooks/charmhelpers/core/services/base.py'
--- hooks/charmhelpers/core/services/base.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/services/base.py 2014-08-13 13:55:05 +0000
@@ -0,0 +1,305 @@
1import os
2import re
3import json
4from collections import Iterable
5
6from charmhelpers.core import host
7from charmhelpers.core import hookenv
8
9
10__all__ = ['ServiceManager', 'ManagerCallback',
11 'PortManagerCallback', 'open_ports', 'close_ports', 'manage_ports',
12 'service_restart', 'service_stop']
13
14
15class ServiceManager(object):
16 def __init__(self, services=None):
17 """
18 Register a list of services, given their definitions.
19
20 Traditional charm authoring is focused on implementing hooks. That is,
21 the charm author is thinking in terms of "What hook am I handling; what
22 does this hook need to do?" However, in most cases, the real question
23 should be "Do I have the information I need to configure and start this
24 piece of software and, if so, what are the steps for doing so?" The
25 ServiceManager framework tries to bring the focus to the data and the
26 setup tasks, in the most declarative way possible.
27
28 Service definitions are dicts in the following formats (all keys except
29 'service' are optional)::
30
31 {
32 "service": <service name>,
33 "required_data": <list of required data contexts>,
34 "data_ready": <one or more callbacks>,
35 "data_lost": <one or more callbacks>,
36 "start": <one or more callbacks>,
37 "stop": <one or more callbacks>,
38 "ports": <list of ports to manage>,
39 }
40
41 The 'required_data' list should contain dicts of required data (or
42 dependency managers that act like dicts and know how to collect the data).
43 Only when all items in the 'required_data' list are populated are the list
44 of 'data_ready' and 'start' callbacks executed. See `is_ready()` for more
45 information.
46
47 The 'data_ready' value should be either a single callback, or a list of
48 callbacks, to be called when all items in 'required_data' pass `is_ready()`.
49 Each callback will be called with the service name as the only parameter.
50 After all of the 'data_ready' callbacks are called, the 'start' callbacks
51 are fired.
52
53 The 'data_lost' value should be either a single callback, or a list of
54 callbacks, to be called when a 'required_data' item no longer passes
55 `is_ready()`. Each callback will be called with the service name as the
56 only parameter. After all of the 'data_lost' callbacks are called,
57 the 'stop' callbacks are fired.
58
59 The 'start' value should be either a single callback, or a list of
60 callbacks, to be called when starting the service, after the 'data_ready'
61 callbacks are complete. Each callback will be called with the service
62 name as the only parameter. This defaults to
63 `[host.service_start, services.open_ports]`.
64
65 The 'stop' value should be either a single callback, or a list of
66 callbacks, to be called when stopping the service. If the service is
67 being stopped because it no longer has all of its 'required_data', this
68 will be called after all of the 'data_lost' callbacks are complete.
69 Each callback will be called with the service name as the only parameter.
70 This defaults to `[services.close_ports, host.service_stop]`.
71
72 The 'ports' value should be a list of ports to manage. The default
73 'start' handler will open the ports after the service is started,
74 and the default 'stop' handler will close the ports prior to stopping
75 the service.
76
77
78 Examples:
79
80 The following registers an Upstart service called bingod that depends on
81 a mongodb relation and which runs a custom `db_migrate` function prior to
82 restarting the service, and a Runit service called spadesd::
83
84 manager = services.ServiceManager([
85 {
86 'service': 'bingod',
87 'ports': [80, 443],
88 'required_data': [MongoRelation(), config(), {'my': 'data'}],
89 'data_ready': [
90 services.template(source='bingod.conf'),
91 services.template(source='bingod.ini',
92 target='/etc/bingod.ini',
93 owner='bingo', perms=0400),
94 ],
95 },
96 {
97 'service': 'spadesd',
98 'data_ready': services.template(source='spadesd_run.j2',
99 target='/etc/sv/spadesd/run',
100 perms=0555),
101 'start': runit_start,
102 'stop': runit_stop,
103 },
104 ])
105 manager.manage()
106 """
107 self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')
108 self._ready = None
109 self.services = {}
110 for service in services or []:
111 service_name = service['service']
112 self.services[service_name] = service
113
114 def manage(self):
115 """
116 Handle the current hook by doing The Right Thing with the registered services.
117 """
118 hook_name = hookenv.hook_name()
119 if hook_name == 'stop':
120 self.stop_services()
121 else:
122 self.provide_data()
123 self.reconfigure_services()
124
125 def provide_data(self):
126 hook_name = hookenv.hook_name()
127 for service in self.services.values():
128 for provider in service.get('provided_data', []):
129 if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):
130 data = provider.provide_data()
131 if provider._is_ready(data):
132 hookenv.relation_set(None, data)
133
134 def reconfigure_services(self, *service_names):
135 """
136 Update all files for one or more registered services, and,
137 if ready, optionally restart them.
138
139 If no service names are given, reconfigures all registered services.
140 """
141 for service_name in service_names or self.services.keys():
142 if self.is_ready(service_name):
143 self.fire_event('data_ready', service_name)
144 self.fire_event('start', service_name, default=[
145 service_restart,
146 manage_ports])
147 self.save_ready(service_name)
148 else:
149 if self.was_ready(service_name):
150 self.fire_event('data_lost', service_name)
151 self.fire_event('stop', service_name, default=[
152 manage_ports,
153 service_stop])
154 self.save_lost(service_name)
155
156 def stop_services(self, *service_names):
157 """
158 Stop one or more registered services, by name.
159
160 If no service names are given, stops all registered services.
161 """
162 for service_name in service_names or self.services.keys():
163 self.fire_event('stop', service_name, default=[
164 manage_ports,
165 service_stop])
166
167 def get_service(self, service_name):
168 """
169 Given the name of a registered service, return its service definition.
170 """
171 service = self.services.get(service_name)
172 if not service:
173 raise KeyError('Service not registered: %s' % service_name)
174 return service
175
176 def fire_event(self, event_name, service_name, default=None):
177 """
178 Fire a data_ready, data_lost, start, or stop event on a given service.
179 """
180 service = self.get_service(service_name)
181 callbacks = service.get(event_name, default)
182 if not callbacks:
183 return
184 if not isinstance(callbacks, Iterable):
185 callbacks = [callbacks]
186 for callback in callbacks:
187 if isinstance(callback, ManagerCallback):
188 callback(self, service_name, event_name)
189 else:
190 callback(service_name)
191
192 def is_ready(self, service_name):
193 """
194 Determine if a registered service is ready, by checking its 'required_data'.
195
196 A 'required_data' item can be any mapping type, and is considered ready
197 if `bool(item)` evaluates as True.
198 """
199 service = self.get_service(service_name)
200 reqs = service.get('required_data', [])
201 return all(bool(req) for req in reqs)
202
203 def _load_ready_file(self):
204 if self._ready is not None:
205 return
206 if os.path.exists(self._ready_file):
207 with open(self._ready_file) as fp:
208 self._ready = set(json.load(fp))
209 else:
210 self._ready = set()
211
212 def _save_ready_file(self):
213 if self._ready is None:
214 return
215 with open(self._ready_file, 'w') as fp:
216 json.dump(list(self._ready), fp)
217
218 def save_ready(self, service_name):
219 """
220 Save an indicator that the given service is now data_ready.
221 """
222 self._load_ready_file()
223 self._ready.add(service_name)
224 self._save_ready_file()
225
226 def save_lost(self, service_name):
227 """
228 Save an indicator that the given service is no longer data_ready.
229 """
230 self._load_ready_file()
231 self._ready.discard(service_name)
232 self._save_ready_file()
233
234 def was_ready(self, service_name):
235 """
236 Determine if the given service was previously data_ready.
237 """
238 self._load_ready_file()
239 return service_name in self._ready
240
241
242class ManagerCallback(object):
243 """
244 Special case of a callback that takes the `ServiceManager` instance
245 in addition to the service name.
246
247 Subclasses should implement `__call__` which should accept three parameters:
248
249 * `manager` The `ServiceManager` instance
250 * `service_name` The name of the service it's being triggered for
251 * `event_name` The name of the event that this callback is handling
252 """
253 def __call__(self, manager, service_name, event_name):
254 raise NotImplementedError()
255
256
257class PortManagerCallback(ManagerCallback):
258 """
259 Callback class that will open or close ports, for use as either
260 a start or stop action.
261 """
262 def __call__(self, manager, service_name, event_name):
263 service = manager.get_service(service_name)
264 new_ports = service.get('ports', [])
265 port_file = os.path.join(hookenv.charm_dir(), '.{}.ports'.format(service_name))
266 if os.path.exists(port_file):
267 with open(port_file) as fp:
268 old_ports = fp.read().split(',')
269 for old_port in old_ports:
270 if bool(old_port):
271 old_port = int(old_port)
272 if old_port not in new_ports:
273 hookenv.close_port(old_port)
274 with open(port_file, 'w') as fp:
275 fp.write(','.join(str(port) for port in new_ports))
276 for port in new_ports:
277 if event_name == 'start':
278 hookenv.open_port(port)
279 elif event_name == 'stop':
280 hookenv.close_port(port)
281
282
283def service_stop(service_name):
284 """
285 Wrapper around host.service_stop to prevent spurious "unknown service"
286 messages in the logs.
287 """
288 if host.service_running(service_name):
289 host.service_stop(service_name)
290
291
292def service_restart(service_name):
293 """
294 Wrapper around host.service_restart to prevent spurious "unknown service"
295 messages in the logs.
296 """
297 if host.service_available(service_name):
298 if host.service_running(service_name):
299 host.service_restart(service_name)
300 else:
301 host.service_start(service_name)
302
303
304# Convenience aliases
305open_ports = close_ports = manage_ports = PortManagerCallback()
0306
=== added file 'hooks/charmhelpers/core/services/helpers.py'
--- hooks/charmhelpers/core/services/helpers.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/services/helpers.py 2014-08-13 13:55:05 +0000
@@ -0,0 +1,125 @@
1from charmhelpers.core import hookenv
2from charmhelpers.core import templating
3
4from charmhelpers.core.services.base import ManagerCallback
5
6
7__all__ = ['RelationContext', 'TemplateCallback',
8 'render_template', 'template']
9
10
11class RelationContext(dict):
12 """
13 Base class for a context generator that gets relation data from juju.
14
15 Subclasses must provide the attributes `name`, which is the name of the
16 interface of interest, `interface`, which is the type of the interface of
17 interest, and `required_keys`, which is the set of keys required for the
18 relation to be considered complete. The data for all interfaces matching
19 the `name` attribute that are complete will used to populate the dictionary
20 values (see `get_data`, below).
21
22 The generated context will be namespaced under the interface type, to prevent
23 potential naming conflicts.
24 """
25 name = None
26 interface = None
27 required_keys = []
28
29 def __init__(self, *args, **kwargs):
30 super(RelationContext, self).__init__(*args, **kwargs)
31 self.get_data()
32
33 def __bool__(self):
34 """
35 Returns True if all of the required_keys are available.
36 """
37 return self.is_ready()
38
39 __nonzero__ = __bool__
40
41 def __repr__(self):
42 return super(RelationContext, self).__repr__()
43
44 def is_ready(self):
45 """
46 Returns True if all of the `required_keys` are available from any units.
47 """
48 ready = len(self.get(self.name, [])) > 0
49 if not ready:
50 hookenv.log('Incomplete relation: {}'.format(self.__class__.__name__), hookenv.DEBUG)
51 return ready
52
53 def _is_ready(self, unit_data):
54 """
55 Helper method that tests a set of relation data and returns True if
56 all of the `required_keys` are present.
57 """
58 return set(unit_data.keys()).issuperset(set(self.required_keys))
59
60 def get_data(self):
61 """
62 Retrieve the relation data for each unit involved in a relation and,
63 if complete, store it in a list under `self[self.name]`. This
64 is automatically called when the RelationContext is instantiated.
65
66 The units are sorted lexographically first by the service ID, then by
67 the unit ID. Thus, if an interface has two other services, 'db:1'
68 and 'db:2', with 'db:1' having two units, 'wordpress/0' and 'wordpress/1',
69 and 'db:2' having one unit, 'mediawiki/0', all of which have a complete
70 set of data, the relation data for the units will be stored in the
71 order: 'wordpress/0', 'wordpress/1', 'mediawiki/0'.
72
73 If you only care about a single unit on the relation, you can just
74 access it as `{{ interface[0]['key'] }}`. However, if you can at all
75 support multiple units on a relation, you should iterate over the list,
76 like::
77
78 {% for unit in interface -%}
79 {{ unit['key'] }}{% if not loop.last %},{% endif %}
80 {%- endfor %}
81
82 Note that since all sets of relation data from all related services and
83 units are in a single list, if you need to know which service or unit a
84 set of data came from, you'll need to extend this class to preserve
85 that information.
86 """
87 if not hookenv.relation_ids(self.name):
88 return
89
90 ns = self.setdefault(self.name, [])
91 for rid in sorted(hookenv.relation_ids(self.name)):
92 for unit in sorted(hookenv.related_units(rid)):
93 reldata = hookenv.relation_get(rid=rid, unit=unit)
94 if self._is_ready(reldata):
95 ns.append(reldata)
96
97 def provide_data(self):
98 """
99 Return data to be relation_set for this interface.
100 """
101 return {}
102
103
104class TemplateCallback(ManagerCallback):
105 """
106 Callback class that will render a template, for use as a ready action.
107 """
108 def __init__(self, source, target, owner='root', group='root', perms=0444):
109 self.source = source
110 self.target = target
111 self.owner = owner
112 self.group = group
113 self.perms = perms
114
115 def __call__(self, manager, service_name, event_name):
116 service = manager.get_service(service_name)
117 context = {}
118 for ctx in service.get('required_data', []):
119 context.update(ctx)
120 templating.render(self.source, self.target, context,
121 self.owner, self.group, self.perms)
122
123
124# Convenience aliases for templates
125render_template = template = TemplateCallback
0126
=== added file 'hooks/charmhelpers/core/templating.py'
--- hooks/charmhelpers/core/templating.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/templating.py 2014-08-13 13:55:05 +0000
@@ -0,0 +1,51 @@
1import os
2
3from charmhelpers.core import host
4from charmhelpers.core import hookenv
5
6
7def render(source, target, context, owner='root', group='root', perms=0444, templates_dir=None):
8 """
9 Render a template.
10
11 The `source` path, if not absolute, is relative to the `templates_dir`.
12
13 The `target` path should be absolute.
14
15 The context should be a dict containing the values to be replaced in the
16 template.
17
18 The `owner`, `group`, and `perms` options will be passed to `write_file`.
19
20 If omitted, `templates_dir` defaults to the `templates` folder in the charm.
21
22 Note: Using this requires python-jinja2; if it is not installed, calling
23 this will attempt to use charmhelpers.fetch.apt_install to install it.
24 """
25 try:
26 from jinja2 import FileSystemLoader, Environment, exceptions
27 except ImportError:
28 try:
29 from charmhelpers.fetch import apt_install
30 except ImportError:
31 hookenv.log('Could not import jinja2, and could not import '
32 'charmhelpers.fetch to install it',
33 level=hookenv.ERROR)
34 raise
35 apt_install('python-jinja2', fatal=True)
36 from jinja2 import FileSystemLoader, Environment, exceptions
37
38 if templates_dir is None:
39 templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
40 loader = Environment(loader=FileSystemLoader(templates_dir))
41 try:
42 source = source
43 template = loader.get_template(source)
44 except exceptions.TemplateNotFound as e:
45 hookenv.log('Could not load template %s from %s.' %
46 (source, templates_dir),
47 level=hookenv.ERROR)
48 raise e
49 content = template.render(context)
50 host.mkdir(os.path.dirname(target))
51 host.write_file(target, content, owner, group, perms)
052
=== modified file 'hooks/charmhelpers/fetch/__init__.py'
--- hooks/charmhelpers/fetch/__init__.py 2014-07-28 14:41:41 +0000
+++ hooks/charmhelpers/fetch/__init__.py 2014-08-13 13:55:05 +0000
@@ -122,6 +122,7 @@
122 # Tell apt to build an in-memory cache to prevent race conditions (if122 # Tell apt to build an in-memory cache to prevent race conditions (if
123 # another process is already building the cache).123 # another process is already building the cache).
124 apt_pkg.config.set("Dir::Cache::pkgcache", "")124 apt_pkg.config.set("Dir::Cache::pkgcache", "")
125 apt_pkg.config.set("Dir::Cache::srcpkgcache", "")
125126
126 cache = apt_pkg.Cache()127 cache = apt_pkg.Cache()
127 _pkgs = []128 _pkgs = []

Subscribers

People subscribed via source and target branches