Update Copyright header point to COPYING file
[cascardo/ipsilon.git] / ipsilon / admin / common.py
1 # Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING
2
3 import cherrypy
4 import logging
5 from ipsilon.util.page import Page
6 from ipsilon.util.page import admin_protect
7 from ipsilon.util.endpoint import allow_iframe
8 from ipsilon.util import config as pconfig
9
10
11 ADMIN_STATUS_OK = "success"
12 ADMIN_STATUS_ERROR = "danger"
13 ADMIN_STATUS_WARN = "warning"
14
15
16 class AdminError(Exception):
17     def __init__(self, message):
18         super(AdminError, self).__init__(message)
19         self.message = message
20
21     def __str__(self):
22         return str(self.message)
23
24
25 class AdminPage(Page):
26
27     def __init__(self, *args, **kwargs):
28         super(AdminPage, self).__init__(*args, **kwargs)
29         self.auth_protect = True
30
31
32 class AdminPluginConfig(AdminPage):
33
34     def __init__(self, po, site, parent):
35         super(AdminPluginConfig, self).__init__(site, form=True)
36         self._po = po
37         self.title = '%s plugin' % po.name
38         self.url = '%s/%s' % (parent.url, po.name)
39         self.facility = parent.facility
40         self.menu = [parent]
41         self.back = parent.url
42
43     def root_with_msg(self, message=None, message_type=None):
44         return self._template('admin/option_config.html', title=self.title,
45                               menu=self.menu, action=self.url, back=self.back,
46                               message=message, message_type=message_type,
47                               name='admin_%s_%s_form' % (self.facility,
48                                                          self._po.name),
49                               config=self._po.get_config_obj())
50
51     @admin_protect
52     def GET(self, *args, **kwargs):
53         return self.root_with_msg()
54
55     @admin_protect
56     def POST(self, *args, **kwargs):
57
58         if self._po.is_readonly:
59             return self.root_with_msg(
60                 message="Configuration is marked Read-Only",
61                 message_type=ADMIN_STATUS_WARN)
62
63         message = "Nothing was modified."
64         message_type = "info"
65         new_db_values = dict()
66
67         conf = self._po.get_config_obj()
68
69         for name, option in conf.iteritems():
70             if name in kwargs:
71                 value = kwargs[name]
72                 if isinstance(option, pconfig.List):
73                     value = [x.strip() for x in value.split('\n')]
74                 elif isinstance(option, pconfig.Condition):
75                     value = True
76             else:
77                 if isinstance(option, pconfig.Condition):
78                     value = False
79                 elif isinstance(option, pconfig.Choice):
80                     value = list()
81                     for a in option.get_allowed():
82                         aname = '%s_%s' % (name, a)
83                         if aname in kwargs:
84                             value.append(a)
85                 elif isinstance(option, pconfig.MappingList):
86                     value = get_mapping_list_value(name,
87                                                    option.get_value(),
88                                                    **kwargs)
89                     if value is None:
90                         continue
91                 elif isinstance(option, pconfig.ComplexList):
92                     value = get_complex_list_value(name,
93                                                    option.get_value(),
94                                                    **kwargs)
95                     if value is None:
96                         continue
97                 else:
98                     continue
99
100             if value != option.get_value():
101                 cherrypy.log.error("Storing [%s]: %s = %s" %
102                                    (self._po.name, name, value),
103                                    severity=logging.DEBUG)
104             option.set_value(value)
105             new_db_values[name] = option.export_value()
106
107         if len(new_db_values) != 0:
108             # First we try to save in the database
109             try:
110                 self._po.save_plugin_config(new_db_values)
111                 message = "New configuration saved."
112                 message_type = ADMIN_STATUS_OK
113             except Exception as e:  # pylint: disable=broad-except
114                 self.error('Failed to save data: %s' % e)
115                 message = "Failed to save data!"
116                 message_type = ADMIN_STATUS_ERROR
117
118             # Then refresh the actual objects
119             self._po.refresh_plugin_config()
120
121         return self.root_with_msg(message=message,
122                                   message_type=message_type)
123
124
125 class AdminPluginsOrder(AdminPage):
126
127     def __init__(self, site, parent, facility):
128         super(AdminPluginsOrder, self).__init__(site, form=True)
129         self.parent = parent
130         self.facility = facility
131         self.url = '%s/order' % parent.url
132         self.menu = [parent]
133
134     @admin_protect
135     def GET(self, *args, **kwargs):
136         return self.parent.root_with_msg()
137
138     @admin_protect
139     def POST(self, *args, **kwargs):
140
141         if self._site[self.facility].is_readonly:
142             return self.parent.root_with_msg(
143                 message="Configuration is marked Read-Only",
144                 message_type=ADMIN_STATUS_WARN)
145
146         message = "Nothing was modified."
147         message_type = "info"
148         changed = None
149         cur_enabled = self._site[self.facility].enabled
150
151         if 'order' in kwargs:
152             order = kwargs['order'].split(',')
153             if len(order) != 0:
154                 new_order = []
155                 try:
156                     for v in order:
157                         val = v.strip()
158                         if val not in cur_enabled:
159                             error = "Invalid plugin name: %s" % val
160                             raise ValueError(error)
161                         new_order.append(val)
162                     if len(new_order) < len(cur_enabled):
163                         for val in cur_enabled:
164                             if val not in new_order:
165                                 new_order.append(val)
166
167                     self.parent.save_enabled_plugins(new_order)
168
169                     # When all is saved update also live config. The
170                     # live config is the ordered list of plugin names.
171                     self._site[self.facility].refresh_enabled()
172
173                     message = "New configuration saved."
174                     message_type = ADMIN_STATUS_OK
175
176                     changed = dict()
177                     self.debug('%s -> %s' % (cur_enabled, new_order))
178                     for i in range(0, len(cur_enabled)):
179                         if cur_enabled[i] != new_order[i]:
180                             changed[cur_enabled[i]] = 'reordered'
181
182                 except ValueError, e:
183                     message = str(e)
184                     message_type = ADMIN_STATUS_ERROR
185
186                 except Exception as e:  # pylint: disable=broad-except
187                     self.error('Failed to save data: %s' % e)
188                     message = "Failed to save data!"
189                     message_type = ADMIN_STATUS_ERROR
190
191         return self.parent.root_with_msg(message=message,
192                                          message_type=message_type,
193                                          changed=changed)
194
195
196 class AdminPlugins(AdminPage):
197     def __init__(self, name, site, parent, facility, ordered=True):
198         super(AdminPlugins, self).__init__(site)
199         self._master = parent
200         self.name = name
201         self.title = '%s plugins' % name
202         self.url = '%s/%s' % (parent.url, name)
203         self.facility = facility
204         self.template = 'admin/plugins.html'
205         self.order = None
206         parent.add_subtree(name, self)
207
208         if self._site[facility] is None:
209             return
210
211         for plugin in self._site[facility].available:
212             cherrypy.log.error('Admin info plugin: %s' % plugin,
213                                severity=logging.DEBUG)
214             obj = self._site[facility].available[plugin]
215             page = AdminPluginConfig(obj, self._site, self)
216             if hasattr(obj, 'admin'):
217                 obj.admin.mount(page)
218             self.add_subtree(plugin, page)
219
220         if ordered:
221             self.order = AdminPluginsOrder(self._site, self, facility)
222
223     def save_enabled_plugins(self, names):
224         self._site[self.facility].save_enabled(names)
225
226     def root_with_msg(self, message=None, message_type=None, changed=None):
227         plugins = self._site[self.facility]
228
229         if changed is None:
230             changed = dict()
231
232         targs = {'title': self.title,
233                  'menu': self._master.menu,
234                  'message': message,
235                  'message_type': message_type,
236                  'available': plugins.available,
237                  'enabled': plugins.enabled,
238                  'changed': changed,
239                  'baseurl': self.url,
240                  'newurl': self.url}
241         if self.order:
242             targs['order_name'] = '%s_order_form' % self.name
243             targs['order_action'] = self.order.url
244
245         return self._template(self.template, **targs)
246
247     def root(self, *args, **kwargs):
248         return self.root_with_msg()
249
250     def _get_plugin_obj(self, plugin):
251         plugins = self._site[self.facility]
252         if plugins.is_readonly:
253             msg = "Configuration is marked Read-Only"
254             raise AdminError(msg)
255         if plugin not in plugins.available:
256             msg = "Unknown plugin %s" % plugin
257             raise AdminError(msg)
258         obj = plugins.available[plugin]
259         if obj.is_readonly:
260             msg = "Plugin Configuration is marked Read-Only"
261             raise AdminError(msg)
262         return obj
263
264     @admin_protect
265     def enable(self, plugin):
266         msg = None
267         try:
268             obj = self._get_plugin_obj(plugin)
269         except AdminError, e:
270             return self.root_with_msg(str(e), ADMIN_STATUS_WARN)
271         if not obj.is_enabled:
272             try:
273                 obj.enable()
274             except Exception as e:  # pylint: disable=broad-except
275                 return self.root_with_msg(str(e), ADMIN_STATUS_WARN)
276             obj.save_enabled_state()
277             msg = "Plugin %s enabled" % obj.name
278         return self.root_with_msg(msg, ADMIN_STATUS_OK,
279                                   changed={obj.name: 'enabled'})
280     enable.public_function = True
281
282     @admin_protect
283     def disable(self, plugin):
284         msg = None
285         try:
286             obj = self._get_plugin_obj(plugin)
287         except AdminError, e:
288             return self.root_with_msg(str(e), ADMIN_STATUS_WARN)
289         if obj.is_enabled:
290             try:
291                 obj.disable()
292             except Exception as e:  # pylint: disable=broad-except
293                 return self.root_with_msg(str(e), ADMIN_STATUS_WARN)
294             obj.save_enabled_state()
295             msg = "Plugin %s disabled" % obj.name
296         return self.root_with_msg(msg, ADMIN_STATUS_OK,
297                                   changed={obj.name: 'disabled'})
298     disable.public_function = True
299
300
301 class Admin(AdminPage):
302
303     def __init__(self, site, mount):
304         super(Admin, self).__init__(site)
305         self.title = 'Home'
306         self.mount = mount
307         self.url = '%s/%s' % (self.basepath, mount)
308         self.menu = [self]
309
310     def root(self, *args, **kwargs):
311         return self._template('admin/index.html',
312                               title='Configuration',
313                               baseurl=self.url,
314                               menu=self.menu)
315
316     def add_subtree(self, name, page):
317         self.__dict__[name] = page
318         self.menu.append(page)
319
320     def del_subtree(self, name):
321         self.menu.remove(self.__dict__[name])
322         del self.__dict__[name]
323
324     def get_menu_urls(self):
325         urls = dict()
326         for item in self.menu:
327             name = getattr(item, 'name', None)
328             if name:
329                 urls['%s_url' % name] = cherrypy.url('/%s/%s' % (self.mount,
330                                                                  name))
331         return urls
332
333     @admin_protect
334     @allow_iframe
335     def scheme(self):
336         cherrypy.response.headers.update({'Content-Type': 'image/svg+xml'})
337         urls = self.get_menu_urls()
338         return str(self._template('admin/ipsilon-scheme.svg', **urls))
339     scheme.public_function = True
340
341
342 def get_complex_list_value(name, old_value, **kwargs):
343     delete = list()
344     change = dict()
345     for key, val in kwargs.iteritems():
346         if not key.startswith(name):
347             continue
348         n = key[len(name):]
349         if len(n) == 0 or n[0] != ' ':
350             continue
351         try:
352             index, field = n[1:].split('-')
353         except ValueError:
354             continue
355         if field == 'delete':
356             delete.append(int(index))
357         elif field == 'name':
358             change[int(index)] = val
359
360     if len(delete) == 0 and len(change) == 0:
361         return None
362
363     value = old_value
364
365     # remove unwanted changes
366     for i in delete:
367         if i in change:
368             del change[i]
369
370     # perform requested changes
371     for index, val in change.iteritems():
372         val_list = val.split('/')
373         stripped = list()
374         for v in val_list:
375             stripped.append(v.strip())
376         if len(stripped) == 1:
377             stripped = stripped[0]
378         if len(value) <= index:
379             value.extend([None]*(index + 1 - len(value)))
380         value[index] = stripped
381
382         if len(value[index]) == 0:
383             value[index] = None
384
385     # the previous loop may add 'None' entries
386     # if any still exists mark them to be deleted
387     for i in xrange(0, len(value)):
388         if value[i] is None:
389             delete.append(i)
390
391     # remove duplicates and set in reverse order
392     delete = list(set(delete))
393     delete.sort(reverse=True)
394
395     for i in delete:
396         if len(value) > i:
397             del value[i]
398
399     if len(value) == 0:
400         value = None
401     return value
402
403
404 def get_mapping_list_value(name, old_value, **kwargs):
405     delete = list()
406     change = dict()
407     for key, val in kwargs.iteritems():
408         if not key.startswith(name):
409             continue
410         n = key[len(name):]
411         if len(n) == 0 or n[0] != ' ':
412             continue
413         try:
414             index, field = n[1:].split('-')
415         except ValueError:
416             continue
417         if field == 'delete':
418             delete.append(int(index))
419         else:
420             i = int(index)
421             if i not in change:
422                 change[i] = dict()
423             change[i][field] = val
424
425     if len(delete) == 0 and len(change) == 0:
426         return None
427
428     value = old_value
429
430     # remove unwanted changes
431     for i in delete:
432         if i in change:
433             del change[i]
434
435     # perform requested changes
436     for index, fields in change.iteritems():
437         for k in 'from', 'to':
438             if k in fields:
439                 val = fields[k]
440                 val_list = val.split('/')
441                 stripped = list()
442                 for v in val_list:
443                     stripped.append(v.strip())
444                 if len(stripped) == 1:
445                     stripped = stripped[0]
446                 if len(value) <= index:
447                     value.extend([None]*(index + 1 - len(value)))
448                 if value[index] is None:
449                     value[index] = [None, None]
450                 if k == 'from':
451                     i = 0
452                 else:
453                     i = 1
454                 value[index][i] = stripped
455
456         # eliminate incomplete/incorrect entries
457         if value[index] is not None:
458             if ((len(value[index]) != 2 or
459                  value[index][0] is None or
460                  len(value[index][0]) == 0 or
461                  value[index][1] is None or
462                  len(value[index][1]) == 0)):
463                 value[index] = None
464
465     # the previous loop may add 'None' entries
466     # if any still exists mark them to be deleted
467     for i in xrange(0, len(value)):
468         if value[i] is None:
469             delete.append(i)
470
471     # remove duplicates and set in reverse order
472     delete = list(set(delete))
473     delete.sort(reverse=True)
474
475     for i in delete:
476         if len(value) > i:
477             del value[i]
478
479     if len(value) == 0:
480         value = None
481     return value