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