Add option to source configuration from a file
[cascardo/ipsilon.git] / ipsilon / util / data.py
1 #!/usr/bin/python
2 #
3 # Copyright (C) 2013  Simo Sorce <simo@redhat.com>
4 #
5 # see file 'COPYING' for use and warranty information
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program 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 General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20 import cherrypy
21 from ipsilon.util.log import Log
22 from sqlalchemy import create_engine
23 from sqlalchemy import MetaData, Table, Column, Text
24 from sqlalchemy.sql import select
25 import ConfigParser
26 import os
27 import uuid
28
29
30 OPTIONS_COLUMNS = ['name', 'option', 'value']
31 UNIQUE_DATA_COLUMNS = ['uuid', 'name', 'value']
32
33
34 class SqlStore(Log):
35
36     def __init__(self, name):
37         engine_name = name
38         if '://' not in engine_name:
39             engine_name = 'sqlite:///' + engine_name
40         self._dbengine = create_engine(engine_name)
41         self.is_readonly = False
42
43     def engine(self):
44         return self._dbengine
45
46     def connection(self):
47         return self._dbengine.connect()
48
49
50 def SqlAutotable(f):
51     def at(self, *args, **kwargs):
52         self.create()
53         return f(self, *args, **kwargs)
54     return at
55
56
57 class SqlQuery(Log):
58
59     def __init__(self, db_obj, table, columns, trans=True):
60         self._db = db_obj
61         self._con = self._db.connection()
62         self._trans = self._con.begin() if trans else None
63         self._table = self._get_table(table, columns)
64
65     def _get_table(self, name, columns):
66         table = Table(name, MetaData(self._db.engine()))
67         for c in columns:
68             table.append_column(Column(c, Text()))
69         return table
70
71     def _where(self, kvfilter):
72         where = None
73         if kvfilter is not None:
74             for k in kvfilter:
75                 w = self._table.columns[k] == kvfilter[k]
76                 if where is None:
77                     where = w
78                 else:
79                     where = where & w
80         return where
81
82     def _columns(self, columns=None):
83         cols = None
84         if columns is not None:
85             cols = []
86             for c in columns:
87                 cols.append(self._table.columns[c])
88         else:
89             cols = self._table.columns
90         return cols
91
92     def rollback(self):
93         self._trans.rollback()
94
95     def commit(self):
96         self._trans.commit()
97
98     def create(self):
99         self._table.create(checkfirst=True)
100
101     def drop(self):
102         self._table.drop(checkfirst=True)
103
104     @SqlAutotable
105     def select(self, kvfilter=None, columns=None):
106         return self._con.execute(select(self._columns(columns),
107                                         self._where(kvfilter)))
108
109     @SqlAutotable
110     def insert(self, values):
111         self._con.execute(self._table.insert(values))
112
113     @SqlAutotable
114     def update(self, values, kvfilter):
115         self._con.execute(self._table.update(self._where(kvfilter), values))
116
117     @SqlAutotable
118     def delete(self, kvfilter):
119         self._con.execute(self._table.delete(self._where(kvfilter)))
120
121
122 class FileStore(Log):
123
124     def __init__(self, name):
125         self._filename = name
126         self.is_readonly = True
127         self._timestamp = None
128         self._config = None
129
130     def get_config(self):
131         try:
132             stat = os.stat(self._filename)
133         except OSError, e:
134             self.error("Unable to check config file %s: [%s]" % (
135                 self._filename, e))
136             self._config = None
137             raise
138         timestamp = stat.st_mtime
139         if self._config is None or timestamp > self._timestamp:
140             self._config = ConfigParser.RawConfigParser()
141             self._config.read(self._filename)
142         return self._config
143
144
145 class FileQuery(Log):
146
147     def __init__(self, fstore, table, columns, trans=True):
148         self._fstore = fstore
149         self._config = fstore.get_config()
150         self._section = table
151         if len(columns) > 3 or columns[-1] != 'value':
152             raise ValueError('Unsupported configuration format')
153         self._columns = columns
154
155     def rollback(self):
156         return
157
158     def commit(self):
159         return
160
161     def create(self):
162         raise NotImplementedError
163
164     def drop(self):
165         raise NotImplementedError
166
167     def select(self, kvfilter=None, columns=None):
168         if self._section not in self._config.sections():
169             return []
170
171         opts = self._config.options(self._section)
172
173         prefix = None
174         prefix_ = ''
175         if self._columns[0] in kvfilter:
176             prefix = kvfilter[self._columns[0]]
177             prefix_ = prefix + ' '
178
179         name = None
180         if len(self._columns) == 3 and self._columns[1] in kvfilter:
181             name = kvfilter[self._columns[1]]
182
183         value = None
184         if self._columns[-1] in kvfilter:
185             value = kvfilter[self._columns[-1]]
186
187         res = []
188         for o in opts:
189             if len(self._columns) == 3:
190                 # 3 cols
191                 if prefix and not o.startswith(prefix_):
192                     continue
193
194                 col1, col2 = o.split(' ', 1)
195                 if name and col2 != name:
196                     continue
197
198                 col3 = self._config.get(self._section, o)
199                 if value and col3 != value:
200                     continue
201
202                 r = [col1, col2, col3]
203             else:
204                 # 2 cols
205                 if prefix and o != prefix:
206                     continue
207                 r = [o, self._config.get(self._section, o)]
208
209             if columns:
210                 s = []
211                 for c in columns:
212                     s.append(r[self._columns.index(c)])
213                 res.append(s)
214             else:
215                 res.append(r)
216
217         self.debug('SELECT(%s, %s, %s) -> %s' % (self._section,
218                                                  repr(kvfilter),
219                                                  repr(columns),
220                                                  repr(res)))
221         return res
222
223     def insert(self, values):
224         raise NotImplementedError
225
226     def update(self, values, kvfilter):
227         raise NotImplementedError
228
229     def delete(self, kvfilter):
230         raise NotImplementedError
231
232
233 class Store(Log):
234     def __init__(self, config_name):
235         if config_name not in cherrypy.config:
236             raise NameError('Unknown database %s' % config_name)
237         name = cherrypy.config[config_name]
238         if name.startswith('configfile://'):
239             _, filename = name.split('://')
240             self._db = FileStore(filename)
241             self._query = FileQuery
242         else:
243             self._db = SqlStore(name)
244             self._query = SqlQuery
245
246     @property
247     def is_readonly(self):
248         return self._db.is_readonly
249
250     def _row_to_dict_tree(self, data, row):
251         name = row[0]
252         if len(row) > 2:
253             if name not in data:
254                 data[name] = dict()
255             d2 = data[name]
256             self._row_to_dict_tree(d2, row[1:])
257         else:
258             value = row[1]
259             if name in data:
260                 if data[name] is list:
261                     data[name].append(value)
262                 else:
263                     v = data[name]
264                     data[name] = [v, value]
265             else:
266                 data[name] = value
267
268     def _rows_to_dict_tree(self, rows):
269         data = dict()
270         for r in rows:
271             self._row_to_dict_tree(data, r)
272         return data
273
274     def _load_data(self, table, columns, kvfilter=None):
275         rows = []
276         try:
277             q = self._query(self._db, table, columns, trans=False)
278             rows = q.select(kvfilter)
279         except Exception, e:  # pylint: disable=broad-except
280             self.error("Failed to load data for table %s: [%s]" % (table, e))
281         return self._rows_to_dict_tree(rows)
282
283     def load_config(self):
284         table = 'config'
285         columns = ['name', 'value']
286         return self._load_data(table, columns)
287
288     def load_options(self, table, name=None):
289         kvfilter = dict()
290         if name:
291             kvfilter['name'] = name
292         options = self._load_data(table, OPTIONS_COLUMNS, kvfilter)
293         if name and name in options:
294             return options[name]
295         return options
296
297     def save_options(self, table, name, options):
298         curvals = dict()
299         q = None
300         try:
301             q = self._query(self._db, table, OPTIONS_COLUMNS)
302             rows = q.select({'name': name}, ['option', 'value'])
303             for row in rows:
304                 curvals[row[0]] = row[1]
305
306             for opt in options:
307                 if opt in curvals:
308                     q.update({'value': options[opt]},
309                              {'name': name, 'option': opt})
310                 else:
311                     q.insert((name, opt, options[opt]))
312
313             q.commit()
314         except Exception, e:  # pylint: disable=broad-except
315             if q:
316                 q.rollback()
317             self.error("Failed to save options: [%s]" % e)
318             raise
319
320     def delete_options(self, table, name, options=None):
321         kvfilter = {'name': name}
322         q = None
323         try:
324             q = self._query(self._db, table, OPTIONS_COLUMNS)
325             if options is None:
326                 q.delete(kvfilter)
327             else:
328                 for opt in options:
329                     kvfilter['option'] = opt
330                     q.delete(kvfilter)
331             q.commit()
332         except Exception, e:  # pylint: disable=broad-except
333             if q:
334                 q.rollback()
335             self.error("Failed to delete from %s: [%s]" % (table, e))
336             raise
337
338     def new_unique_data(self, table, data):
339         newid = str(uuid.uuid4())
340         q = None
341         try:
342             q = self._query(self._db, table, UNIQUE_DATA_COLUMNS)
343             for name in data:
344                 q.insert((newid, name, data[name]))
345             q.commit()
346         except Exception, e:  # pylint: disable=broad-except
347             if q:
348                 q.rollback()
349             self.error("Failed to store %s data: [%s]" % (table, e))
350             raise
351         return newid
352
353     def get_unique_data(self, table, uuidval=None, name=None, value=None):
354         kvfilter = dict()
355         if uuidval:
356             kvfilter['uuid'] = uuidval
357         if name:
358             kvfilter['name'] = name
359         if value:
360             kvfilter['value'] = value
361         return self._load_data(table, UNIQUE_DATA_COLUMNS, kvfilter)
362
363     def save_unique_data(self, table, data):
364         q = None
365         try:
366             q = self._query(self._db, table, UNIQUE_DATA_COLUMNS)
367             for uid in data:
368                 curvals = dict()
369                 rows = q.select({'uuid': uid}, ['name', 'value'])
370                 for r in rows:
371                     curvals[r[0]] = r[1]
372
373                 datum = data[uid]
374                 for name in datum:
375                     if name in curvals:
376                         q.update({'value': datum[name]},
377                                  {'uuid': uid, 'name': name})
378                     else:
379                         q.insert((uid, name, datum[name]))
380
381             q.commit()
382         except Exception, e:  # pylint: disable=broad-except
383             if q:
384                 q.rollback()
385             self.error("Failed to store data in %s: [%s]" % (table, e))
386             raise
387
388     def del_unique_data(self, table, uuidval):
389         kvfilter = {'uuid': uuidval}
390         try:
391             q = self._query(self._db, table, UNIQUE_DATA_COLUMNS, trans=False)
392             q.delete(kvfilter)
393         except Exception, e:  # pylint: disable=broad-except
394             self.error("Failed to delete data from %s: [%s]" % (table, e))
395
396     def _reset_data(self, table):
397         try:
398             q = self._query(self._db, table, UNIQUE_DATA_COLUMNS)
399             q.drop()
400             q.create()
401             q.commit()
402         except Exception, e:  # pylint: disable=broad-except
403             if q:
404                 q.rollback()
405             self.error("Failed to erase all data from %s: [%s]" % (table, e))
406
407
408 class AdminStore(Store):
409
410     def __init__(self):
411         super(AdminStore, self).__init__('admin.config.db')
412
413     def get_data(self, plugin, idval=None, name=None, value=None):
414         return self.get_unique_data(plugin+"_data", idval, name, value)
415
416     def save_data(self, plugin, data):
417         return self.save_unique_data(plugin+"_data", data)
418
419     def new_datum(self, plugin, datum):
420         table = plugin+"_data"
421         return self.new_unique_data(table, datum)
422
423     def del_datum(self, plugin, idval):
424         table = plugin+"_data"
425         return self.del_unique_data(table, idval)
426
427     def wipe_data(self, plugin):
428         table = plugin+"_data"
429         self._reset_data(table)
430
431
432 class UserStore(Store):
433
434     def __init__(self, path=None):
435         super(UserStore, self).__init__('user.prefs.db')
436
437     def save_user_preferences(self, user, options):
438         self.save_options('users', user, options)
439
440     def load_user_preferences(self, user):
441         return self.load_options('users', user)
442
443     def save_plugin_data(self, plugin, user, options):
444         self.save_options(plugin+"_data", user, options)
445
446     def load_plugin_data(self, plugin, user):
447         return self.load_options(plugin+"_data", user)
448
449
450 class TranStore(Store):
451
452     def __init__(self, path=None):
453         super(TranStore, self).__init__('transactions.db')