Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

vigiboard / vigiboard / controllers / root.py @ 0c8b0e15

History | View | Annotate | Download (24.7 KB)

1
# -*- coding: utf-8 -*-
2
# vim:set expandtab tabstop=4 shiftwidth=4: 
3
"""Vigiboard Controller"""
4

    
5
from datetime import datetime
6
from time import mktime
7
import math
8
import urllib
9

    
10
from tg.exceptions import HTTPNotFound
11
from tg import expose, validate, require, flash, \
12
    tmpl_context, request, config, session, redirect, url
13
from tw.forms import validators
14
from pylons.i18n import ugettext as _
15
from pylons.i18n import lazy_ugettext as l_
16
from pylons.controllers.util import abort
17
from sqlalchemy import asc
18
from sqlalchemy.sql import func
19
from repoze.what.predicates import Any, not_anonymous
20

    
21
from vigilo.models.configure import DBSession
22
from vigilo.models import Event, EventHistory, CorrEvent, SupItem, \
23
                            HostGroup, ServiceGroup, StateName, User
24
from vigilo.models.functions import sql_escape_like
25
from vigilo.models.secondary_tables import EVENTSAGGREGATE_TABLE
26

    
27
from vigilo.turbogears.controllers.autocomplete \
28
    import make_autocomplete_controller
29
from vigiboard.controllers.vigiboardrequest import VigiboardRequest
30
from vigiboard.controllers.vigiboard_controller import VigiboardRootController
31
from vigiboard.widgets.edit_event import edit_event_status_options
32
from vigiboard.lib.base import BaseController
33

    
34
__all__ = ('RootController', 'get_last_modification_timestamp', 
35
           'date_to_timestamp')
36

    
37
# pylint: disable-msg=R0201
38
class RootController(VigiboardRootController):
39
    """
40
    Le controller général de vigiboard
41
    """
42
    autocomplete = make_autocomplete_controller(BaseController)
43

    
44
    def process_form_errors(self, *argv, **kwargv):
45
        """
46
        Gestion des erreurs de validation : On affiche les erreurs
47
        puis on redirige vers la dernière page accédée.
48
        """
49
        for k in tmpl_context.form_errors:
50
            flash("'%s': %s" % (k, tmpl_context.form_errors[k]), 'error')
51
        if request.environ.get('HTTP_REFERER') :
52
            redirect(request.environ.get('HTTP_REFERER'
53
                ).split(request.environ.get('HTTP_HOST'))[1])
54
        else :
55
            redirect('/')
56

    
57
    @validate(validators={
58
            'page': validators.Int(min=1),
59
        }, error_handler=process_form_errors)
60
    @expose('events_table.html')
61
    @require(Any(not_anonymous(), msg=l_("You need to be authenticated")))
62
    def default(self, page=1, hostgroup=None, servicegroup=None,
63
            host=None, service=None, output=None, trouble_ticket=None,
64
            from_date=None, to_date=None, *argv, **krgv):
65
        """
66
        Page d'accueil de Vigiboard. Elle affiche, suivant la page demandée
67
        (page 1 par defaut), la liste des événements, rangés par ordre de prise
68
        en compte, puis de sévérité.
69
        Pour accéder à cette page, l'utilisateur doit être authentifié.
70

71
        @param page: Numéro de la page souhaitée, commence à 1
72
        @param host: Si l'utilisateur souhaite sélectionner seulement certains
73
                     événements suivant leur hôte, il peut placer une expression
74
                     ici en suivant la structure du LIKE en SQL
75
        @param service: Idem que host mais sur les services
76
        @param output: Idem que host mais sur le text explicatif
77
        @param trouble_ticket: Idem que host mais sur les tickets attribués
78
        """
79
        if not page:
80
            page = 1
81

    
82
        username = request.environ['repoze.who.identity']['repoze.who.userid']
83
        user = User.by_user_name(username)
84

    
85
        aggregates = VigiboardRequest(user)
86
        aggregates.add_table(
87
            CorrEvent,
88
            aggregates.items.c.hostname,
89
            aggregates.items.c.servicename
90
        )
91
        aggregates.add_join((Event, CorrEvent.idcause == Event.idevent))
92
        aggregates.add_join((aggregates.items, 
93
            Event.idsupitem == aggregates.items.c.idsupitem))
94
        aggregates.add_order_by(asc(aggregates.items.c.hostname))
95
        
96
        search = {
97
            'host': '',
98
            'service': '',
99
            'output': '',
100
            'tt': '',
101
            'from_date': '',
102
            'to_date': '',
103
            'hostgroup': '',
104
            'servicegroup': '',
105
        }
106

    
107
        # Application des filtres si nécessaire
108
        if hostgroup:
109
            search['hostgroup'] = hostgroup
110
            hostgroup = sql_escape_like(hostgroup)
111
            aggregates.add_join((HostGroup, HostGroup.idgroup == \
112
                aggregates.items.c.idhostgroup))
113
            aggregates.add_filter(HostGroup.name.ilike('%%%s%%' % hostgroup))
114

    
115
        if servicegroup:
116
            search['servicegroup'] = servicegroup
117
            servicegroup = sql_escape_like(servicegroup)
118
            aggregates.add_join((ServiceGroup, ServiceGroup.idgroup == \
119
                aggregates.items.c.idservicegroup))
120
            aggregates.add_filter(
121
                ServiceGroup.name.ilike('%%%s%%' % servicegroup))
122

    
123
        if host:
124
            search['host'] = host
125
            host = sql_escape_like(host)
126
            aggregates.add_filter(aggregates.items.c.hostname.ilike(
127
                '%%%s%%' % host))
128

    
129
        if service:
130
            search['service'] = service
131
            service = sql_escape_like(service)
132
            aggregates.add_filter(aggregates.items.c.servicename.ilike(
133
                '%%%s%%' % service))
134

    
135
        if output:
136
            search['output'] = output
137
            output = sql_escape_like(output)
138
            aggregates.add_filter(Event.message.ilike('%%%s%%' % output))
139

    
140
        if trouble_ticket:
141
            search['tt'] = trouble_ticket
142
            trouble_ticket = sql_escape_like(trouble_ticket)
143
            aggregates.add_filter(CorrEvent.trouble_ticket.ilike(
144
                '%%%s%%' % trouble_ticket))
145

    
146
        if from_date:
147
            search['from_date'] = from_date
148
            try:
149
                # TRANSLATORS: Format de date et heure.
150
                from_date = datetime.strptime(
151
                    from_date, _('%Y-%m-%d %I:%M:%S %p'))
152
            except ValueError:
153
                to_date = None
154
            aggregates.add_filter(CorrEvent.timestamp_active >= from_date)
155

    
156
        if to_date:
157
            search['to_date'] = to_date
158
            try:
159
                # TRANSLATORS: Format de date et heure.
160
                to_date = datetime.strptime(
161
                    to_date, _('%Y-%m-%d %I:%M:%S %p'))
162
            except ValueError:
163
                to_date = None
164
            aggregates.add_filter(CorrEvent.timestamp_active <= to_date)
165

    
166
        # Calcul des éléments à afficher et du nombre de pages possibles
167
        total_rows = aggregates.num_rows()
168
        items_per_page = int(config['vigiboard_items_per_page'])
169

    
170
        id_first_row = items_per_page * (page-1)
171
        id_last_row = min(id_first_row + items_per_page, total_rows)
172

    
173
        aggregates.format_events(id_first_row, id_last_row)
174
        aggregates.generate_tmpl_context()
175

    
176
        nb_pages = int(math.ceil(total_rows / (items_per_page + 0.0)))
177
        if not total_rows:
178
            id_first_row = 0
179
        else:
180
            id_first_row += 1
181

    
182
        return dict(
183
            events = aggregates.events,
184
            plugins = get_plugins_instances(),
185
            rows_info = {
186
                'id_first_row': id_first_row,
187
                'id_last_row': id_last_row,
188
                'total_rows': total_rows,
189
            },
190
            nb_pages = nb_pages,
191
            page = page,
192
            event_edit_status_options = edit_event_status_options,
193
            search = search,
194
            refresh_times = config['vigiboard_refresh_times'],
195
        )
196

    
197
    @validate(validators={
198
            'idcorrevent': validators.Int(not_empty=True),
199
            'page': validators.Int(min=1),
200
        }, error_handler=process_form_errors)
201
    @expose('raw_events_table.html')
202
    @require(Any(not_anonymous(), msg=l_("You need to be authenticated")))
203
    def masked_events(self, idcorrevent, page=1):
204
        """
205
        Affichage de la liste des événements bruts masqués dans un
206
        événement corrélé (agrégés).
207

208
        @param idevent: identifiant de l'événement souhaité
209
        """
210
        if not page:
211
            page = 1
212

    
213
        username = request.environ['repoze.who.identity']['repoze.who.userid']
214
        events = VigiboardRequest(User.by_user_name(username), False)
215
        events.add_table(
216
            Event,
217
            events.items.c.hostname,
218
            events.items.c.servicename,
219
        )
220
        events.add_join((EVENTSAGGREGATE_TABLE, \
221
            EVENTSAGGREGATE_TABLE.c.idevent == Event.idevent))
222
        events.add_join((CorrEvent, CorrEvent.idcorrevent == \
223
            EVENTSAGGREGATE_TABLE.c.idcorrevent))
224
        events.add_join((events.items, 
225
            Event.idsupitem == events.items.c.idsupitem))
226
        events.add_filter(Event.idevent != CorrEvent.idcause)
227
        events.add_filter(CorrEvent.idcorrevent == idcorrevent)
228

    
229
        # Vérification que l'événement existe
230
        total_rows = events.num_rows()
231
        if total_rows < 1:
232
            flash(_('No masked event or access denied'), 'error')
233
            redirect('/')
234

    
235
        # Calcul des éléments à afficher et du nombre de pages possibles
236
        items_per_page = int(config['vigiboard_items_per_page'])
237

    
238
        id_first_row = items_per_page * (page-1)
239
        id_last_row = min(id_first_row + items_per_page, total_rows)
240

    
241
        events.format_events(id_first_row, id_last_row)
242
        events.generate_tmpl_context()
243

    
244
        nb_pages = int(math.ceil(total_rows / (items_per_page + 0.0)))
245
        if not total_rows:
246
            id_first_row = 0
247
        else:
248
            id_first_row += 1
249

    
250
        return dict(
251
            idcorrevent = idcorrevent,
252
            events = events.events,
253
            plugins = get_plugins_instances(),
254
            rows_info = {
255
                'id_first_row': id_first_row,
256
                'id_last_row': id_last_row,
257
                'total_rows': total_rows,
258
            },
259
            nb_pages = nb_pages,
260
            page = page,
261
            search = {
262
                'host': '',
263
                'service': '',
264
                'output': '',
265
                'tt': '',
266
                'from_date': '',
267
                'to_date': '',
268
                'hostgroup': '',
269
                'servicegroup': '',
270
            },
271
           refresh_times=config['vigiboard_refresh_times'],
272
        )
273

    
274
    @validate(validators={
275
            'idevent': validators.Int(not_empty=True),
276
            'page': validators.Int(min=1),
277
        }, error_handler=process_form_errors)
278
    @expose('history_table.html')
279
    @require(Any(not_anonymous(), msg=l_("You need to be authenticated")))
280
    def event(self, idevent, page=1):
281
        """
282
        Affichage de l'historique d'un événement brut.
283
        Pour accéder à cette page, l'utilisateur doit être authentifié.
284

285
        @param idevent: identifiant de l'événement brut souhaité.
286
        @type idevent: C{int}
287
        """
288
        if not page:
289
            page = 1
290

    
291
        username = request.environ['repoze.who.identity']['repoze.who.userid']
292
        events = VigiboardRequest(User.by_user_name(username), False)
293
        events.add_table(
294
            Event,
295
            events.items.c.hostname,
296
            events.items.c.servicename,
297
        )
298
        events.add_join((EVENTSAGGREGATE_TABLE, \
299
            EVENTSAGGREGATE_TABLE.c.idevent == Event.idevent))
300
        events.add_join((CorrEvent, CorrEvent.idcorrevent == \
301
            EVENTSAGGREGATE_TABLE.c.idcorrevent))
302
        events.add_join((events.items, 
303
            Event.idsupitem == events.items.c.idsupitem))
304
        events.add_filter(Event.idevent == idevent)
305

    
306
        if events.num_rows() != 1:
307
            flash(_('No such event or access denied'), 'error')
308
            redirect('/')
309

    
310
        events.format_events(0, 1)
311
        events.generate_tmpl_context()
312
        history = events.format_history()
313

    
314
        total_rows = history.count()
315
        items_per_page = int(config['vigiboard_items_per_page'])
316

    
317
        id_first_row = items_per_page * (page-1)
318
        id_last_row = min(id_first_row + items_per_page, total_rows)
319

    
320
        history_entries = history[id_first_row : id_last_row]
321

    
322
        nb_pages = int(math.ceil(total_rows / (items_per_page + 0.0)))
323
        if not total_rows:
324
            id_first_row = 0
325
        else:
326
            id_first_row += 1
327

    
328
        return dict(
329
            idevent = idevent,
330
            plugins = get_plugins_instances(),
331
            rows_info = {
332
                'id_first_row': id_first_row,
333
                'id_last_row': id_last_row,
334
                'total_rows': total_rows,
335
            },
336
            nb_pages = nb_pages,
337
            page = page,
338
            history = history_entries,
339
            search = {
340
                'host': '',
341
                'service': '',
342
                'output': '',
343
                'tt': '',
344
                'from_date': '',
345
                'to_date': '',
346
                'hostgroup': '',
347
                'servicegroup': '',
348
            },
349
           refresh_times=config['vigiboard_refresh_times'],
350
        )
351

    
352
    @validate(
353
        validators={
354
            'host': validators.NotEmpty(),
355
#            'service': validators.NotEmpty(),
356
            'page': validators.Int(min=1),
357
        }, 
358
        error_handler = process_form_errors)
359
    @expose('events_table.html')
360
    @require(Any(not_anonymous(), msg=l_("You need to be authenticated")))
361
    def item(self, host, service=None, page=1):
362
        """
363
        Affichage de l'historique de l'ensemble des événements corrélés
364
        jamais ouverts sur l'hôte / service demandé.
365
        Pour accéder à cette page, l'utilisateur doit être authentifié.
366

367
        @param host: Nom de l'hôte souhaité.
368
        @param service: Nom du service souhaité
369
        """
370
        if not page:
371
            page = 1
372
            
373
        idsupitem = SupItem.get_supitem(host, service)
374

    
375
        username = request.environ['repoze.who.identity']['repoze.who.userid']
376
        aggregates = VigiboardRequest(User.by_user_name(username), False)
377
        aggregates.add_table(
378
            CorrEvent,
379
            aggregates.items.c.hostname,
380
            aggregates.items.c.servicename,
381
        )
382
        aggregates.add_join((Event, CorrEvent.idcause == Event.idevent))
383
        aggregates.add_join((aggregates.items, 
384
            Event.idsupitem == aggregates.items.c.idsupitem))
385
        aggregates.add_filter(aggregates.items.c.idsupitem == idsupitem)
386

    
387
        # Vérification qu'il y a au moins 1 événement qui correspond
388
        total_rows = aggregates.num_rows()
389
        if not total_rows:
390
            flash(_('No access to this host/service or no event yet'), 'error')
391
            redirect('/')
392

    
393
        items_per_page = int(config['vigiboard_items_per_page'])
394

    
395
        id_first_row = items_per_page * (page-1)
396
        id_last_row = min(id_first_row + items_per_page, total_rows)
397

    
398
        aggregates.format_events(id_first_row, id_last_row)
399
        aggregates.generate_tmpl_context()
400

    
401
        nb_pages = int(math.ceil(total_rows / (items_per_page + 0.0)))
402
        if not total_rows:
403
            id_first_row = 0
404
        else:
405
            id_first_row += 1
406
        
407
        return dict(
408
            hostname = host,
409
            servicename = service,
410
            events = aggregates.events,
411
            item_label = item_label,
412
            plugins = get_plugins_instances(),
413
            rows_info = {
414
                'id_first_row': id_first_row,
415
                'id_last_row': id_last_row,
416
                'total_rows': total_rows,
417
            },
418
            nb_pages = nb_pages,
419
            page = page,
420
            event_edit_status_options = edit_event_status_options,
421
            search = {
422
                'host': '',
423
                'service': '',
424
                'output': '',
425
                'tt': '',
426
                'from_date': '',
427
                'to_date': '',
428
                'hostgroup': '',
429
                'servicegroup': '',
430
            },
431
            refresh_times=config['vigiboard_refresh_times'],
432
        )
433

    
434
    @validate(validators={
435
        "id": validators.Regex(r'^[0-9]+(,[0-9]+)*,?$'),
436
#        "trouble_ticket": validators.Regex(r'^[0-9]*$'),
437
        "ack": validators.OneOf([
438
            u'NoChange',
439
            u'None',
440
            u'Acknowledged',
441
            u'AAClosed'
442
        ])}, error_handler=process_form_errors)
443
    @require(Any(not_anonymous(), msg=l_("You need to be authenticated")))
444
    def update(self, **krgv):
445
        """
446
        Mise à jour d'un événement suivant les arguments passés.
447
        Cela peut être un changement de ticket ou un changement de statut.
448
        
449
        @param krgv['id']: Le ou les identifiants des événements à traiter
450
        @param krgv['last_modification']: La date de la dernière modification
451
        dont l'utilisateur est au courant.
452
        @param krgv['tt']: Nouveau numéro du ticket associé.
453
        @param krgv['status']: Nouveau status de/des événements.
454
        """
455

    
456
        # On vérifie que des identifiants ont bien été transmis via
457
        # le formulaire, et on informe l'utilisateur le cas échéant.
458
        if krgv['id'] is None:
459
            flash(_('No event has been selected'), 'warning')
460
            raise redirect(request.environ.get('HTTP_REFERER', url('/')))
461

    
462
        # Le filtre permet d'éliminer les chaines vides contenues dans le
463
        # tableau ('a,b,' -> split -> ['a','b',''] -> filter -> ['a','b']).
464
        ids = map(int, filter(len, krgv['id'].split(',')))
465

    
466
        # Si l'utilisateur édite plusieurs événements à la fois,
467
        # il nous faut chacun des identifiants
468
       
469
        username = request.environ['repoze.who.identity']['repoze.who.userid']
470
        events = VigiboardRequest(User.by_user_name(username))
471
        events.add_table(CorrEvent)
472
        events.add_join((Event, CorrEvent.idcause == Event.idevent))
473
        events.add_join((events.items, 
474
            Event.idsupitem == events.items.c.idsupitem))
475
        events.add_filter(CorrEvent.idcorrevent.in_(ids))
476
        
477
        events.generate_request()
478
        idevents = [cause.idcause for cause in events.req]
479
        # Si des changements sont survenus depuis que la 
480
        # page est affichée, on en informe l'utilisateur.
481
        last_modification = get_last_modification_timestamp(idevents, None)
482
        if last_modification and datetime.fromtimestamp(\
483
            float(krgv['last_modification'])) < last_modification:
484
            flash(_('Changes have occurred since the page was last displayed, '
485
                    'your changes HAVE NOT been saved.'), 'warning')
486
            raise redirect(request.environ.get('HTTP_REFERER', url('/')))
487
        
488
        # Vérification que au moins un des identifiants existe et est éditable
489
        if not events.num_rows():
490
            flash(_('No access to this event'), 'error')
491
            redirect('/')
492
        
493
        # Modification des événements et création d'un historique
494
        # pour chacun d'eux.
495
        for req in events.req:
496
            if isinstance(req, CorrEvent):
497
                event = req
498
            else:
499
                event = req[0]
500

    
501
            if krgv['trouble_ticket'] != '' :
502
                history = EventHistory(
503
                        type_action="Ticket change",
504
                        idevent=event.idcause,
505
                        value=krgv['trouble_ticket'],
506
                        text="Changed trouble ticket from '%s' to '%s'" % (
507
                            event.trouble_ticket, krgv['trouble_ticket']
508
                        ),
509
                        username=username,
510
                        timestamp=datetime.now(),
511
                    )
512
                DBSession.add(history)   
513
                event.trouble_ticket = krgv['trouble_ticket']
514

    
515
            if krgv['ack'] != 'NoChange' :
516
                history = EventHistory(
517
                        type_action="Acknowledgement change state",
518
                        idevent=event.idcause,
519
                        value=krgv['ack'],
520
                        text="Changed acknowledgement status "
521
                            "from '%s' to '%s'" % (
522
                            event.status, krgv['ack']
523
                        ),
524
                        username=username,
525
                        timestamp=datetime.now(),
526
                    )
527
                DBSession.add(history)
528
                event.status = krgv['ack']
529

    
530
        DBSession.flush()
531
        flash(_('Updated successfully'))
532
        redirect(request.environ.get('HTTP_REFERER', url('/')))
533

    
534
    @validate(validators={
535
        "plugin_name": validators.OneOf([i[0] for i \
536
            in config.get('vigiboard_plugins', [])]),
537
        'idcorrevent': validators.Int(not_empty=True),
538
        }, error_handler=process_form_errors)
539
    @expose('json')
540
    @require(Any(not_anonymous(), msg=l_("You need to be authenticated")))
541
    def get_plugin_value(self, idcorrevent, plugin_name, *arg, **krgv):
542
        """
543
        Permet de récupérer la valeur d'un plugin associée à un CorrEvent
544
        donné via JSON.
545
        """
546
        plugins = config.get('vigiboard_plugins', {})
547

    
548
        # Permet de vérifier si l'utilisateur a bien les permissions
549
        # pour accéder à cet événement et si l'événement existe.
550
        username = request.environ['repoze.who.identity']['repoze.who.userid']
551
        events = VigiboardRequest(User.by_user_name(username))
552
        events.add_table(CorrEvent.idcorrevent)
553
        events.add_join((Event, CorrEvent.idcause == Event.idevent))
554
        events.add_join((events.items, 
555
            Event.idsupitem == events.items.c.idsupitem))
556
        events.add_filter(CorrEvent.idcorrevent == idcorrevent)
557

    
558
        # Pas d'événement ou permission refusée. On ne distingue pas
559
        # les 2 cas afin d'éviter la divulgation d'informations.
560
        if not events.num_rows():
561
            raise HTTPNotFound(_('No such incident or insufficient permissions'))
562

    
563
        plugin_class = [p[1] for p in plugins if p[0] == plugin_name]
564
        if not plugin_class:
565
            raise HTTPNotFound(_('No such plugin'))
566

    
567
        plugin_class = plugin_class[0]
568
        try:
569
            mypac = __import__(
570
                'vigiboard.controllers.plugins.' + plugin_name,
571
                globals(), locals(), [plugin_class], -1)
572
            plugin = getattr(mypac, plugin_class)
573
            if callable(plugin):
574
                return plugin().get_value(idcorrevent, *arg, **krgv)
575
            raise HTTPInternalServerError(_('Not a valid plugin'))
576
        except ImportError:
577
            raise HTTPInternalServerError(_('Plugin could not be loaded'))
578

    
579
    @validate(validators={
580
        "fontsize": validators.Regex(
581
            r'[0-9]+(pt|px|em|%)',
582
            regexOps = ('I',)
583
        )}, error_handler = process_form_errors)
584
    @expose('json')
585
    def set_fontsize(self, fontsize):
586
        """Enregistre la taille de la police dans les préférences."""
587
        session['fontsize'] = fontsize
588
        session.save()
589
        return dict()
590

    
591
    @validate(validators={"refresh": validators.Int()},
592
            error_handler=process_form_errors)
593
    @expose('json')
594
    def set_refresh(self, refresh):
595
        """Enregistre le temps de rafraichissement dans les préférences."""
596
        session['refresh'] = refresh
597
        session.save()
598
        return dict()
599

    
600
    @expose('json')
601
    def set_theme(self, theme):
602
        """Enregistre le thème à utiliser dans les préférences."""
603
        # On sauvegarde l'ID du thème sans vérifications
604
        # car les thèmes (styles CSS) sont définies dans
605
        # les packages de thèmes (ex: vigilo-themes-default).
606
        # La vérification de la valeur est faite dans les templates.
607
        session['theme'] = theme
608
        session.save()
609
        return dict()
610
    
611
def get_last_modification_timestamp(event_id_list, 
612
                                    value_if_none=datetime.now()):
613
    """
614
    Récupère le timestamp de la dernière modification 
615
    opérée sur l'un des événements dont l'identifiant
616
    fait partie de la liste passée en paramètre.
617
    """
618
    last_modification_timestamp = DBSession.query(
619
                                func.max(EventHistory.timestamp),
620
                         ).filter(EventHistory.idevent.in_(event_id_list)
621
                         ).scalar()
622
    if not last_modification_timestamp:
623
        if not value_if_none:
624
            return None
625
        else:
626
            last_modification_timestamp = value_if_none
627
    return datetime.fromtimestamp(mktime(
628
        last_modification_timestamp.timetuple()))
629

    
630
def get_plugins_instances():
631
    """
632
    Renvoie une liste d'instances de plugins pour VigiBoard.
633

634
    @return: Liste de tuples contenant le nom du plugin et l'instance associé.
635
    @rtype: C{list} of C{tuple}
636
    """
637
    plugins = config.get('vigiboard_plugins', [])
638
    plugins_instances = []
639
    for (plugin_name, plugin_class) in plugins:
640
        try:
641
            mypac = __import__(
642
                'vigiboard.controllers.plugins.' + plugin_name,
643
                globals(), locals(), [plugin_class], -1)
644
            plugin = getattr(mypac, plugin_class)
645
            if callable(plugin):
646
                plugins_instances.append((plugin_name, plugin()))
647
        except ImportError:
648
            pass
649
    return plugins_instances
650