Merge lp:~hopem/charms/trusty/neutron-api/charm-helpers-sync into lp:~openstack-charmers-archive/charms/trusty/neutron-api/next

Proposed by Edward Hope-Morley
Status: Merged
Merged at revision: 74
Proposed branch: lp:~hopem/charms/trusty/neutron-api/charm-helpers-sync
Merge into: lp:~openstack-charmers-archive/charms/trusty/neutron-api/next
Diff against target: 584 lines (+496/-13)
4 files modified
hooks/charmhelpers/core/host.py (+5/-5)
hooks/charmhelpers/core/sysctl.py (+11/-5)
hooks/charmhelpers/core/templating.py (+3/-3)
hooks/charmhelpers/core/unitdata.py (+477/-0)
To merge this branch: bzr merge lp:~hopem/charms/trusty/neutron-api/charm-helpers-sync
Reviewer Review Type Date Requested Status
Liam Young (community) Approve
Review via email: mp+249314@code.launchpad.net
To post a comment you must log in.
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_lint_check #1883 neutron-api-next for hopem mp249314
    LINT OK: passed

Build: http://10.245.162.77:8080/job/charm_lint_check/1883/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_unit_test #1714 neutron-api-next for hopem mp249314
    UNIT OK: passed

Build: http://10.245.162.77:8080/job/charm_unit_test/1714/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_amulet_test #1861 neutron-api-next for hopem mp249314
    AMULET FAIL: amulet-test missing

AMULET Results (max last 2 lines):
INFO:root:Search string not found in makefile target commands.
ERROR:root:No make target was executed.

Full amulet test output: http://paste.ubuntu.com/10172902/
Build: http://10.245.162.77:8080/job/charm_amulet_test/1861/

Revision history for this message
Liam Young (gnuoy) wrote :

Approve

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'hooks/charmhelpers/core/host.py'
2--- hooks/charmhelpers/core/host.py 2015-01-26 09:44:26 +0000
3+++ hooks/charmhelpers/core/host.py 2015-02-11 12:45:32 +0000
4@@ -191,11 +191,11 @@
5
6
7 def write_file(path, content, owner='root', group='root', perms=0o444):
8- """Create or overwrite a file with the contents of a string"""
9+ """Create or overwrite a file with the contents of a byte string."""
10 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
11 uid = pwd.getpwnam(owner).pw_uid
12 gid = grp.getgrnam(group).gr_gid
13- with open(path, 'w') as target:
14+ with open(path, 'wb') as target:
15 os.fchown(target.fileno(), uid, gid)
16 os.fchmod(target.fileno(), perms)
17 target.write(content)
18@@ -305,11 +305,11 @@
19 ceph_client_changed function.
20 """
21 def wrap(f):
22- def wrapped_f(*args):
23+ def wrapped_f(*args, **kwargs):
24 checksums = {}
25 for path in restart_map:
26 checksums[path] = file_hash(path)
27- f(*args)
28+ f(*args, **kwargs)
29 restarts = []
30 for path in restart_map:
31 if checksums[path] != file_hash(path):
32@@ -361,7 +361,7 @@
33 ip_output = (line for line in ip_output if line)
34 for line in ip_output:
35 if line.split()[1].startswith(int_type):
36- matched = re.search('.*: (bond[0-9]+\.[0-9]+)@.*', line)
37+ matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line)
38 if matched:
39 interface = matched.groups()[0]
40 else:
41
42=== modified file 'hooks/charmhelpers/core/sysctl.py'
43--- hooks/charmhelpers/core/sysctl.py 2015-01-26 09:44:26 +0000
44+++ hooks/charmhelpers/core/sysctl.py 2015-02-11 12:45:32 +0000
45@@ -26,25 +26,31 @@
46 from charmhelpers.core.hookenv import (
47 log,
48 DEBUG,
49+ ERROR,
50 )
51
52
53 def create(sysctl_dict, sysctl_file):
54 """Creates a sysctl.conf file from a YAML associative array
55
56- :param sysctl_dict: a dict of sysctl options eg { 'kernel.max_pid': 1337 }
57- :type sysctl_dict: dict
58+ :param sysctl_dict: a YAML-formatted string of sysctl options eg "{ 'kernel.max_pid': 1337 }"
59+ :type sysctl_dict: str
60 :param sysctl_file: path to the sysctl file to be saved
61 :type sysctl_file: str or unicode
62 :returns: None
63 """
64- sysctl_dict = yaml.load(sysctl_dict)
65+ try:
66+ sysctl_dict_parsed = yaml.safe_load(sysctl_dict)
67+ except yaml.YAMLError:
68+ log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict),
69+ level=ERROR)
70+ return
71
72 with open(sysctl_file, "w") as fd:
73- for key, value in sysctl_dict.items():
74+ for key, value in sysctl_dict_parsed.items():
75 fd.write("{}={}\n".format(key, value))
76
77- log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict),
78+ log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict_parsed),
79 level=DEBUG)
80
81 check_call(["sysctl", "-p", sysctl_file])
82
83=== modified file 'hooks/charmhelpers/core/templating.py'
84--- hooks/charmhelpers/core/templating.py 2015-01-26 09:44:26 +0000
85+++ hooks/charmhelpers/core/templating.py 2015-02-11 12:45:32 +0000
86@@ -21,7 +21,7 @@
87
88
89 def render(source, target, context, owner='root', group='root',
90- perms=0o444, templates_dir=None):
91+ perms=0o444, templates_dir=None, encoding='UTF-8'):
92 """
93 Render a template.
94
95@@ -64,5 +64,5 @@
96 level=hookenv.ERROR)
97 raise e
98 content = template.render(context)
99- host.mkdir(os.path.dirname(target), owner, group)
100- host.write_file(target, content, owner, group, perms)
101+ host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
102+ host.write_file(target, content.encode(encoding), owner, group, perms)
103
104=== added file 'hooks/charmhelpers/core/unitdata.py'
105--- hooks/charmhelpers/core/unitdata.py 1970-01-01 00:00:00 +0000
106+++ hooks/charmhelpers/core/unitdata.py 2015-02-11 12:45:32 +0000
107@@ -0,0 +1,477 @@
108+#!/usr/bin/env python
109+# -*- coding: utf-8 -*-
110+#
111+# Copyright 2014-2015 Canonical Limited.
112+#
113+# This file is part of charm-helpers.
114+#
115+# charm-helpers is free software: you can redistribute it and/or modify
116+# it under the terms of the GNU Lesser General Public License version 3 as
117+# published by the Free Software Foundation.
118+#
119+# charm-helpers is distributed in the hope that it will be useful,
120+# but WITHOUT ANY WARRANTY; without even the implied warranty of
121+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
122+# GNU Lesser General Public License for more details.
123+#
124+# You should have received a copy of the GNU Lesser General Public License
125+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
126+#
127+#
128+# Authors:
129+# Kapil Thangavelu <kapil.foss@gmail.com>
130+#
131+"""
132+Intro
133+-----
134+
135+A simple way to store state in units. This provides a key value
136+storage with support for versioned, transactional operation,
137+and can calculate deltas from previous values to simplify unit logic
138+when processing changes.
139+
140+
141+Hook Integration
142+----------------
143+
144+There are several extant frameworks for hook execution, including
145+
146+ - charmhelpers.core.hookenv.Hooks
147+ - charmhelpers.core.services.ServiceManager
148+
149+The storage classes are framework agnostic, one simple integration is
150+via the HookData contextmanager. It will record the current hook
151+execution environment (including relation data, config data, etc.),
152+setup a transaction and allow easy access to the changes from
153+previously seen values. One consequence of the integration is the
154+reservation of particular keys ('rels', 'unit', 'env', 'config',
155+'charm_revisions') for their respective values.
156+
157+Here's a fully worked integration example using hookenv.Hooks::
158+
159+ from charmhelper.core import hookenv, unitdata
160+
161+ hook_data = unitdata.HookData()
162+ db = unitdata.kv()
163+ hooks = hookenv.Hooks()
164+
165+ @hooks.hook
166+ def config_changed():
167+ # Print all changes to configuration from previously seen
168+ # values.
169+ for changed, (prev, cur) in hook_data.conf.items():
170+ print('config changed', changed,
171+ 'previous value', prev,
172+ 'current value', cur)
173+
174+ # Get some unit specific bookeeping
175+ if not db.get('pkg_key'):
176+ key = urllib.urlopen('https://example.com/pkg_key').read()
177+ db.set('pkg_key', key)
178+
179+ # Directly access all charm config as a mapping.
180+ conf = db.getrange('config', True)
181+
182+ # Directly access all relation data as a mapping
183+ rels = db.getrange('rels', True)
184+
185+ if __name__ == '__main__':
186+ with hook_data():
187+ hook.execute()
188+
189+
190+A more basic integration is via the hook_scope context manager which simply
191+manages transaction scope (and records hook name, and timestamp)::
192+
193+ >>> from unitdata import kv
194+ >>> db = kv()
195+ >>> with db.hook_scope('install'):
196+ ... # do work, in transactional scope.
197+ ... db.set('x', 1)
198+ >>> db.get('x')
199+ 1
200+
201+
202+Usage
203+-----
204+
205+Values are automatically json de/serialized to preserve basic typing
206+and complex data struct capabilities (dicts, lists, ints, booleans, etc).
207+
208+Individual values can be manipulated via get/set::
209+
210+ >>> kv.set('y', True)
211+ >>> kv.get('y')
212+ True
213+
214+ # We can set complex values (dicts, lists) as a single key.
215+ >>> kv.set('config', {'a': 1, 'b': True'})
216+
217+ # Also supports returning dictionaries as a record which
218+ # provides attribute access.
219+ >>> config = kv.get('config', record=True)
220+ >>> config.b
221+ True
222+
223+
224+Groups of keys can be manipulated with update/getrange::
225+
226+ >>> kv.update({'z': 1, 'y': 2}, prefix="gui.")
227+ >>> kv.getrange('gui.', strip=True)
228+ {'z': 1, 'y': 2}
229+
230+When updating values, its very helpful to understand which values
231+have actually changed and how have they changed. The storage
232+provides a delta method to provide for this::
233+
234+ >>> data = {'debug': True, 'option': 2}
235+ >>> delta = kv.delta(data, 'config.')
236+ >>> delta.debug.previous
237+ None
238+ >>> delta.debug.current
239+ True
240+ >>> delta
241+ {'debug': (None, True), 'option': (None, 2)}
242+
243+Note the delta method does not persist the actual change, it needs to
244+be explicitly saved via 'update' method::
245+
246+ >>> kv.update(data, 'config.')
247+
248+Values modified in the context of a hook scope retain historical values
249+associated to the hookname.
250+
251+ >>> with db.hook_scope('config-changed'):
252+ ... db.set('x', 42)
253+ >>> db.gethistory('x')
254+ [(1, u'x', 1, u'install', u'2015-01-21T16:49:30.038372'),
255+ (2, u'x', 42, u'config-changed', u'2015-01-21T16:49:30.038786')]
256+
257+"""
258+
259+import collections
260+import contextlib
261+import datetime
262+import json
263+import os
264+import pprint
265+import sqlite3
266+import sys
267+
268+__author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>'
269+
270+
271+class Storage(object):
272+ """Simple key value database for local unit state within charms.
273+
274+ Modifications are automatically committed at hook exit. That's
275+ currently regardless of exit code.
276+
277+ To support dicts, lists, integer, floats, and booleans values
278+ are automatically json encoded/decoded.
279+ """
280+ def __init__(self, path=None):
281+ self.db_path = path
282+ if path is None:
283+ self.db_path = os.path.join(
284+ os.environ.get('CHARM_DIR', ''), '.unit-state.db')
285+ self.conn = sqlite3.connect('%s' % self.db_path)
286+ self.cursor = self.conn.cursor()
287+ self.revision = None
288+ self._closed = False
289+ self._init()
290+
291+ def close(self):
292+ if self._closed:
293+ return
294+ self.flush(False)
295+ self.cursor.close()
296+ self.conn.close()
297+ self._closed = True
298+
299+ def _scoped_query(self, stmt, params=None):
300+ if params is None:
301+ params = []
302+ return stmt, params
303+
304+ def get(self, key, default=None, record=False):
305+ self.cursor.execute(
306+ *self._scoped_query(
307+ 'select data from kv where key=?', [key]))
308+ result = self.cursor.fetchone()
309+ if not result:
310+ return default
311+ if record:
312+ return Record(json.loads(result[0]))
313+ return json.loads(result[0])
314+
315+ def getrange(self, key_prefix, strip=False):
316+ stmt = "select key, data from kv where key like '%s%%'" % key_prefix
317+ self.cursor.execute(*self._scoped_query(stmt))
318+ result = self.cursor.fetchall()
319+
320+ if not result:
321+ return None
322+ if not strip:
323+ key_prefix = ''
324+ return dict([
325+ (k[len(key_prefix):], json.loads(v)) for k, v in result])
326+
327+ def update(self, mapping, prefix=""):
328+ for k, v in mapping.items():
329+ self.set("%s%s" % (prefix, k), v)
330+
331+ def unset(self, key):
332+ self.cursor.execute('delete from kv where key=?', [key])
333+ if self.revision and self.cursor.rowcount:
334+ self.cursor.execute(
335+ 'insert into kv_revisions values (?, ?, ?)',
336+ [key, self.revision, json.dumps('DELETED')])
337+
338+ def set(self, key, value):
339+ serialized = json.dumps(value)
340+
341+ self.cursor.execute(
342+ 'select data from kv where key=?', [key])
343+ exists = self.cursor.fetchone()
344+
345+ # Skip mutations to the same value
346+ if exists:
347+ if exists[0] == serialized:
348+ return value
349+
350+ if not exists:
351+ self.cursor.execute(
352+ 'insert into kv (key, data) values (?, ?)',
353+ (key, serialized))
354+ else:
355+ self.cursor.execute('''
356+ update kv
357+ set data = ?
358+ where key = ?''', [serialized, key])
359+
360+ # Save
361+ if not self.revision:
362+ return value
363+
364+ self.cursor.execute(
365+ 'select 1 from kv_revisions where key=? and revision=?',
366+ [key, self.revision])
367+ exists = self.cursor.fetchone()
368+
369+ if not exists:
370+ self.cursor.execute(
371+ '''insert into kv_revisions (
372+ revision, key, data) values (?, ?, ?)''',
373+ (self.revision, key, serialized))
374+ else:
375+ self.cursor.execute(
376+ '''
377+ update kv_revisions
378+ set data = ?
379+ where key = ?
380+ and revision = ?''',
381+ [serialized, key, self.revision])
382+
383+ return value
384+
385+ def delta(self, mapping, prefix):
386+ """
387+ return a delta containing values that have changed.
388+ """
389+ previous = self.getrange(prefix, strip=True)
390+ if not previous:
391+ pk = set()
392+ else:
393+ pk = set(previous.keys())
394+ ck = set(mapping.keys())
395+ delta = DeltaSet()
396+
397+ # added
398+ for k in ck.difference(pk):
399+ delta[k] = Delta(None, mapping[k])
400+
401+ # removed
402+ for k in pk.difference(ck):
403+ delta[k] = Delta(previous[k], None)
404+
405+ # changed
406+ for k in pk.intersection(ck):
407+ c = mapping[k]
408+ p = previous[k]
409+ if c != p:
410+ delta[k] = Delta(p, c)
411+
412+ return delta
413+
414+ @contextlib.contextmanager
415+ def hook_scope(self, name=""):
416+ """Scope all future interactions to the current hook execution
417+ revision."""
418+ assert not self.revision
419+ self.cursor.execute(
420+ 'insert into hooks (hook, date) values (?, ?)',
421+ (name or sys.argv[0],
422+ datetime.datetime.utcnow().isoformat()))
423+ self.revision = self.cursor.lastrowid
424+ try:
425+ yield self.revision
426+ self.revision = None
427+ except:
428+ self.flush(False)
429+ self.revision = None
430+ raise
431+ else:
432+ self.flush()
433+
434+ def flush(self, save=True):
435+ if save:
436+ self.conn.commit()
437+ elif self._closed:
438+ return
439+ else:
440+ self.conn.rollback()
441+
442+ def _init(self):
443+ self.cursor.execute('''
444+ create table if not exists kv (
445+ key text,
446+ data text,
447+ primary key (key)
448+ )''')
449+ self.cursor.execute('''
450+ create table if not exists kv_revisions (
451+ key text,
452+ revision integer,
453+ data text,
454+ primary key (key, revision)
455+ )''')
456+ self.cursor.execute('''
457+ create table if not exists hooks (
458+ version integer primary key autoincrement,
459+ hook text,
460+ date text
461+ )''')
462+ self.conn.commit()
463+
464+ def gethistory(self, key, deserialize=False):
465+ self.cursor.execute(
466+ '''
467+ select kv.revision, kv.key, kv.data, h.hook, h.date
468+ from kv_revisions kv,
469+ hooks h
470+ where kv.key=?
471+ and kv.revision = h.version
472+ ''', [key])
473+ if deserialize is False:
474+ return self.cursor.fetchall()
475+ return map(_parse_history, self.cursor.fetchall())
476+
477+ def debug(self, fh=sys.stderr):
478+ self.cursor.execute('select * from kv')
479+ pprint.pprint(self.cursor.fetchall(), stream=fh)
480+ self.cursor.execute('select * from kv_revisions')
481+ pprint.pprint(self.cursor.fetchall(), stream=fh)
482+
483+
484+def _parse_history(d):
485+ return (d[0], d[1], json.loads(d[2]), d[3],
486+ datetime.datetime.strptime(d[-1], "%Y-%m-%dT%H:%M:%S.%f"))
487+
488+
489+class HookData(object):
490+ """Simple integration for existing hook exec frameworks.
491+
492+ Records all unit information, and stores deltas for processing
493+ by the hook.
494+
495+ Sample::
496+
497+ from charmhelper.core import hookenv, unitdata
498+
499+ changes = unitdata.HookData()
500+ db = unitdata.kv()
501+ hooks = hookenv.Hooks()
502+
503+ @hooks.hook
504+ def config_changed():
505+ # View all changes to configuration
506+ for changed, (prev, cur) in changes.conf.items():
507+ print('config changed', changed,
508+ 'previous value', prev,
509+ 'current value', cur)
510+
511+ # Get some unit specific bookeeping
512+ if not db.get('pkg_key'):
513+ key = urllib.urlopen('https://example.com/pkg_key').read()
514+ db.set('pkg_key', key)
515+
516+ if __name__ == '__main__':
517+ with changes():
518+ hook.execute()
519+
520+ """
521+ def __init__(self):
522+ self.kv = kv()
523+ self.conf = None
524+ self.rels = None
525+
526+ @contextlib.contextmanager
527+ def __call__(self):
528+ from charmhelpers.core import hookenv
529+ hook_name = hookenv.hook_name()
530+
531+ with self.kv.hook_scope(hook_name):
532+ self._record_charm_version(hookenv.charm_dir())
533+ delta_config, delta_relation = self._record_hook(hookenv)
534+ yield self.kv, delta_config, delta_relation
535+
536+ def _record_charm_version(self, charm_dir):
537+ # Record revisions.. charm revisions are meaningless
538+ # to charm authors as they don't control the revision.
539+ # so logic dependnent on revision is not particularly
540+ # useful, however it is useful for debugging analysis.
541+ charm_rev = open(
542+ os.path.join(charm_dir, 'revision')).read().strip()
543+ charm_rev = charm_rev or '0'
544+ revs = self.kv.get('charm_revisions', [])
545+ if not charm_rev in revs:
546+ revs.append(charm_rev.strip() or '0')
547+ self.kv.set('charm_revisions', revs)
548+
549+ def _record_hook(self, hookenv):
550+ data = hookenv.execution_environment()
551+ self.conf = conf_delta = self.kv.delta(data['conf'], 'config')
552+ self.rels = rels_delta = self.kv.delta(data['rels'], 'rels')
553+ self.kv.set('env', data['env'])
554+ self.kv.set('unit', data['unit'])
555+ self.kv.set('relid', data.get('relid'))
556+ return conf_delta, rels_delta
557+
558+
559+class Record(dict):
560+
561+ __slots__ = ()
562+
563+ def __getattr__(self, k):
564+ if k in self:
565+ return self[k]
566+ raise AttributeError(k)
567+
568+
569+class DeltaSet(Record):
570+
571+ __slots__ = ()
572+
573+
574+Delta = collections.namedtuple('Delta', ['previous', 'current'])
575+
576+
577+_KV = None
578+
579+
580+def kv():
581+ global _KV
582+ if _KV is None:
583+ _KV = Storage()
584+ return _KV

Subscribers

People subscribed via source and target branches