SP Portal administrative interface
[cascardo/ipsilon.git] / ipsilon / util / config.py
1 # Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING
2
3 from ipsilon.util.log import Log
4 import os
5 import json
6 import base64
7 import imghdr
8 import hashlib
9 import cherrypy
10
11
12 def name_from_image(image):
13     if image is None:
14         return None
15
16     fext = imghdr.what(None, base64.b64decode(image))
17     m = hashlib.sha1()
18     m.update(base64.b64decode(image))
19
20     return '%s.%s' % (m.hexdigest(), fext)
21
22
23 def url_from_image(image):
24     if image is None:
25         return None
26
27     return '%s/cache/%s' % (
28         cherrypy.config.get('base.mount', ""),
29         name_from_image(image)
30     )
31
32
33 class Config(Log):
34
35     def __init__(self, name, *args):
36         self.name = name
37         self._list = list()
38         self._dict = dict()
39         for item in args:
40             if not isinstance(item, Option):
41                 raise ValueError('Invalid option type for %s' % repr(item))
42             self._list.append(item.name)
43             self._dict[item.name] = item
44         self.debug('Config(%s) %s' % (self.name, self._dict))
45
46     def __repr__(self):
47         return '%s: %s' % (self.__class__, ', '.join(self._list))
48
49     def __str__(self):
50         return str(self._list)
51
52     def __len__(self):
53         return len(self._list)
54
55     def __getitem__(self, key):
56         return self._dict[key]
57
58     def __setitem__(self, key, value):
59         if not isinstance(value, Option):
60             raise ValueError('Invalid type for %s' % value)
61         if key != value.name:
62             raise NameError('Name mismatch, key=%s but value.name=%s' % (
63                 key, value.name))
64         if key not in self._list:
65             self._list.append(key)
66         self._dict[key] = value
67
68     def __delitem__(self, key):
69         self._list.remove(key)
70         del self._dict[key]
71
72     def __iter__(self):
73         i = 0
74         while i < len(self._list):
75             yield self._list[i]
76             i += 1
77
78     def __reversed__(self):
79         i = len(self._list)
80         while i > 0:
81             yield self._list[i - 1]
82             i -= 1
83
84     def __contains__(self, item):
85         return (item in self._dict)
86
87     def iteritems(self):
88         i = 0
89         while i < len(self._list):
90             yield (self._list[i], self._dict[self._list[i]])
91             i += 1
92
93     def items(self):
94         return [(k, self._dict[k]) for k in self._list]
95
96
97 class Option(Log):
98
99     def __init__(self, name, description, readonly=False):
100         self.name = name
101         self.description = description
102         self._default_value = None
103         self._assigned_value = None
104         self._readonly = readonly
105
106     def __repr__(self):
107         return "%s: %s {%s}, value = %s [def: %s] readonly=%s" % (
108             self.__class__,
109             self.name,
110             self.description,
111             self._assigned_value,
112             self._default_value,
113             self._readonly)
114
115     def __str__(self):
116         return '%s=%s' % (self.name, self.get_value())
117
118     def get_value(self, default=True):
119         if self._assigned_value is not None:
120             return self._assigned_value
121         elif default is True:
122             return self._default_value
123         else:
124             return None
125
126     def set_value(self, value):
127         self._assigned_value = value
128
129     def export_value(self):
130         raise NotImplementedError
131
132     def import_value(self, value):
133         raise NotImplementedError
134
135     def _str_export_value(self):
136         if self._assigned_value:
137             return str(self._assigned_value)
138         return None
139
140     def _str_import_value(self, value):
141         if not isinstance(value, str):
142             raise ValueError('Value must be string')
143         self._assigned_value = value
144
145     def is_readonly(self):
146         return self._readonly
147
148
149 class String(Option):
150
151     def __init__(self, name, description, default_value=None, readonly=False):
152         super(String, self).__init__(name, description, readonly=readonly)
153         self._default_value = str(default_value)
154
155     def set_value(self, value):
156         self._assigned_value = str(value)
157
158     def export_value(self):
159         return self._str_export_value()
160
161     def import_value(self, value):
162         self._str_import_value(value)
163
164
165 class Image(Option):
166     """
167     An image has two components: the binary blob of the image itself and
168     the SHA1 sum of the image.
169
170     We only need the image blob when writing to the cache file or
171     updating the database.
172
173     For the purposes of the UI we only need the filename which is
174     the SHA1 sum of file type the blob + file type.
175     """
176
177     def __init__(self, name, description, default_value=None, readonly=False):
178         super(Image, self).__init__(name, description, readonly=readonly)
179         self._image = None
180
181         if default_value:
182             self._image = default_value
183
184         self._assigned_value = url_from_image(self._image)
185         self.__write_cache_file()
186
187     def set_value(self, value):
188         if value is None:
189             return None
190
191         if os.path.exists(self.__filename()):
192             try:
193                 os.remove(self.__filename())
194             except IOError as e:
195                 self.error('Error removing %s: %s' % (self.__filename(), e))
196
197         self._image = base64.b64encode(value)
198         self._assigned_value = url_from_image(value)
199
200     def export_value(self):
201         if self._image is None:
202             return None
203
204         self.__write_cache_file()
205         return base64.b64decode(self._image)
206
207     def import_value(self, value):
208         if value is None:
209             return None
210
211         if os.path.exists(self.__filename()):
212             try:
213                 os.remove(self.__filename())
214             except IOError as e:
215                 self.error('Error removing %s: %s' % (self.__filename(), e))
216         self._image = base64.b64encode(value)
217         self._assigned_value = url_from_image(self._image)
218         self.__write_cache_file()
219
220     def __filename(self):
221         if self._image is None:
222             return None
223
224         cdir = cherrypy.config.get('cache_dir', '/var/cache/ipsilon')
225
226         return '%s/%s' % (cdir, name_from_image(self._image))
227
228     def __write_cache_file(self):
229         if self._image is None:
230             return None
231
232         if not os.path.exists(self.__filename()):
233             with open(self.__filename(), 'w') as imagefile:
234                 imagefile.write(base64.b64decode(self._image))
235
236
237 class Template(Option):
238
239     def __init__(self, name, description, default_template=None,
240                  readonly=False):
241         super(Template, self).__init__(name, description, readonly=readonly)
242         self._default_value = str(default_template)
243
244     def set_value(self, value):
245         self._assigned_value = str(value)
246
247     def templatize(self, args):
248         if not args:
249             raise ValueError('Templatized called w/o arguments')
250
251         return self.get_value() % args
252
253     def export_value(self):
254         return self._str_export_value()
255
256     def import_value(self, value):
257         self._str_import_value(value)
258
259
260 class List(Option):
261
262     def __init__(self, name, description, default_list=None, readonly=False):
263         super(List, self).__init__(name, description, readonly=readonly)
264         if default_list:
265             self._default_value = default_list
266         else:
267             self._default_value = []
268
269     def set_value(self, value):
270         self._assigned_value = list(value)
271
272     def export_value(self):
273         if self._assigned_value:
274             return ','.join(self._assigned_value)
275         return None
276
277     def import_value(self, value):
278         if not isinstance(value, str):
279             raise ValueError('Value (type: %s) must be string' % type(value))
280         self._assigned_value = [x.strip() for x in value.split(',')]
281
282
283 class ComplexList(List):
284
285     def _check_value(self, value):
286         if value is None:
287             return
288         if not isinstance(value, list):
289             raise ValueError('The value type must be a list, not "%s"' %
290                              type(value))
291
292     def set_value(self, value):
293         self._check_value(value)
294         self._assigned_value = value
295
296     def export_value(self):
297         if self._assigned_value:
298             return json.dumps(self._assigned_value)
299         return None
300
301     def import_value(self, value):
302         if not isinstance(value, str):
303             raise ValueError('The value type must be a string, not "%s"' %
304                              type(value))
305         jsonval = json.loads(value)
306         self.set_value(jsonval)
307
308
309 class MappingList(ComplexList):
310
311     def _check_value(self, value):
312         if value is None:
313             return
314         if not isinstance(value, list):
315             raise ValueError('The value type must be a list, not "%s"' %
316                              type(value))
317         for v in value:
318             if not isinstance(v, list):
319                 raise ValueError('Each element must be a list, not "%s"' %
320                                  type(v))
321             if len(v) != 2:
322                 raise ValueError('Each element must contain 2 values,'
323                                  ' not %d' % len(v))
324
325     def import_value(self, value):
326         if not isinstance(value, str):
327             raise ValueError('Value (type: %s) must be string' % type(value))
328         jsonval = json.loads(value)
329         self.set_value(jsonval)
330
331
332 class Choice(Option):
333
334     def __init__(self, name, description, allowed=None, default=None,
335                  readonly=False):
336         super(Choice, self).__init__(name, description, readonly=readonly)
337         if allowed:
338             self._allowed_values = list(allowed)
339         else:
340             self._allowed_values = list()
341         self._default_value = list()
342         if default is None:
343             default = []
344         for name in default:
345             if name not in self._allowed_values:
346                 raise ValueError(
347                     'item [%s] is not in allowed [%s]' % (name, allowed))
348             self._default_value.append(name)
349
350     def __repr__(self):
351         return "%s: %s {%s}, values = %s d:%s ok:%s" % (self.__class__,
352                                                         self.name,
353                                                         self.description,
354                                                         self._assigned_value,
355                                                         self._default_value,
356                                                         self._allowed_values)
357
358     def __str__(self):
359         return '%s=%s' % (self.name, self.get_value())
360
361     def set_value(self, value):
362         if not isinstance(value, list):
363             value = [value]
364         self._assigned_value = list()
365         for val in value:
366             if val not in self._allowed_values:
367                 raise ValueError(
368                     'Value "%s" not allowed [%s]' % (val,
369                                                      self._allowed_values))
370             self._assigned_value.append(val)
371
372         if not self._assigned_value:
373             self._assigned_value = None
374
375     def unset_value(self, value):
376         if isinstance(value, str):
377             value = [value]
378         unset = list()
379         for val in value:
380             unset.append((val, False))
381         self.set_value(unset)
382
383     def get_allowed(self):
384         return self._allowed_values
385
386     def export_value(self):
387         enabled = self.get_value()
388         return ', '.join(enabled)
389
390     def import_value(self, value):
391         enabled = [x.strip() for x in value.split(',')]
392         if enabled:
393             if self._assigned_value is None:
394                 self._assigned_value = list()
395         for val in enabled:
396             if val not in self._allowed_values:
397                 # We silently ignore invalid options on import for now
398                 continue
399             self._assigned_value.append(val)
400
401
402 class Pick(Option):
403
404     def __init__(self, name, description, allowed, default_value,
405                  readonly=False):
406         super(Pick, self).__init__(name, description, readonly=readonly)
407         self._allowed_values = list(allowed)
408         if default_value not in self._allowed_values:
409             raise ValueError('The default value is not in the allowed list')
410         self._default_value = default_value
411
412     def set_value(self, value):
413         if value not in self._allowed_values:
414             raise ValueError(
415                 'Value "%s" not allowed [%s]' % (value, self._allowed_values))
416         self._assigned_value = value
417
418     def get_allowed(self):
419         return self._allowed_values
420
421     def export_value(self):
422         return self._str_export_value()
423
424     def import_value(self, value):
425         self._str_import_value(value)
426
427
428 class Condition(Pick):
429
430     def __init__(self, name, description, default_value=False,
431                  readonly=False):
432         # The db stores 1/0. Convert the passed-in value if
433         # necessary
434         if default_value in [u'1', 'True', True]:
435             default_value = True
436         else:
437             default_value = False
438         super(Condition, self).__init__(name, description,
439                                         [True, False], default_value,
440                                         readonly=readonly)
441
442     def import_value(self, value):
443         self._assigned_value = value
444
445
446 class ConfigHelper(Log):
447
448     def __init__(self):
449         self._config = None
450
451     def new_config(self, name, *config_args):
452         self._config = Config(name, *config_args)
453
454     def get_config_obj(self):
455         if self._config is None:
456             raise AttributeError('Config not initialized')
457         return self._config
458
459     def import_config(self, config):
460         if not self._config:
461             raise AttributeError('Config not initialized, cannot import')
462
463         for key, value in config.iteritems():
464             if key in self._config:
465                 self._config[key].import_value(str(value))
466
467     def export_config(self):
468         config = dict()
469         for name, option in self._config.iteritems():
470             config[name] = option.export_value()
471         return config
472
473     def get_config_value(self, name):
474         if not self._config:
475             raise AttributeError('Config not initialized')
476         return self._config[name].get_value()
477
478     def set_config_value(self, name, value):
479         if not self._config:
480             raise AttributeError('Config not initialized')
481         return self._config[name].set_value(value)