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

Proposed by Edward Hope-Morley
Status: Merged
Merged at revision: 102
Proposed branch: lp:~hopem/charms/trusty/nova-compute/charm-helpers-sync
Merge into: lp:~openstack-charmers-archive/charms/trusty/nova-compute/next
Diff against target: 716 lines (+559/-28)
5 files modified
hooks/charmhelpers/contrib/network/ufw.py (+63/-15)
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/nova-compute/charm-helpers-sync
Reviewer Review Type Date Requested Status
Liam Young (community) Approve
Review via email: mp+249317@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 #1886 nova-compute-next for hopem mp249317
    LINT OK: passed

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

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

charm_unit_test #1717 nova-compute-next for hopem mp249317
    UNIT OK: passed

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

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

charm_amulet_test #1864 nova-compute-next for hopem mp249317
    AMULET OK: passed

Build: http://10.245.162.77:8080/job/charm_amulet_test/1864/

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

Subscribers

People subscribed via source and target branches