~openstack-charmers-archive/charms/precise/ceph-osd/trunk

« back to all changes in this revision

Viewing changes to hooks/charmhelpers/core/unitdata.py

  • Committer: Liam Young
  • Date: 2015-02-26 13:37:18 UTC
  • mfrom: (36.2.5 ceph-osd)
  • Revision ID: liam.young@canonical.com-20150226133718-pbqm6sbxi2jeuxpc
[bradm, r=gnuoy] Add setting of nagios_servicegroups

Show diffs side-by-side

added added

removed removed

Lines of Context:
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
 
"""
25
 
Intro
26
 
-----
27
 
 
28
 
A simple way to store state in units. This provides a key value
29
 
storage with support for versioned, transactional operation,
30
 
and can calculate deltas from previous values to simplify unit logic
31
 
when processing changes.
32
 
 
33
 
 
34
 
Hook Integration
35
 
----------------
36
 
 
37
 
There are several extant frameworks for hook execution, including
38
 
 
39
 
 - charmhelpers.core.hookenv.Hooks
40
 
 - charmhelpers.core.services.ServiceManager
41
 
 
42
 
The storage classes are framework agnostic, one simple integration is
43
 
via the HookData contextmanager. It will record the current hook
44
 
execution environment (including relation data, config data, etc.),
45
 
setup a transaction and allow easy access to the changes from
46
 
previously seen values. One consequence of the integration is the
47
 
reservation of particular keys ('rels', 'unit', 'env', 'config',
48
 
'charm_revisions') for their respective values.
49
 
 
50
 
Here'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
 
 
83
 
A more basic integration is via the hook_scope context manager which simply
84
 
manages 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
 
 
95
 
Usage
96
 
-----
97
 
 
98
 
Values are automatically json de/serialized to preserve basic typing
99
 
and complex data struct capabilities (dicts, lists, ints, booleans, etc).
100
 
 
101
 
Individual 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
 
 
117
 
Groups 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
 
 
123
 
When updating values, its very helpful to understand which values
124
 
have actually changed and how have they changed. The storage
125
 
provides 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
 
 
136
 
Note the delta method does not persist the actual change, it needs to
137
 
be explicitly saved via 'update' method::
138
 
 
139
 
   >>> kv.update(data, 'config.')
140
 
 
141
 
Values modified in the context of a hook scope retain historical values
142
 
associated 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
 
 
152
 
import collections
153
 
import contextlib
154
 
import datetime
155
 
import json
156
 
import os
157
 
import pprint
158
 
import sqlite3
159
 
import sys
160
 
 
161
 
__author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>'
162
 
 
163
 
 
164
 
class 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
 
 
377
 
def _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
 
 
382
 
class 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 charm_rev not 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
 
 
452
 
class 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
 
 
462
 
class DeltaSet(Record):
463
 
 
464
 
    __slots__ = ()
465
 
 
466
 
 
467
 
Delta = collections.namedtuple('Delta', ['previous', 'current'])
468
 
 
469
 
 
470
 
_KV = None
471
 
 
472
 
 
473
 
def kv():
474
 
    global _KV
475
 
    if _KV is None:
476
 
        _KV = Storage()
477
 
    return _KV