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