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

Subscribers

People subscribed via source and target branches