Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

vigiboard / vigiboard / controllers / root.py @ 6f6efdcd

History | View | Annotate | Download (32.4 KB)

1
# -*- coding: utf-8 -*-
2
# vim:set expandtab tabstop=4 shiftwidth=4:
3
################################################################################
4
#
5
# Copyright (C) 2007-2011 CS-SI
6
#
7
# This program is free software; you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License version 2 as
9
# published by the Free Software Foundation.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
19
################################################################################
20

    
21
"""VigiBoard Controller"""
22

    
23
from datetime import datetime
24
from time import mktime
25
import math
26

    
27
from tg.exceptions import HTTPNotFound, HTTPInternalServerError
28
from tg import expose, validate, require, flash, \
29
    tmpl_context, request, config, session, redirect
30
from tw.forms import validators
31
from pylons.i18n import ugettext as _, lazy_ugettext as l_
32
from sqlalchemy import asc
33
from sqlalchemy.sql import func
34
from sqlalchemy.orm import aliased
35
from repoze.what.predicates import Any, All, in_group, \
36
                                    has_permission, not_anonymous, \
37
                                    NotAuthorizedError
38
from formencode import schema
39
from pkg_resources import working_set
40

    
41
from vigilo.models.session import DBSession
42
from vigilo.models.tables import Event, EventHistory, CorrEvent, Host, \
43
                                    SupItem, SupItemGroup, LowLevelService, \
44
                                    StateName, State
45
from vigilo.models.functions import sql_escape_like
46
from vigilo.models.tables.secondary_tables import EVENTSAGGREGATE_TABLE
47

    
48
from vigilo.turbogears.controllers.autocomplete import AutoCompleteController
49
from vigilo.turbogears.controllers.proxy import ProxyController
50
from vigilo.turbogears.controllers.api.root import ApiRootController
51
from vigilo.turbogears.helpers import get_current_user
52

    
53
from vigiboard.controllers.vigiboardrequest import VigiboardRequest
54
from vigiboard.controllers.vigiboard_controller import VigiboardRootController
55

    
56
from vigiboard.widgets.edit_event import edit_event_status_options
57
from vigiboard.widgets.search_form import create_search_form, get_calendar_lang
58

    
59
__all__ = ('RootController', 'get_last_modification_timestamp',
60
           'date_to_timestamp')
61

    
62
# pylint: disable-msg=R0201
63
class RootController(VigiboardRootController):
64
    """
65
    Le controller général de vigiboard
66
    """
67
    autocomplete = AutoCompleteController()
68
    nagios = ProxyController('nagios', '/nagios/',
69
        not_anonymous(l_('You need to be authenticated')))
70
    api = ApiRootController("/api")
71

    
72

    
73
    # Prédicat pour la restriction de l'accès aux interfaces.
74
    # L'utilisateur doit avoir la permission "vigiboard-access"
75
    # ou appartenir au groupe "managers" pour accéder à VigiBoard.
76
    access_restriction = All(
77
        not_anonymous(msg=l_("You need to be authenticated")),
78
        Any(in_group('managers'),
79
            has_permission('vigiboard-access'),
80
            msg=l_("You don't have access to VigiBoard"))
81
    )
82

    
83
    def process_form_errors(self, *argv, **kwargv):
84
        """
85
        Gestion des erreurs de validation : On affiche les erreurs
86
        puis on redirige vers la dernière page accédée.
87
        """
88
        for k in tmpl_context.form_errors:
89
            flash("'%s': %s" % (k, tmpl_context.form_errors[k]), 'error')
90
        redirect(request.environ.get('HTTP_REFERER', '/'))
91

    
92
    @expose('json')
93
    def handle_validation_errors_json(self, *args, **kwargs):
94
        kwargs['errors'] = tmpl_context.form_errors
95
        return dict(kwargs)
96

    
97
    class DefaultSchema(schema.Schema):
98
        """Schéma de validation de la méthode default."""
99
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
100
        supitemgroup = validators.Int(if_missing=None, if_invalid=None)
101
        host = validators.String(if_missing=None)
102
        service = validators.String(if_missing=None)
103
        output = validators.String(if_missing=None)
104
        trouble_ticket = validators.String(if_missing=None)
105
        from_date = validators.String(if_missing=None)
106
        to_date = validators.String(if_missing=None)
107

    
108
    @validate(
109
        validators=DefaultSchema(),
110
        error_handler = process_form_errors)
111
    @expose('events_table.html')
112
    @require(access_restriction)
113
    def default(self, page, supitemgroup, host, service,
114
                output, trouble_ticket, from_date, to_date):
115
        """
116
        Page d'accueil de Vigiboard. Elle affiche, suivant la page demandée
117
        (page 1 par defaut), la liste des événements, rangés par ordre de prise
118
        en compte, puis de sévérité.
119
        Pour accéder à cette page, l'utilisateur doit être authentifié.
120

121
        @param page: Numéro de la page souhaitée, commence à 1
122
        @param host: Si l'utilisateur souhaite sélectionner seulement certains
123
                     événements suivant leur hôte, il peut placer une expression
124
                     ici en suivant la structure du LIKE en SQL
125
        @param service: Idem que host mais sur les services
126
        @param output: Idem que host mais sur le text explicatif
127
        @param trouble_ticket: Idem que host mais sur les tickets attribués
128

129
        Cette méthode permet de satisfaire les exigences suivantes :
130
            - VIGILO_EXIG_VIGILO_BAC_0040,
131
            - VIGILO_EXIG_VIGILO_BAC_0070,
132
            - VIGILO_EXIG_VIGILO_BAC_0100,
133
        """
134
        user = get_current_user()
135
        aggregates = VigiboardRequest(user)
136
        aggregates.add_table(
137
            CorrEvent,
138
            aggregates.items.c.hostname,
139
            aggregates.items.c.servicename
140
        )
141
        aggregates.add_join((Event, CorrEvent.idcause == Event.idevent))
142
        aggregates.add_join((aggregates.items,
143
            Event.idsupitem == aggregates.items.c.idsupitem))
144
        aggregates.add_order_by(asc(aggregates.items.c.hostname))
145

    
146
        search = {}
147

    
148
        # Application des filtres si nécessaire
149
        if supitemgroup:
150
            base = DBSession.query(SupItemGroup).get(supitemgroup)
151
            if base:
152
                search['supitemgroup'] = supitemgroup
153
                sig = aliased(SupItemGroup)
154
                aggregates.add_join(
155
                    (sig, aggregates.items.c.idsupitemgroup == sig.idgroup),
156
                )
157
                aggregates.add_filter(sig.left >= base.left)
158
                aggregates.add_filter(sig.right <= base.right)
159

    
160
        if host:
161
            search['host_'] = host
162
            host = sql_escape_like(host)
163
            aggregates.add_filter(aggregates.items.c.hostname.ilike(host))
164

    
165
        if service:
166
            search['service'] = service
167
            service = sql_escape_like(service)
168
            aggregates.add_filter(aggregates.items.c.servicename.ilike(service))
169

    
170
        if output:
171
            search['output'] = output
172
            output = sql_escape_like(output)
173
            aggregates.add_filter(Event.message.ilike(output))
174

    
175
        if trouble_ticket:
176
            search['tt'] = trouble_ticket
177
            trouble_ticket = sql_escape_like(trouble_ticket)
178
            aggregates.add_filter(CorrEvent.trouble_ticket.ilike(trouble_ticket))
179

    
180
        if from_date:
181
            search['from_date'] = from_date.lower()
182
            try:
183
                # TRANSLATORS: Format de date et heure Python/JavaScript.
184
                # TRANSLATORS: http://www.dynarch.com/static/jscalendar-1.0/doc/html/reference.html#node_sec_5.3.5
185
                # TRANSLATORS: http://docs.python.org/release/2.5/lib/module-time.html
186
                from_date = datetime.strptime(
187
                    from_date.encode('utf8'),
188
                    _('%Y-%m-%d %I:%M:%S %p').encode('utf8'))
189
            except ValueError:
190
                # On ignore silencieusement la date invalide reçue.
191
                pass
192
            else:
193
                aggregates.add_filter(CorrEvent.timestamp_active >= from_date)
194

    
195
        if to_date:
196
            search['to_date'] = to_date.lower()
197
            try:
198
                # TRANSLATORS: Format de date et heure.
199
                # TRANSLATORS: http://www.dynarch.com/static/jscalendar-1.0/doc/html/reference.html#node_sec_5.3.5
200
                # TRANSLATORS: http://docs.python.org/release/2.5/lib/module-time.html
201
                to_date = datetime.strptime(
202
                    to_date.encode('utf8'),
203
                    _('%Y-%m-%d %I:%M:%S %p').encode('utf8'))
204
            except ValueError:
205
                # On ignore silencieusement la date invalide reçue.
206
                pass
207
            else:
208
                aggregates.add_filter(CorrEvent.timestamp_active <= to_date)
209

    
210

    
211
        # Calcul des éléments à afficher et du nombre de pages possibles
212
        total_rows = aggregates.num_rows()
213
        items_per_page = int(config['vigiboard_items_per_page'])
214

    
215
        id_first_row = items_per_page * (page-1)
216
        id_last_row = min(id_first_row + items_per_page, total_rows)
217

    
218
        # Si le numéro de page dépasse le nombre de pages existantes,
219
        # on redirige automatiquement vers la 1ère page.
220
        if total_rows and id_first_row >= total_rows:
221
            redirect('/', page=total_rows / items_per_page, **search)
222

    
223
        aggregates.format_events(id_first_row, id_last_row)
224
        aggregates.generate_tmpl_context()
225

    
226
        nb_pages = int(math.ceil(total_rows / (items_per_page + 0.0)))
227
        if not total_rows:
228
            id_first_row = 0
229
        else:
230
            id_first_row += 1
231

    
232
        return dict(
233
            hostname = None,
234
            servicename = None,
235
            events = aggregates.events,
236
            rows_info = {
237
                'id_first_row': id_first_row,
238
                'id_last_row': id_last_row,
239
                'total_rows': total_rows,
240
            },
241
            nb_pages = nb_pages,
242
            page = page,
243
            event_edit_status_options = edit_event_status_options,
244
            search_form = create_search_form,
245
            search = search,
246
            get_calendar_lang = get_calendar_lang,
247
        )
248

    
249

    
250
    class MaskedEventsSchema(schema.Schema):
251
        """Schéma de validation de la méthode masked_events."""
252
        idcorrevent = validators.Int(not_empty=True)
253
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
254

    
255
    @validate(
256
        validators=MaskedEventsSchema(),
257
        error_handler = process_form_errors)
258
    @expose('raw_events_table.html')
259
    @require(access_restriction)
260
    def masked_events(self, idcorrevent, page):
261
        """
262
        Affichage de la liste des événements bruts masqués d'un événement
263
        corrélé (événements agrégés dans l'événement corrélé).
264

265
        @param idcorrevent: identifiant de l'événement corrélé souhaité.
266
        @type idcorrevent: C{int}
267
        """
268
        user = get_current_user()
269

    
270
        # Récupère la liste des événements masqués de l'événement
271
        # corrélé donné par idcorrevent.
272
        events = VigiboardRequest(user, False)
273
        events.add_table(
274
            Event,
275
            events.items.c.hostname,
276
            events.items.c.servicename,
277
        )
278
        events.add_join((EVENTSAGGREGATE_TABLE, \
279
            EVENTSAGGREGATE_TABLE.c.idevent == Event.idevent))
280
        events.add_join((CorrEvent, CorrEvent.idcorrevent == \
281
            EVENTSAGGREGATE_TABLE.c.idcorrevent))
282
        events.add_join((events.items,
283
            Event.idsupitem == events.items.c.idsupitem))
284
        events.add_filter(Event.idevent != CorrEvent.idcause)
285
        events.add_filter(CorrEvent.idcorrevent == idcorrevent)
286

    
287
        # Récupère l'instance de SupItem associé à la cause de
288
        # l'événement corrélé. Cette instance est utilisé pour
289
        # obtenir le nom d'hôte/service auquel la cause est
290
        # rattachée (afin de fournir un contexte à l'utilisateur).
291
        hostname = None
292
        servicename = None
293
        cause_supitem = DBSession.query(
294
                SupItem,
295
            ).join(
296
                (Event, Event.idsupitem == SupItem.idsupitem),
297
                (EVENTSAGGREGATE_TABLE, EVENTSAGGREGATE_TABLE.c.idevent ==
298
                    Event.idevent),
299
                (CorrEvent, CorrEvent.idcorrevent ==
300
                    EVENTSAGGREGATE_TABLE.c.idcorrevent),
301
            ).filter(CorrEvent.idcorrevent == idcorrevent
302
            ).filter(Event.idevent == CorrEvent.idcause
303
            ).one()
304

    
305
        if isinstance(cause_supitem, LowLevelService):
306
            hostname = cause_supitem.host.name
307
            servicename = cause_supitem.servicename
308
        elif isinstance(cause_supitem, Host):
309
            hostname = cause_supitem.name
310

    
311
        # Vérification que l'événement existe
312
        total_rows = events.num_rows()
313
        if total_rows < 1:
314
            flash(_('No masked event or access denied'), 'error')
315
            redirect('/')
316

    
317
         # Calcul des éléments à afficher et du nombre de pages possibles
318
        total_rows = events.num_rows()
319
        items_per_page = int(config['vigiboard_items_per_page'])
320

    
321
        id_first_row = items_per_page * (page-1)
322
        id_last_row = min(id_first_row + items_per_page, total_rows)
323

    
324
        events.format_events(id_first_row, id_last_row)
325
        events.generate_tmpl_context()
326

    
327
        nb_pages = int(math.ceil(total_rows / (items_per_page + 0.0)))
328
        if not total_rows:
329
            id_first_row = 0
330
        else:
331
            id_first_row += 1
332

    
333
        return dict(
334
            idcorrevent = idcorrevent,
335
            hostname = hostname,
336
            servicename = servicename,
337
            events = events.events,
338
            rows_info = {
339
                'id_first_row': id_first_row,
340
                'id_last_row': id_last_row,
341
                'total_rows': total_rows,
342
            },
343
            nb_pages = nb_pages,
344
            page = page,
345
            search_form = create_search_form,
346
            search = {},
347
            get_calendar_lang = get_calendar_lang,
348
        )
349

    
350

    
351
    class EventSchema(schema.Schema):
352
        """Schéma de validation de la méthode event."""
353
        idevent = validators.Int(not_empty=True)
354
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
355

    
356
    @validate(
357
        validators=EventSchema(),
358
        error_handler = process_form_errors)
359
    @expose('history_table.html')
360
    @require(access_restriction)
361
    def event(self, idevent, page):
362
        """
363
        Affichage de l'historique d'un événement brut.
364
        Pour accéder à cette page, l'utilisateur doit être authentifié.
365

366
        @param idevent: identifiant de l'événement brut souhaité.
367
        @type idevent: C{int}
368
        @param page: numéro de la page à afficher.
369
        @type page: C{int}
370

371
        Cette méthode permet de satisfaire l'exigence
372
        VIGILO_EXIG_VIGILO_BAC_0080.
373
        """
374
        user = get_current_user()
375
        events = VigiboardRequest(user, False)
376
        events.add_table(
377
            Event,
378
            events.items.c.hostname.label('hostname'),
379
            events.items.c.servicename.label('servicename'),
380
        )
381
        events.add_join((EVENTSAGGREGATE_TABLE, \
382
            EVENTSAGGREGATE_TABLE.c.idevent == Event.idevent))
383
        events.add_join((CorrEvent, CorrEvent.idcorrevent == \
384
            EVENTSAGGREGATE_TABLE.c.idcorrevent))
385
        events.add_join((events.items,
386
            Event.idsupitem == events.items.c.idsupitem))
387
        events.add_filter(Event.idevent == idevent)
388

    
389
        if events.num_rows() != 1:
390
            flash(_('No such event or access denied'), 'error')
391
            redirect('/')
392

    
393
        events.format_events(0, 1)
394
        events.generate_tmpl_context()
395
        history = events.format_history()
396

    
397
        total_rows = history.count()
398
        items_per_page = int(config['vigiboard_items_per_page'])
399

    
400
        id_first_row = items_per_page * (page-1)
401
        id_last_row = min(id_first_row + items_per_page, total_rows)
402

    
403
        history_entries = history[id_first_row : id_last_row]
404

    
405
        nb_pages = int(math.ceil(total_rows / (items_per_page + 0.0)))
406
        if not total_rows:
407
            id_first_row = 0
408
        else:
409
            id_first_row += 1
410

    
411
        event = events.req[0]
412

    
413
        return dict(
414
            idevent = idevent,
415
            hostname = event.hostname,
416
            servicename = event.servicename,
417
            rows_info = {
418
                'id_first_row': id_first_row,
419
                'id_last_row': id_last_row,
420
                'total_rows': total_rows,
421
            },
422
            nb_pages = nb_pages,
423
            page = page,
424
            history = history_entries,
425
            search_form = create_search_form,
426
            search = {},
427
            get_calendar_lang = get_calendar_lang,
428
        )
429

    
430

    
431
    class ItemSchema(schema.Schema):
432
        """Schéma de validation de la méthode item."""
433
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
434
        host = validators.String(not_empty=True)
435
        service = validators.String(if_missing=None)
436

    
437
    @validate(
438
        validators=ItemSchema(),
439
        error_handler = process_form_errors)
440
    @expose('events_table.html')
441
    @require(access_restriction)
442
    def item(self, page, host, service):
443
        """
444
        Affichage de l'historique de l'ensemble des événements corrélés
445
        jamais ouverts sur l'hôte / service demandé.
446
        Pour accéder à cette page, l'utilisateur doit être authentifié.
447

448
        @param page: Numéro de la page à afficher.
449
        @param host: Nom de l'hôte souhaité.
450
        @param service: Nom du service souhaité
451

452
        Cette méthode permet de satisfaire l'exigence
453
        VIGILO_EXIG_VIGILO_BAC_0080.
454
        """
455
        idsupitem = SupItem.get_supitem(host, service)
456
        if not idsupitem:
457
            flash(_('No such host/service'), 'error')
458
            redirect('/')
459

    
460
        user = get_current_user()
461
        aggregates = VigiboardRequest(user, False)
462
        aggregates.add_table(
463
            CorrEvent,
464
            aggregates.items.c.hostname,
465
            aggregates.items.c.servicename,
466
        )
467
        aggregates.add_join((Event, CorrEvent.idcause == Event.idevent))
468
        aggregates.add_join((aggregates.items,
469
            Event.idsupitem == aggregates.items.c.idsupitem))
470
        aggregates.add_filter(aggregates.items.c.idsupitem == idsupitem)
471

    
472
        # Vérification qu'il y a au moins 1 événement qui correspond
473
        total_rows = aggregates.num_rows()
474
        if not total_rows:
475
            flash(_('No access to this host/service or no event yet'), 'error')
476
            redirect('/')
477

    
478
        items_per_page = int(config['vigiboard_items_per_page'])
479

    
480
        id_first_row = items_per_page * (page-1)
481
        id_last_row = min(id_first_row + items_per_page, total_rows)
482

    
483
        aggregates.format_events(id_first_row, id_last_row)
484
        aggregates.generate_tmpl_context()
485

    
486
        nb_pages = int(math.ceil(total_rows / (items_per_page + 0.0)))
487
        if not total_rows:
488
            id_first_row = 0
489
        else:
490
            id_first_row += 1
491

    
492
        return dict(
493
            hostname = host,
494
            servicename = service,
495
            events = aggregates.events,
496
            rows_info = {
497
                'id_first_row': id_first_row,
498
                'id_last_row': id_last_row,
499
                'total_rows': total_rows,
500
            },
501
            nb_pages = nb_pages,
502
            page = page,
503
            event_edit_status_options = edit_event_status_options,
504
            search_form = create_search_form,
505
            search = {},
506
            get_calendar_lang = get_calendar_lang,
507
        )
508

    
509

    
510
    class UpdateSchema(schema.Schema):
511
        """Schéma de validation de la méthode update."""
512
        id = validators.Regex(r'^[0-9]+(,[0-9]+)*,?$')
513
        last_modification = validators.Number(not_empty=True)
514
        trouble_ticket = validators.String(if_missing='')
515
        ack = validators.OneOf(
516
            [unicode(s[0]) for s in edit_event_status_options],
517
            not_empty=True)
518

    
519
    @validate(
520
        validators=UpdateSchema(),
521
        error_handler = process_form_errors)
522
    @require(
523
        All(
524
            not_anonymous(msg=l_("You need to be authenticated")),
525
            Any(in_group('managers'),
526
                has_permission('vigiboard-update'),
527
                msg=l_("You don't have write access to VigiBoard"))
528
        ))
529
    @expose()
530
    def update(self, id, last_modification, trouble_ticket, ack):
531
        """
532
        Mise à jour d'un événement suivant les arguments passés.
533
        Cela peut être un changement de ticket ou un changement de statut.
534

535
        @param id: Le ou les identifiants des événements à traiter
536
        @param last_modification: La date de la dernière modification
537
            dont l'utilisateur est au courant.
538
        @param trouble_ticket: Nouveau numéro du ticket associé.
539
        @param ack: Nouvel état d'acquittement des événements sélectionnés.
540

541
        Cette méthode permet de satisfaire les exigences suivantes :
542
            - VIGILO_EXIG_VIGILO_BAC_0020,
543
            - VIGILO_EXIG_VIGILO_BAC_0060,
544
            - VIGILO_EXIG_VIGILO_BAC_0110.
545
        """
546

    
547
        # On vérifie que des identifiants ont bien été transmis via
548
        # le formulaire, et on informe l'utilisateur le cas échéant.
549
        if id is None:
550
            flash(_('No event has been selected'), 'warning')
551
            raise redirect(request.environ.get('HTTP_REFERER', '/'))
552

    
553
        # On récupère la liste de tous les identifiants des événements
554
        # à mettre à jour.
555
        ids = map(int, id.strip(',').split(','))
556

    
557
        user = get_current_user()
558
        events = VigiboardRequest(user)
559
        events.add_table(CorrEvent)
560
        events.add_join((Event, CorrEvent.idcause == Event.idevent))
561
        events.add_join((events.items,
562
            Event.idsupitem == events.items.c.idsupitem))
563
        events.add_filter(CorrEvent.idcorrevent.in_(ids))
564

    
565
        events.generate_request()
566
        idevents = [cause.idcause for cause in events.req]
567

    
568
        # Si des changements sont survenus depuis que la
569
        # page est affichée, on en informe l'utilisateur.
570
        last_modification = datetime.fromtimestamp(last_modification)
571
        cur_last_modification = get_last_modification_timestamp(idevents, None)
572
        if cur_last_modification and last_modification < cur_last_modification:
573
            flash(_('Changes have occurred since the page was last displayed, '
574
                    'your changes HAVE NOT been saved.'), 'warning')
575
            raise redirect(request.environ.get('HTTP_REFERER', '/'))
576

    
577
        # Vérification que au moins un des identifiants existe et est éditable
578
        if not events.num_rows():
579
            flash(_('No access to this event'), 'error')
580
            redirect('/')
581

    
582
        if ack == u'Forced':
583
            condition = Any(
584
                in_group('managers'),
585
                has_permission('vigiboard-admin'),
586
                msg=l_("You don't have administrative access "
587
                        "to VigiBoard"))
588
            try:
589
                condition.check_authorization(request.environ)
590
            except NotAuthorizedError, e:
591
                reason = unicode(e)
592
                flash(reason, 'error')
593
                raise redirect(request.environ.get('HTTP_REFERER', '/'))
594

    
595
        # Modification des événements et création d'un historique
596
        # chaque fois que cela est nécessaire.
597
        for event in events.req:
598
            if trouble_ticket and trouble_ticket != event.trouble_ticket:
599
                history = EventHistory(
600
                        type_action="Ticket change",
601
                        idevent=event.idcause,
602
                        value=unicode(trouble_ticket),
603
                        text="Changed trouble ticket from '%(from)s' "
604
                             "to '%(to)s'" % {
605
                            'from': event.trouble_ticket,
606
                            'to': trouble_ticket,
607
                        },
608
                        username=user.user_name,
609
                        timestamp=datetime.now(),
610
                    )
611
                DBSession.add(history)
612
                event.trouble_ticket = trouble_ticket
613

    
614
            # Changement du statut d'acquittement.
615
            if ack != u'NoChange':
616
                changed_ack = ack
617
                # Pour forcer l'acquittement d'un événement,
618
                # il faut en plus avoir la permission
619
                # "vigiboard-admin".
620
                if ack == u'Forced':
621
                    changed_ack = u'AAClosed'
622
                    cause = event.cause
623
                    # On met systématiquement l'événement à l'état "OK",
624
                    # même s'il s'agit d'un hôte.
625
                    # Techniquement, c'est incorrect, mais on fait ça
626
                    # pour masquer l'événement de toutes façons...
627
                    cause.current_state = \
628
                        StateName.statename_to_value(u'OK')
629

    
630
                    # Mise à jour de l'état dans State, pour que
631
                    # VigiMap soit également mis à jour.
632
                    DBSession.query(State).filter(
633
                            State.idsupitem == cause.idsupitem,
634
                        ).update({
635
                            'state': StateName.statename_to_value(u'OK'),
636
                        })
637

    
638
                    history = EventHistory(
639
                            type_action="Forced change state",
640
                            idevent=event.idcause,
641
                            value=u'OK',
642
                            text="Forced state to 'OK'",
643
                            username=user.user_name,
644
                            timestamp=datetime.now(),
645
                        )
646
                    DBSession.add(history)
647

    
648
                history = EventHistory(
649
                        type_action=u"Acknowledgement change state",
650
                        idevent=event.idcause,
651
                        value=ack,
652
                        text="Changed acknowledgement status "
653
                            "from '%s' to '%s'" % (
654
                            event.status, changed_ack
655
                        ),
656
                        username=user.user_name,
657
                        timestamp=datetime.now(),
658
                    )
659
                DBSession.add(history)
660
                event.status = changed_ack
661

    
662
        DBSession.flush()
663
        flash(_('Updated successfully'))
664
        redirect(request.environ.get('HTTP_REFERER', '/'))
665

    
666

    
667
    class GetPluginValueSchema(schema.Schema):
668
        """Schéma de validation de la méthode get_plugin_value."""
669
        idcorrevent = validators.Int(not_empty=True)
670
        plugin_name = validators.String(not_empty=True)
671
        # Permet de passer des paramètres supplémentaires au plugin.
672
        allow_extra_fields = True
673

    
674
    @validate(
675
        validators=GetPluginValueSchema(),
676
        error_handler = handle_validation_errors_json)
677
    @expose('json')
678
    @require(access_restriction)
679
    def get_plugin_value(self, idcorrevent, plugin_name, *arg, **krgv):
680
        """
681
        Permet de récupérer la valeur d'un plugin associée à un CorrEvent
682
        donné via JSON.
683
        """
684
        plugins = dict(config['columns_plugins'])
685
        if plugin_name not in plugins:
686
            raise HTTPNotFound(_("No such plugin '%s'") % plugin_name)
687

    
688
        # Permet de vérifier si l'utilisateur a bien les permissions
689
        # pour accéder à cet événement et si l'événement existe.
690
        user = get_current_user()
691
        events = VigiboardRequest(user, False)
692
        events.add_table(CorrEvent.idcorrevent)
693
        events.add_join((Event, CorrEvent.idcause == Event.idevent))
694
        events.add_join((events.items,
695
            Event.idsupitem == events.items.c.idsupitem))
696
        events.add_filter(CorrEvent.idcorrevent == idcorrevent)
697

    
698
        # Pas d'événement ou permission refusée. On ne distingue pas
699
        # les 2 cas afin d'éviter la divulgation d'informations.
700
        if not events.num_rows():
701
            raise HTTPNotFound(_('No such incident or insufficient '
702
                                'permissions'))
703

    
704
        return plugins[plugin_name].get_value(idcorrevent, *arg, **krgv)
705

    
706
    @validate(validators={
707
        "fontsize": validators.Regex(
708
            r'[0-9]+(pt|px|em|%)',
709
            regexOps = ('I',)
710
        )}, error_handler = handle_validation_errors_json)
711
    @expose('json')
712
    def set_fontsize(self, fontsize):
713
        """Enregistre la taille de la police dans les préférences."""
714
        session['fontsize'] = fontsize
715
        session.save()
716
        return dict()
717

    
718
    @validate(validators={"refresh": validators.Int()},
719
            error_handler = handle_validation_errors_json)
720
    @expose('json')
721
    def set_refresh(self, refresh):
722
        """Enregistre le temps de rafraichissement dans les préférences."""
723
        session['refresh'] = refresh
724
        session.save()
725
        return dict()
726

    
727
    @expose('json')
728
    def set_theme(self, theme):
729
        """Enregistre le thème à utiliser dans les préférences."""
730
        # On sauvegarde l'ID du thème sans vérifications
731
        # car les thèmes (styles CSS) sont définies dans
732
        # les packages de thèmes (ex: vigilo-themes-default).
733
        # La vérification de la valeur est faite dans les templates.
734
        session['theme'] = theme
735
        session.save()
736
        return dict()
737

    
738
    @require(access_restriction)
739
    @expose('json')
740
    def get_groups(self, parent_id=None):
741
        """
742
        Affiche un étage de l'arbre de
743
        sélection des hôtes et groupes d'hôtes.
744

745
        @param parent_id: identifiant du groupe d'hôte parent
746
        @type  parent_id: C{int} or None
747
        """
748

    
749
        # Si l'identifiant du groupe parent n'est pas
750
        # spécifié, on retourne la liste des groupes racines,
751
        # fournie par la méthode get_root_hosts_groups.
752
        if parent_id is None:
753
            return self.get_root_host_groups()
754

    
755
        parent = DBSession.query(SupItemGroup).get(parent_id)
756

    
757
        # On vérifie si le groupe parent fait partie des
758
        # groupes auxquel l'utilisateur a accès, et on
759
        # retourne une liste vide dans le cas contraire
760
        is_manager = in_group('managers').is_met(request.environ)
761

    
762
        # On récupère la liste des groupes dont
763
        # l'identifiant du parent est passé en paramètre
764
        db_groups = DBSession.query(
765
            SupItemGroup
766
        ).filter(SupItemGroup.depth == parent.depth + 1
767
        ).filter(SupItemGroup.left > parent.left
768
        ).filter(SupItemGroup.right < parent.right
769
        ).order_by(SupItemGroup.name.asc())
770

    
771
        if not is_manager:
772
            user = get_current_user()
773
            user_groups = [g[0] for g in user.supitemgroups() if g[1]]
774
            db_groups = db_groups.filter(SupItemGroup.idgroup.in_(user_groups))
775

    
776
        groups = []
777
        for group in db_groups.all():
778
            groups.append({
779
                'id'   : group.idgroup,
780
                'name' : group.name,
781
            })
782

    
783
        return dict(groups = groups, leaves = [])
784

    
785
    def get_root_host_groups(self):
786
        """
787
        Retourne tous les groupes racines (c'est à dire n'ayant
788
        aucun parent) d'hôtes auquel l'utilisateur a accès.
789

790
        @return: Un dictionnaire contenant la liste de ces groupes.
791
        @rtype : C{dict} of C{list} of C{dict} of C{mixed}
792
        """
793

    
794
        root_groups = SupItemGroup.get_top_groups()
795

    
796
        # On filtre ces groupes racines afin de ne
797
        # retourner que ceux auquels l'utilisateur a accès
798
        user = get_current_user()
799
        is_manager = in_group('managers').is_met(request.environ)
800
        if not is_manager:
801
            user_groups = [sig[0] for sig in user.supitemgroups()]
802
            for root_group in root_groups:
803
                if root_group.idgroup not in user_groups:
804
                    root_groups.remove(root_group)
805

    
806
        groups = []
807
        for group in root_groups:
808
            groups.append({
809
                'id'   : group.idgroup,
810
                'name' : group.name,
811
            })
812

    
813
        return dict(groups = groups, leaves=[])
814

    
815
def get_last_modification_timestamp(event_id_list,
816
                                    value_if_none=datetime.now()):
817
    """
818
    Récupère le timestamp de la dernière modification
819
    opérée sur l'un des événements dont l'identifiant
820
    fait partie de la liste passée en paramètre.
821
    """
822
    last_modification_timestamp = DBSession.query(
823
                                func.max(EventHistory.timestamp),
824
                         ).filter(EventHistory.idevent.in_(event_id_list)
825
                         ).scalar()
826
    if not last_modification_timestamp:
827
        if not value_if_none:
828
            return None
829
        else:
830
            last_modification_timestamp = value_if_none
831
    return datetime.fromtimestamp(mktime(
832
        last_modification_timestamp.timetuple()))