Fix file permissions and remove shebang's
[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.read(self._filename)
140         return self._config
141
142
143 class FileQuery(Log):
144
145     def __init__(self, fstore, table, columns, trans=True):
146         self._fstore = fstore
147         self._config = fstore.get_config()
148         self._section = table
149         if len(columns) > 3 or columns[-1] != 'value':
150             raise ValueError('Unsupported configuration format')
151         self._columns = columns
152
153     def rollback(self):
154         return
155
156     def commit(self):
157         return
158
159     def create(self):
160         raise NotImplementedError
161
162     def drop(self):
163         raise NotImplementedError
164
165     def select(self, kvfilter=None, columns=None):
166         if self._section not in self._config.sections():
167             return []
168
169         opts = self._config.options(self._section)
170
171         prefix = None
172         prefix_ = ''
173         if self._columns[0] in kvfilter:
174             prefix = kvfilter[self._columns[0]]
175             prefix_ = prefix + ' '
176
177         name = None
178         if len(self._columns) == 3 and self._columns[1] in kvfilter:
179             name = kvfilter[self._columns[1]]
180
181         value = None
182         if self._columns[-1] in kvfilter:
183             value = kvfilter[self._columns[-1]]
184
185         res = []
186         for o in opts:
187             if len(self._columns) == 3:
188                 # 3 cols
189                 if prefix and not o.startswith(prefix_):
190                     continue
191
192                 col1, col2 = o.split(' ', 1)
193                 if name and col2 != name:
194                     continue
195
196                 col3 = self._config.get(self._section, o)
197                 if value and col3 != value:
198                     continue
199
200                 r = [col1, col2, col3]
201             else:
202                 # 2 cols
203                 if prefix and o != prefix:
204                     continue
205                 r = [o, self._config.get(self._section, o)]
206
207             if columns:
208                 s = []
209                 for c in columns:
210                     s.append(r[self._columns.index(c)])
211                 res.append(s)
212             else:
213                 res.append(r)
214
215         self.debug('SELECT(%s, %s, %s) -> %s' % (self._section,
216                                                  repr(kvfilter),
217                                                  repr(columns),
218                                                  repr(res)))
219         return res
220
221     def insert(self, values):
222         raise NotImplementedError
223
224     def update(self, values, kvfilter):
225         raise NotImplementedError
226
227     def delete(self, kvfilter):
228         raise NotImplementedError
229
230
231 class Store(Log):
232     def __init__(self, config_name=None, database_url=None):
233         if config_name is None and database_url is None:
234             raise ValueError('config_name or database_url must be provided')
235         if config_name:
236             if config_name not in cherrypy.config:
237                 raise NameError('Unknown database %s' % config_name)
238             name = cherrypy.config[config_name]
239         else:
240             name = database_url
241         if name.startswith('configfile://'):
242             _, filename = name.split('://')
243             self._db = FileStore(filename)
244             self._query = FileQuery
245         else:
246             self._db = SqlStore(name)
247             self._query = SqlQuery
248
249     @property
250     def is_readonly(self):
251         return self._db.is_readonly
252
253     def _row_to_dict_tree(self, data, row):
254         name = row[0]
255         if len(row) > 2:
256             if name not in data:
257                 data[name] = dict()
258             d2 = data[name]
259             self._row_to_dict_tree(d2, row[1:])
260         else:
261             value = row[1]
262             if name in data:
263                 if data[name] is list:
264                     data[name].append(value)
265                 else:
266                     v = data[name]
267                     data[name] = [v, value]
268             else:
269                 data[name] = value
270
271     def _rows_to_dict_tree(self, rows):
272         data = dict()
273         for r in rows:
274             self._row_to_dict_tree(data, r)
275         return data
276
277     def _load_data(self, table, columns, kvfilter=None):
278         rows = []
279         try:
280             q = self._query(self._db, table, columns, trans=False)
281             rows = q.select(kvfilter)
282         except Exception, e:  # pylint: disable=broad-except
283             self.error("Failed to load data for table %s: [%s]" % (table, e))
284         return self._rows_to_dict_tree(rows)
285
286     def load_config(self):
287         table = 'config'
288         columns = ['name', 'value']
289         return self._load_data(table, columns)
290
291     def load_options(self, table, name=None):
292         kvfilter = dict()
293         if name:
294             kvfilter['name'] = name
295         options = self._load_data(table, OPTIONS_COLUMNS, kvfilter)
296         if name and name in options:
297             return options[name]
298         return options
299
300     def save_options(self, table, name, options):
301         curvals = dict()
302         q = None
303         try:
304             q = self._query(self._db, table, OPTIONS_COLUMNS)
305             rows = q.select({'name': name}, ['option', 'value'])
306             for row in rows:
307                 curvals[row[0]] = row[1]
308
309             for opt in options:
310                 if opt in curvals:
311                     q.update({'value': options[opt]},
312                              {'name': name, 'option': opt})
313                 else:
314                     q.insert((name, opt, options[opt]))
315
316             q.commit()
317         except Exception, e:  # pylint: disable=broad-except
318             if q:
319                 q.rollback()
320             self.error("Failed to save options: [%s]" % e)
321             raise
322
323     def delete_options(self, table, name, options=None):
324         kvfilter = {'name': name}
325         q = None
326         try:
327             q = self._query(self._db, table, OPTIONS_COLUMNS)
328             if options is None:
329                 q.delete(kvfilter)
330             else:
331                 for opt in options:
332                     kvfilter['option'] = opt
333                     q.delete(kvfilter)
334             q.commit()
335         except Exception, e:  # pylint: disable=broad-except
336             if q:
337                 q.rollback()
338             self.error("Failed to delete from %s: [%s]" % (table, e))
339             raise
340
341     def new_unique_data(self, table, data):
342         newid = str(uuid.uuid4())
343         q = None
344         try:
345             q = self._query(self._db, table, UNIQUE_DATA_COLUMNS)
346             for name in data:
347                 q.insert((newid, name, data[name]))
348             q.commit()
349         except Exception, e:  # pylint: disable=broad-except
350             if q:
351                 q.rollback()
352             self.error("Failed to store %s data: [%s]" % (table, e))
353             raise
354         return newid
355
356     def get_unique_data(self, table, uuidval=None, name=None, value=None):
357         kvfilter = dict()
358         if uuidval:
359             kvfilter['uuid'] = uuidval
360         if name:
361             kvfilter['name'] = name
362         if value:
363             kvfilter['value'] = value
364         return self._load_data(table, UNIQUE_DATA_COLUMNS, kvfilter)
365
366     def save_unique_data(self, table, data):
367         q = None
368         try:
369             q = self._query(self._db, table, UNIQUE_DATA_COLUMNS)
370             for uid in data:
371                 curvals = dict()
372                 rows = q.select({'uuid': uid}, ['name', 'value'])
373                 for r in rows:
374                     curvals[r[0]] = r[1]
375
376                 datum = data[uid]
377                 for name in datum:
378                     if name in curvals:
379                         q.update({'value': datum[name]},
380                                  {'uuid': uid, 'name': name})
381                     else:
382                         q.insert((uid, name, datum[name]))
383
384             q.commit()
385         except Exception, e:  # pylint: disable=broad-except
386             if q:
387                 q.rollback()
388             self.error("Failed to store data in %s: [%s]" % (table, e))
389             raise
390
391     def del_unique_data(self, table, uuidval):
392         kvfilter = {'uuid': uuidval}
393         try:
394             q = self._query(self._db, table, UNIQUE_DATA_COLUMNS, trans=False)
395             q.delete(kvfilter)
396         except Exception, e:  # pylint: disable=broad-except
397             self.error("Failed to delete data from %s: [%s]" % (table, e))
398
399     def _reset_data(self, table):
400         try:
401             q = self._query(self._db, table, UNIQUE_DATA_COLUMNS)
402             q.drop()
403             q.create()
404             q.commit()
405         except Exception, e:  # pylint: disable=broad-except
406             if q:
407                 q.rollback()
408             self.error("Failed to erase all data from %s: [%s]" % (table, e))
409
410
411 class AdminStore(Store):
412
413     def __init__(self):
414         super(AdminStore, self).__init__('admin.config.db')
415
416     def get_data(self, plugin, idval=None, name=None, value=None):
417         return self.get_unique_data(plugin+"_data", idval, name, value)
418
419     def save_data(self, plugin, data):
420         return self.save_unique_data(plugin+"_data", data)
421
422     def new_datum(self, plugin, datum):
423         table = plugin+"_data"
424         return self.new_unique_data(table, datum)
425
426     def del_datum(self, plugin, idval):
427         table = plugin+"_data"
428         return self.del_unique_data(table, idval)
429
430     def wipe_data(self, plugin):
431         table = plugin+"_data"
432         self._reset_data(table)
433
434
435 class UserStore(Store):
436
437     def __init__(self, path=None):
438         super(UserStore, self).__init__('user.prefs.db')
439
440     def save_user_preferences(self, user, options):
441         self.save_options('users', user, options)
442
443     def load_user_preferences(self, user):
444         return self.load_options('users', user)
445
446     def save_plugin_data(self, plugin, user, options):
447         self.save_options(plugin+"_data", user, options)
448
449     def load_plugin_data(self, plugin, user):
450         return self.load_options(plugin+"_data", user)
451
452
453 class TranStore(Store):
454
455     def __init__(self, path=None):
456         super(TranStore, self).__init__('transactions.db')