Merge lp:~abentley/charms/precise/juju-reports/use-charm-helpers into lp:~juju-qa/charms/precise/juju-reports/trunk

Proposed by Aaron Bentley on 2014-05-14
Status: Merged
Merged at revision: 29
Proposed branch: lp:~abentley/charms/precise/juju-reports/use-charm-helpers
Merge into: lp:~juju-qa/charms/precise/juju-reports/trunk
Diff against target: 550 lines (+454/-32)
4 files modified
hooks/common.py (+48/-14)
hooks/database-relation-changed (+2/-18)
hooks/hookenv.py (+401/-0)
hooks/install (+3/-0)
To merge this branch: bzr merge lp:~abentley/charms/precise/juju-reports/use-charm-helpers
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code 2014-05-14 Approve on 2014-05-14
Review via email: mp+219594@code.launchpad.net

Commit message

Use charm-helpers, run daemon as ubuntu.

Description of the change

This branch uses charm-helpers to retrive configuration and runs as user ubuntu.

hookenv.py is added to the environment, so that its relation-related commands can be used. update_from_config is updated to get the mongodb url from the database relation. get_mongo_url is introduced from charmworld, almost verbatim. (The explicit 'hostname' is used instead of the implicit 'private-address'.)

hookenv's config, open_port and close_port are used too.

call_script is updated to run scripts as 'ubuntu', by default. scripts/stop is called by root so that any servers started by root can be stopped.

Because the web server may have been run as root, /tmp/app.log is chowned to ubuntu.

To post a comment you must log in.
Curtis Hovey (sinzui) wrote :

Thank you

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'hooks/common.py'
2--- hooks/common.py 2014-05-12 16:00:50 +0000
3+++ hooks/common.py 2014-05-14 20:25:09 +0000
4@@ -9,7 +9,8 @@
5 import sys
6
7 from Cheetah.Template import Template
8-import yaml
9+
10+import hookenv
11
12
13 CHARM_DIR = os.getcwd()
14@@ -48,22 +49,30 @@
15 port = ini_get(ini, 'server:main', 'port')
16 if port is None:
17 return
18- cmd = 'open-port' if open_port else 'close-port'
19- subprocess.check_call([cmd, '%s/tcp' % port])
20-
21-
22-def call_script(script_path):
23+ if open_port:
24+ hookenv.open_port(port)
25+ else:
26+ hookenv.close_port(port)
27+
28+
29+def call_script(script_path, root=False):
30 env = os.environ.copy()
31 ini = get_ini_path()
32 env['HOME'] = HOME
33 env['INI'] = ini
34- subprocess.check_call([script_path], env=env, cwd=PROJECT_DIR)
35+ if root:
36+ args = []
37+ else:
38+ args = ['su', 'ubuntu', '-p', '-c']
39+
40+ args.append(script_path)
41+ subprocess.check_call(args, env=env, cwd=PROJECT_DIR)
42 return env
43
44
45 def stop():
46 try:
47- call_script('scripts/stop')
48+ call_script('scripts/stop', root=True)
49 except OSError as e:
50 if e.errno != errno.ENOENT:
51 raise
52@@ -74,7 +83,7 @@
53
54
55 def get_config():
56- config = yaml.safe_load(StringIO(subprocess.check_output('config-get')))
57+ config = hookenv.config()
58 if config['source'] == '':
59 raise IncompleteConfig('source not specified.')
60 if re.match('(lp:|.*://bazaar.launchpad.net/)', config['source']):
61@@ -142,14 +151,39 @@
62 return
63
64
65-def update_from_config(mongo_url=None):
66+def get_mongo_url():
67+ """Use the 'database' relation settings to build a mongodb url.
68+
69+ If no mongodb units are present, return an empty string.
70+ """
71+ host_ports = []
72+ replset = None
73+ for relation_id in hookenv.relation_ids('database'):
74+ for unit in hookenv.related_units(relation_id):
75+ relation_data = hookenv.relation_get(rid=relation_id, unit=unit)
76+ # Consider configuration incomplete until port is supplied.
77+ if (relation_data is None or 'port' not in relation_data or
78+ 'hostname' not in relation_data):
79+ continue
80+ if replset is None:
81+ replset = relation_data['replset']
82+ # All units should be members of the same replication set.
83+ elif replset != relation_data['replset']:
84+ raise AssertionError('DB instances are from different sets!')
85+ host_ports.append(
86+ '%(hostname)s:%(port)s' % relation_data)
87+ if len(host_ports) == 0:
88+ return ''
89+ return 'mongodb://%s/?replicaSet=%s' % (','.join(host_ports), replset)
90+
91+
92+def update_from_config():
93 ini = get_ini()
94- if mongo_url is None:
95- mongo_url = ini_get(ini, 'app:main', 'mongo.url')
96 try:
97 try:
98- if mongo_url is None:
99- raise IncompleteConfig('mongo url is missing.')
100+ mongo_url = get_mongo_url()
101+ if mongo_url == '':
102+ raise IncompleteConfig('No mongodb set up.')
103 config = get_config()
104 except:
105 set_port(ini, False)
106
107=== modified file 'hooks/database-relation-changed'
108--- hooks/database-relation-changed 2014-05-07 18:03:51 +0000
109+++ hooks/database-relation-changed 2014-05-14 20:25:09 +0000
110@@ -1,21 +1,5 @@
111 #!/usr/bin/env python
112-import json
113-import subprocess
114-import sys
115-
116-from common import (
117- update_from_config
118-)
119-
120+from common import update_from_config
121
122 if __name__ == '__main__':
123- config = json.loads(
124- subprocess.check_output(['relation-get', '--format=json']))
125- try:
126- mongo_url = 'mongodb://%s:%s/?replicaSet=%s' % (
127- config['hostname'], config['port'], config['replset'])
128- except KeyError:
129- # We don't have the environment data we're supposed to have; by
130- # convention we exit silently.
131- sys.exit()
132- update_from_config(mongo_url)
133+ update_from_config()
134
135=== added file 'hooks/hookenv.py'
136--- hooks/hookenv.py 1970-01-01 00:00:00 +0000
137+++ hooks/hookenv.py 2014-05-14 20:25:09 +0000
138@@ -0,0 +1,401 @@
139+"Interactions with the Juju environment"
140+# Copyright 2013 Canonical Ltd.
141+#
142+# Authors:
143+# Charm Helpers Developers <juju@lists.ubuntu.com>
144+
145+import os
146+import json
147+import yaml
148+import subprocess
149+import sys
150+import UserDict
151+from subprocess import CalledProcessError
152+
153+CRITICAL = "CRITICAL"
154+ERROR = "ERROR"
155+WARNING = "WARNING"
156+INFO = "INFO"
157+DEBUG = "DEBUG"
158+MARKER = object()
159+
160+cache = {}
161+
162+
163+def cached(func):
164+ """Cache return values for multiple executions of func + args
165+
166+ For example:
167+
168+ @cached
169+ def unit_get(attribute):
170+ pass
171+
172+ unit_get('test')
173+
174+ will cache the result of unit_get + 'test' for future calls.
175+ """
176+ def wrapper(*args, **kwargs):
177+ global cache
178+ key = str((func, args, kwargs))
179+ try:
180+ return cache[key]
181+ except KeyError:
182+ res = func(*args, **kwargs)
183+ cache[key] = res
184+ return res
185+ return wrapper
186+
187+
188+def flush(key):
189+ """Flushes any entries from function cache where the
190+ key is found in the function+args """
191+ flush_list = []
192+ for item in cache:
193+ if key in item:
194+ flush_list.append(item)
195+ for item in flush_list:
196+ del cache[item]
197+
198+
199+def log(message, level=None):
200+ """Write a message to the juju log"""
201+ command = ['juju-log']
202+ if level:
203+ command += ['-l', level]
204+ command += [message]
205+ subprocess.call(command)
206+
207+
208+class Serializable(UserDict.IterableUserDict):
209+ """Wrapper, an object that can be serialized to yaml or json"""
210+
211+ def __init__(self, obj):
212+ # wrap the object
213+ UserDict.IterableUserDict.__init__(self)
214+ self.data = obj
215+
216+ def __getattr__(self, attr):
217+ # See if this object has attribute.
218+ if attr in ("json", "yaml", "data"):
219+ return self.__dict__[attr]
220+ # Check for attribute in wrapped object.
221+ got = getattr(self.data, attr, MARKER)
222+ if got is not MARKER:
223+ return got
224+ # Proxy to the wrapped object via dict interface.
225+ try:
226+ return self.data[attr]
227+ except KeyError:
228+ raise AttributeError(attr)
229+
230+ def __getstate__(self):
231+ # Pickle as a standard dictionary.
232+ return self.data
233+
234+ def __setstate__(self, state):
235+ # Unpickle into our wrapper.
236+ self.data = state
237+
238+ def json(self):
239+ """Serialize the object to json"""
240+ return json.dumps(self.data)
241+
242+ def yaml(self):
243+ """Serialize the object to yaml"""
244+ return yaml.dump(self.data)
245+
246+
247+def execution_environment():
248+ """A convenient bundling of the current execution context"""
249+ context = {}
250+ context['conf'] = config()
251+ if relation_id():
252+ context['reltype'] = relation_type()
253+ context['relid'] = relation_id()
254+ context['rel'] = relation_get()
255+ context['unit'] = local_unit()
256+ context['rels'] = relations()
257+ context['env'] = os.environ
258+ return context
259+
260+
261+def in_relation_hook():
262+ """Determine whether we're running in a relation hook"""
263+ return 'JUJU_RELATION' in os.environ
264+
265+
266+def relation_type():
267+ """The scope for the current relation hook"""
268+ return os.environ.get('JUJU_RELATION', None)
269+
270+
271+def relation_id():
272+ """The relation ID for the current relation hook"""
273+ return os.environ.get('JUJU_RELATION_ID', None)
274+
275+
276+def local_unit():
277+ """Local unit ID"""
278+ return os.environ['JUJU_UNIT_NAME']
279+
280+
281+def remote_unit():
282+ """The remote unit for the current relation hook"""
283+ return os.environ['JUJU_REMOTE_UNIT']
284+
285+
286+def service_name():
287+ """The name service group this unit belongs to"""
288+ return local_unit().split('/')[0]
289+
290+
291+def hook_name():
292+ """The name of the currently executing hook"""
293+ return os.path.basename(sys.argv[0])
294+
295+
296+@cached
297+def config(scope=None):
298+ """Juju charm configuration"""
299+ config_cmd_line = ['config-get']
300+ if scope is not None:
301+ config_cmd_line.append(scope)
302+ config_cmd_line.append('--format=json')
303+ try:
304+ return json.loads(subprocess.check_output(config_cmd_line))
305+ except ValueError:
306+ return None
307+
308+
309+@cached
310+def relation_get(attribute=None, unit=None, rid=None):
311+ """Get relation information"""
312+ _args = ['relation-get', '--format=json']
313+ if rid:
314+ _args.append('-r')
315+ _args.append(rid)
316+ _args.append(attribute or '-')
317+ if unit:
318+ _args.append(unit)
319+ try:
320+ return json.loads(subprocess.check_output(_args))
321+ except ValueError:
322+ return None
323+ except CalledProcessError, e:
324+ if e.returncode == 2:
325+ return None
326+ raise
327+
328+
329+def relation_set(relation_id=None, relation_settings={}, **kwargs):
330+ """Set relation information for the current unit"""
331+ relation_cmd_line = ['relation-set']
332+ if relation_id is not None:
333+ relation_cmd_line.extend(('-r', relation_id))
334+ for k, v in (relation_settings.items() + kwargs.items()):
335+ if v is None:
336+ relation_cmd_line.append('{}='.format(k))
337+ else:
338+ relation_cmd_line.append('{}={}'.format(k, v))
339+ subprocess.check_call(relation_cmd_line)
340+ # Flush cache of any relation-gets for local unit
341+ flush(local_unit())
342+
343+
344+@cached
345+def relation_ids(reltype=None):
346+ """A list of relation_ids"""
347+ reltype = reltype or relation_type()
348+ relid_cmd_line = ['relation-ids', '--format=json']
349+ if reltype is not None:
350+ relid_cmd_line.append(reltype)
351+ return json.loads(subprocess.check_output(relid_cmd_line)) or []
352+ return []
353+
354+
355+@cached
356+def related_units(relid=None):
357+ """A list of related units"""
358+ relid = relid or relation_id()
359+ units_cmd_line = ['relation-list', '--format=json']
360+ if relid is not None:
361+ units_cmd_line.extend(('-r', relid))
362+ return json.loads(subprocess.check_output(units_cmd_line)) or []
363+
364+
365+@cached
366+def relation_for_unit(unit=None, rid=None):
367+ """Get the json represenation of a unit's relation"""
368+ unit = unit or remote_unit()
369+ relation = relation_get(unit=unit, rid=rid)
370+ for key in relation:
371+ if key.endswith('-list'):
372+ relation[key] = relation[key].split()
373+ relation['__unit__'] = unit
374+ return relation
375+
376+
377+@cached
378+def relations_for_id(relid=None):
379+ """Get relations of a specific relation ID"""
380+ relation_data = []
381+ relid = relid or relation_ids()
382+ for unit in related_units(relid):
383+ unit_data = relation_for_unit(unit, relid)
384+ unit_data['__relid__'] = relid
385+ relation_data.append(unit_data)
386+ return relation_data
387+
388+
389+@cached
390+def relations_of_type(reltype=None):
391+ """Get relations of a specific type"""
392+ relation_data = []
393+ reltype = reltype or relation_type()
394+ for relid in relation_ids(reltype):
395+ for relation in relations_for_id(relid):
396+ relation['__relid__'] = relid
397+ relation_data.append(relation)
398+ return relation_data
399+
400+
401+@cached
402+def relation_types():
403+ """Get a list of relation types supported by this charm"""
404+ charmdir = os.environ.get('CHARM_DIR', '')
405+ mdf = open(os.path.join(charmdir, 'metadata.yaml'))
406+ md = yaml.safe_load(mdf)
407+ rel_types = []
408+ for key in ('provides', 'requires', 'peers'):
409+ section = md.get(key)
410+ if section:
411+ rel_types.extend(section.keys())
412+ mdf.close()
413+ return rel_types
414+
415+
416+@cached
417+def relations():
418+ """Get a nested dictionary of relation data for all related units"""
419+ rels = {}
420+ for reltype in relation_types():
421+ relids = {}
422+ for relid in relation_ids(reltype):
423+ units = {local_unit(): relation_get(unit=local_unit(), rid=relid)}
424+ for unit in related_units(relid):
425+ reldata = relation_get(unit=unit, rid=relid)
426+ units[unit] = reldata
427+ relids[relid] = units
428+ rels[reltype] = relids
429+ return rels
430+
431+
432+@cached
433+def is_relation_made(relation, keys='private-address'):
434+ '''
435+ Determine whether a relation is established by checking for
436+ presence of key(s). If a list of keys is provided, they
437+ must all be present for the relation to be identified as made
438+ '''
439+ if isinstance(keys, str):
440+ keys = [keys]
441+ for r_id in relation_ids(relation):
442+ for unit in related_units(r_id):
443+ context = {}
444+ for k in keys:
445+ context[k] = relation_get(k, rid=r_id,
446+ unit=unit)
447+ if None not in context.values():
448+ return True
449+ return False
450+
451+
452+def open_port(port, protocol="TCP"):
453+ """Open a service network port"""
454+ _args = ['open-port']
455+ _args.append('{}/{}'.format(port, protocol))
456+ subprocess.check_call(_args)
457+
458+
459+def close_port(port, protocol="TCP"):
460+ """Close a service network port"""
461+ _args = ['close-port']
462+ _args.append('{}/{}'.format(port, protocol))
463+ subprocess.check_call(_args)
464+
465+
466+@cached
467+def unit_get(attribute):
468+ """Get the unit ID for the remote unit"""
469+ _args = ['unit-get', '--format=json', attribute]
470+ try:
471+ return json.loads(subprocess.check_output(_args))
472+ except ValueError:
473+ return None
474+
475+
476+def unit_private_ip():
477+ """Get this unit's private IP address"""
478+ return unit_get('private-address')
479+
480+
481+class UnregisteredHookError(Exception):
482+ """Raised when an undefined hook is called"""
483+ pass
484+
485+
486+class Hooks(object):
487+ """A convenient handler for hook functions.
488+
489+ Example:
490+ hooks = Hooks()
491+
492+ # register a hook, taking its name from the function name
493+ @hooks.hook()
494+ def install():
495+ ...
496+
497+ # register a hook, providing a custom hook name
498+ @hooks.hook("config-changed")
499+ def config_changed():
500+ ...
501+
502+ if __name__ == "__main__":
503+ # execute a hook based on the name the program is called by
504+ hooks.execute(sys.argv)
505+ """
506+
507+ def __init__(self):
508+ super(Hooks, self).__init__()
509+ self._hooks = {}
510+
511+ def register(self, name, function):
512+ """Register a hook"""
513+ self._hooks[name] = function
514+
515+ def execute(self, args):
516+ """Execute a registered hook based on args[0]"""
517+ hook_name = os.path.basename(args[0])
518+ if hook_name in self._hooks:
519+ self._hooks[hook_name]()
520+ else:
521+ raise UnregisteredHookError(hook_name)
522+
523+ def hook(self, *hook_names):
524+ """Decorator, registering them as hooks"""
525+ def wrapper(decorated):
526+ for hook_name in hook_names:
527+ self.register(hook_name, decorated)
528+ else:
529+ self.register(decorated.__name__, decorated)
530+ if '_' in decorated.__name__:
531+ self.register(
532+ decorated.__name__.replace('_', '-'), decorated)
533+ return decorated
534+ return wrapper
535+
536+
537+def charm_dir():
538+ """Return the root directory of the current charm"""
539+ return os.environ.get('CHARM_DIR')
540
541=== modified file 'hooks/install'
542--- hooks/install 2014-05-07 18:03:51 +0000
543+++ hooks/install 2014-05-14 20:25:09 +0000
544@@ -25,3 +25,6 @@
545 cd $project_dir
546 bzr init
547 chown ubuntu:ubuntu $project_dir
548+if [ -f /tmp/app.log ]; then
549+ chown ubuntu:ubuntu /tmp/app.log
550+fi

Subscribers

People subscribed via source and target branches

to all changes: