Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

vigiboard / vigiboard / controllers / root.py @ cf3c2494

History | View | Annotate | Download (34 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 sqlalchemy.orm import contains_eager
36
from repoze.what.predicates import Any, All, in_group, \
37
                                    has_permission, not_anonymous, \
38
                                    NotAuthorizedError
39
from formencode import schema
40
from pkg_resources import working_set
41

    
42
from vigilo.models.session import DBSession
43
from vigilo.models.tables import Event, EventHistory, CorrEvent, Host, \
44
                                    SupItem, SupItemGroup, LowLevelService, \
45
                                    StateName, State, DataPermission
46
from vigilo.models.tables.grouphierarchy import GroupHierarchy
47
from vigilo.models.functions import sql_escape_like
48
from vigilo.models.tables.secondary_tables import EVENTSAGGREGATE_TABLE, \
49
        USER_GROUP_TABLE
50

    
51
from vigilo.turbogears.controllers.autocomplete import AutoCompleteController
52
from vigilo.turbogears.controllers.proxy import ProxyController
53
from vigilo.turbogears.controllers.api.root import ApiRootController
54
from vigilo.turbogears.helpers import get_current_user
55

    
56
from vigiboard.controllers.vigiboardrequest import VigiboardRequest
57
from vigiboard.controllers.vigiboard_controller import VigiboardRootController
58

    
59
from vigiboard.widgets.edit_event import edit_event_status_options
60
from vigiboard.widgets.search_form import create_search_form, get_calendar_lang
61

    
62
__all__ = ('RootController', 'get_last_modification_timestamp',
63
           'date_to_timestamp')
64

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

    
75

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

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

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

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

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

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

132
        Cette méthode permet de satisfaire les exigences suivantes :
133
            - VIGILO_EXIG_VIGILO_BAC_0040,
134
            - VIGILO_EXIG_VIGILO_BAC_0070,
135
            - VIGILO_EXIG_VIGILO_BAC_0100,
136
        """
137
        user = get_current_user()
138
        aggregates = VigiboardRequest(user)
139

    
140
        aggregates.add_table(
141
            CorrEvent,
142
            aggregates.items.c.hostname,
143
            aggregates.items.c.servicename
144
        )
145
        aggregates.add_join((Event, CorrEvent.idcause == Event.idevent))
146
        aggregates.add_contains_eager(CorrEvent.cause)
147
        aggregates.add_group_by(Event)
148
        aggregates.add_join((aggregates.items,
149
            Event.idsupitem == aggregates.items.c.idsupitem))
150
        aggregates.add_order_by(asc(aggregates.items.c.hostname))
151

    
152
        search = {}
153

    
154
        # Application des filtres si nécessaire
155
        if supitemgroup:
156
            search['supitemgroup'] = supitemgroup
157
            aggregates.add_join((GroupHierarchy, GroupHierarchy.idchild ==
158
                aggregates.items.c.idsupitemgroup))
159
            aggregates.add_filter(GroupHierarchy.idparent == supitemgroup)
160

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

    
167
        if service:
168
            search['service'] = service
169
            service = sql_escape_like(service)
170
            aggregates.add_filter(aggregates.items.c.servicename.ilike(
171
                '%s' % service))
172

    
173
        if output:
174
            search['output'] = output
175
            output = sql_escape_like(output)
176
            aggregates.add_filter(Event.message.ilike('%s' % output))
177

    
178
        if trouble_ticket:
179
            search['tt'] = trouble_ticket
180
            trouble_ticket = sql_escape_like(trouble_ticket)
181
            aggregates.add_filter(CorrEvent.trouble_ticket.ilike(
182
                '%s' % trouble_ticket))
183

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

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

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

    
218
        id_first_row = items_per_page * (page-1)
219
        id_last_row = min(id_first_row + items_per_page, total_rows)
220

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

    
226
        aggregates.format_events(id_first_row, id_last_row)
227
        aggregates.generate_tmpl_context()
228

    
229
        nb_pages = int(math.ceil(total_rows / (items_per_page + 0.0)))
230
        if not total_rows:
231
            id_first_row = 0
232
        else:
233
            id_first_row += 1
234

    
235
        # Récupération des données des plugins
236
        plugins_data = {}
237
        plugins = dict(config['columns_plugins'])
238
        for plugin in plugins:
239
            plugin_data = plugins[plugin].get_bulk_data(
240
                [event[0].idcorrevent for event in aggregates.events]
241
            )
242
            if plugin_data :
243
                plugins_data[plugin] = plugin_data
244

    
245
        return dict(
246
            hostname = None,
247
            servicename = None,
248
            events = aggregates.events,
249
            plugins_data = plugins_data,
250
            rows_info = {
251
                'id_first_row': id_first_row,
252
                'id_last_row': id_last_row,
253
                'total_rows': total_rows,
254
            },
255
            nb_pages = nb_pages,
256
            page = page,
257
            event_edit_status_options = edit_event_status_options,
258
            search_form = create_search_form,
259
            search = search,
260
            get_calendar_lang = get_calendar_lang,
261
        )
262

    
263

    
264
    class MaskedEventsSchema(schema.Schema):
265
        """Schéma de validation de la méthode masked_events."""
266
        idcorrevent = validators.Int(not_empty=True)
267
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
268

    
269
    @validate(
270
        validators=MaskedEventsSchema(),
271
        error_handler = process_form_errors)
272
    @expose('raw_events_table.html')
273
    @require(access_restriction)
274
    def masked_events(self, idcorrevent, page):
275
        """
276
        Affichage de la liste des événements bruts masqués d'un événement
277
        corrélé (événements agrégés dans l'événement corrélé).
278

279
        @param idcorrevent: identifiant de l'événement corrélé souhaité.
280
        @type idcorrevent: C{int}
281
        """
282
        user = get_current_user()
283

    
284
        # Récupère la liste des événements masqués de l'événement
285
        # corrélé donné par idcorrevent.
286
        events = VigiboardRequest(user, False)
287
        events.add_table(
288
            Event,
289
            events.items.c.hostname,
290
            events.items.c.servicename,
291
        )
292
        events.add_join((EVENTSAGGREGATE_TABLE, \
293
            EVENTSAGGREGATE_TABLE.c.idevent == Event.idevent))
294
        events.add_join((CorrEvent, CorrEvent.idcorrevent == \
295
            EVENTSAGGREGATE_TABLE.c.idcorrevent))
296
        events.add_join((events.items,
297
            Event.idsupitem == events.items.c.idsupitem))
298
        events.add_filter(Event.idevent != CorrEvent.idcause)
299
        events.add_filter(CorrEvent.idcorrevent == idcorrevent)
300

    
301
        # Récupère l'instance de SupItem associé à la cause de
302
        # l'événement corrélé. Cette instance est utilisé pour
303
        # obtenir le nom d'hôte/service auquel la cause est
304
        # rattachée (afin de fournir un contexte à l'utilisateur).
305
        hostname = None
306
        servicename = None
307
        cause_supitem = DBSession.query(
308
                SupItem,
309
            ).join(
310
                (Event, Event.idsupitem == SupItem.idsupitem),
311
                (CorrEvent, Event.idevent == CorrEvent.idcause),
312
            ).filter(CorrEvent.idcorrevent == idcorrevent
313
            ).one()
314

    
315
        if isinstance(cause_supitem, LowLevelService):
316
            hostname = cause_supitem.host.name
317
            servicename = cause_supitem.servicename
318
        elif isinstance(cause_supitem, Host):
319
            hostname = cause_supitem.name
320

    
321
        # Vérification que l'événement existe
322
        total_rows = events.num_rows()
323
        if total_rows < 1:
324
            flash(_('No masked event or access denied'), 'error')
325
            redirect('/')
326

    
327
         # Calcul des éléments à afficher et du nombre de pages possibles
328
        total_rows = events.num_rows()
329
        items_per_page = int(config['vigiboard_items_per_page'])
330

    
331
        id_first_row = items_per_page * (page-1)
332
        id_last_row = min(id_first_row + items_per_page, total_rows)
333

    
334
        events.format_events(id_first_row, id_last_row)
335
        events.generate_tmpl_context()
336

    
337
        nb_pages = int(math.ceil(total_rows / (items_per_page + 0.0)))
338
        if not total_rows:
339
            id_first_row = 0
340
        else:
341
            id_first_row += 1
342

    
343
        return dict(
344
            idcorrevent = idcorrevent,
345
            hostname = hostname,
346
            servicename = servicename,
347
            events = events.events,
348
            plugins_data = {},
349
            rows_info = {
350
                'id_first_row': id_first_row,
351
                'id_last_row': id_last_row,
352
                'total_rows': total_rows,
353
            },
354
            nb_pages = nb_pages,
355
            page = page,
356
            search_form = create_search_form,
357
            search = {},
358
            get_calendar_lang = get_calendar_lang,
359
        )
360

    
361

    
362
    class EventSchema(schema.Schema):
363
        """Schéma de validation de la méthode event."""
364
        idevent = validators.Int(not_empty=True)
365
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
366

    
367
    @validate(
368
        validators=EventSchema(),
369
        error_handler = process_form_errors)
370
    @expose('history_table.html')
371
    @require(access_restriction)
372
    def event(self, idevent, page):
373
        """
374
        Affichage de l'historique d'un événement brut.
375
        Pour accéder à cette page, l'utilisateur doit être authentifié.
376

377
        @param idevent: identifiant de l'événement brut souhaité.
378
        @type idevent: C{int}
379
        @param page: numéro de la page à afficher.
380
        @type page: C{int}
381

382
        Cette méthode permet de satisfaire l'exigence
383
        VIGILO_EXIG_VIGILO_BAC_0080.
384
        """
385
        user = get_current_user()
386
        events = VigiboardRequest(user, False)
387
        events.add_table(
388
            Event,
389
            events.items.c.hostname.label('hostname'),
390
            events.items.c.servicename.label('servicename'),
391
        )
392
        events.add_join((EVENTSAGGREGATE_TABLE, \
393
            EVENTSAGGREGATE_TABLE.c.idevent == Event.idevent))
394
        events.add_join((CorrEvent, CorrEvent.idcorrevent == \
395
            EVENTSAGGREGATE_TABLE.c.idcorrevent))
396
        events.add_join((events.items,
397
            Event.idsupitem == events.items.c.idsupitem))
398
        events.add_filter(Event.idevent == idevent)
399

    
400
        if events.num_rows() != 1:
401
            flash(_('No such event or access denied'), 'error')
402
            redirect('/')
403

    
404
        events.format_events(0, 1)
405
        events.generate_tmpl_context()
406
        history = events.format_history()
407

    
408
        total_rows = history.count()
409
        items_per_page = int(config['vigiboard_items_per_page'])
410

    
411
        id_first_row = items_per_page * (page-1)
412
        id_last_row = min(id_first_row + items_per_page, total_rows)
413

    
414
        history_entries = history[id_first_row : id_last_row]
415

    
416
        nb_pages = int(math.ceil(total_rows / (items_per_page + 0.0)))
417
        if not total_rows:
418
            id_first_row = 0
419
        else:
420
            id_first_row += 1
421

    
422
        event = events.req[0]
423

    
424
        return dict(
425
            idevent = idevent,
426
            hostname = event.hostname,
427
            servicename = event.servicename,
428
            plugins_data = {},
429
            rows_info = {
430
                'id_first_row': id_first_row,
431
                'id_last_row': id_last_row,
432
                'total_rows': total_rows,
433
            },
434
            nb_pages = nb_pages,
435
            page = page,
436
            history = history_entries,
437
            search_form = create_search_form,
438
            search = {},
439
            get_calendar_lang = get_calendar_lang,
440
        )
441

    
442

    
443
    class ItemSchema(schema.Schema):
444
        """Schéma de validation de la méthode item."""
445
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
446
        host = validators.String(not_empty=True)
447
        service = validators.String(if_missing=None)
448

    
449
    @validate(
450
        validators=ItemSchema(),
451
        error_handler = process_form_errors)
452
    @expose('events_table.html')
453
    @require(access_restriction)
454
    def item(self, page, host, service):
455
        """
456
        Affichage de l'historique de l'ensemble des événements corrélés
457
        jamais ouverts sur l'hôte / service demandé.
458
        Pour accéder à cette page, l'utilisateur doit être authentifié.
459

460
        @param page: Numéro de la page à afficher.
461
        @param host: Nom de l'hôte souhaité.
462
        @param service: Nom du service souhaité
463

464
        Cette méthode permet de satisfaire l'exigence
465
        VIGILO_EXIG_VIGILO_BAC_0080.
466
        """
467
        idsupitem = SupItem.get_supitem(host, service)
468
        if not idsupitem:
469
            flash(_('No such host/service'), 'error')
470
            redirect('/')
471

    
472
        user = get_current_user()
473
        aggregates = VigiboardRequest(user, False)
474
        aggregates.add_table(
475
            CorrEvent,
476
            aggregates.items.c.hostname,
477
            aggregates.items.c.servicename,
478
        )
479
        aggregates.add_join((Event, CorrEvent.idcause == Event.idevent))
480
        aggregates.add_join((aggregates.items,
481
            Event.idsupitem == aggregates.items.c.idsupitem))
482
        aggregates.add_filter(aggregates.items.c.idsupitem == idsupitem)
483

    
484
        # Vérification qu'il y a au moins 1 événement qui correspond
485
        total_rows = aggregates.num_rows()
486
        if not total_rows:
487
            flash(_('No access to this host/service or no event yet'), 'error')
488
            redirect('/')
489

    
490
        items_per_page = int(config['vigiboard_items_per_page'])
491

    
492
        id_first_row = items_per_page * (page-1)
493
        id_last_row = min(id_first_row + items_per_page, total_rows)
494

    
495
        aggregates.format_events(id_first_row, id_last_row)
496
        aggregates.generate_tmpl_context()
497

    
498
        nb_pages = int(math.ceil(total_rows / (items_per_page + 0.0)))
499
        if not total_rows:
500
            id_first_row = 0
501
        else:
502
            id_first_row += 1
503

    
504
        return dict(
505
            hostname = host,
506
            servicename = service,
507
            events = aggregates.events,
508
            plugins_data = {},
509
            rows_info = {
510
                'id_first_row': id_first_row,
511
                'id_last_row': id_last_row,
512
                'total_rows': total_rows,
513
            },
514
            nb_pages = nb_pages,
515
            page = page,
516
            event_edit_status_options = edit_event_status_options,
517
            search_form = create_search_form,
518
            search = {},
519
            get_calendar_lang = get_calendar_lang,
520
        )
521

    
522

    
523
    class UpdateSchema(schema.Schema):
524
        """Schéma de validation de la méthode update."""
525
        id = validators.Regex(r'^[0-9]+(,[0-9]+)*,?$')
526
        last_modification = validators.Number(not_empty=True)
527
        trouble_ticket = validators.String(if_missing='')
528
        ack = validators.OneOf(
529
            [unicode(s[0]) for s in edit_event_status_options],
530
            not_empty=True)
531

    
532
    @validate(
533
        validators=UpdateSchema(),
534
        error_handler = process_form_errors)
535
    @require(
536
        All(
537
            not_anonymous(msg=l_("You need to be authenticated")),
538
            Any(in_group('managers'),
539
                has_permission('vigiboard-update'),
540
                msg=l_("You don't have write access to VigiBoard"))
541
        ))
542
    @expose()
543
    def update(self, id, last_modification, trouble_ticket, ack):
544
        """
545
        Mise à jour d'un événement suivant les arguments passés.
546
        Cela peut être un changement de ticket ou un changement de statut.
547

548
        @param id: Le ou les identifiants des événements à traiter
549
        @param last_modification: La date de la dernière modification
550
            dont l'utilisateur est au courant.
551
        @param trouble_ticket: Nouveau numéro du ticket associé.
552
        @param ack: Nouvel état d'acquittement des événements sélectionnés.
553

554
        Cette méthode permet de satisfaire les exigences suivantes :
555
            - VIGILO_EXIG_VIGILO_BAC_0020,
556
            - VIGILO_EXIG_VIGILO_BAC_0060,
557
            - VIGILO_EXIG_VIGILO_BAC_0110.
558
        """
559

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

    
566
        # On récupère la liste de tous les identifiants des événements
567
        # à mettre à jour.
568
        ids = map(int, id.strip(',').split(','))
569

    
570
        user = get_current_user()
571
        events = VigiboardRequest(user)
572
        events.add_table(CorrEvent)
573
        events.add_join((Event, CorrEvent.idcause == Event.idevent))
574
        events.add_join((events.items,
575
            Event.idsupitem == events.items.c.idsupitem))
576
        events.add_filter(CorrEvent.idcorrevent.in_(ids))
577

    
578
        events.generate_request()
579
        idevents = [cause.idcause for cause in events.req]
580

    
581
        # Si des changements sont survenus depuis que la
582
        # page est affichée, on en informe l'utilisateur.
583
        last_modification = datetime.fromtimestamp(last_modification)
584
        cur_last_modification = get_last_modification_timestamp(idevents, None)
585
        if cur_last_modification and last_modification < cur_last_modification:
586
            flash(_('Changes have occurred since the page was last displayed, '
587
                    'your changes HAVE NOT been saved.'), 'warning')
588
            raise redirect(request.environ.get('HTTP_REFERER', '/'))
589

    
590
        # Vérification que au moins un des identifiants existe et est éditable
591
        if not events.num_rows():
592
            flash(_('No access to this event'), 'error')
593
            redirect('/')
594

    
595
        if ack == u'Forced':
596
            condition = Any(
597
                in_group('managers'),
598
                has_permission('vigiboard-admin'),
599
                msg=l_("You don't have administrative access "
600
                        "to VigiBoard"))
601
            try:
602
                condition.check_authorization(request.environ)
603
            except NotAuthorizedError, e:
604
                reason = unicode(e)
605
                flash(reason, 'error')
606
                raise redirect(request.environ.get('HTTP_REFERER', '/'))
607

    
608
        # Modification des événements et création d'un historique
609
        # chaque fois que cela est nécessaire.
610
        for event in events.req:
611
            if trouble_ticket and trouble_ticket != event.trouble_ticket:
612
                history = EventHistory(
613
                        type_action="Ticket change",
614
                        idevent=event.idcause,
615
                        value=unicode(trouble_ticket),
616
                        text="Changed trouble ticket from '%(from)s' "
617
                             "to '%(to)s'" % {
618
                            'from': event.trouble_ticket,
619
                            'to': trouble_ticket,
620
                        },
621
                        username=user.user_name,
622
                        timestamp=datetime.now(),
623
                    )
624
                DBSession.add(history)
625
                event.trouble_ticket = trouble_ticket
626

    
627
            # Changement du statut d'acquittement.
628
            if ack != u'NoChange':
629
                changed_ack = ack
630
                # Pour forcer l'acquittement d'un événement,
631
                # il faut en plus avoir la permission
632
                # "vigiboard-admin".
633
                if ack == u'Forced':
634
                    changed_ack = u'AAClosed'
635
                    cause = event.cause
636
                    # On met systématiquement l'événement à l'état "OK",
637
                    # même s'il s'agit d'un hôte.
638
                    # Techniquement, c'est incorrect, mais on fait ça
639
                    # pour masquer l'événement de toutes façons...
640
                    cause.current_state = \
641
                        StateName.statename_to_value(u'OK')
642

    
643
                    # Mise à jour de l'état dans State, pour que
644
                    # VigiMap soit également mis à jour.
645
                    DBSession.query(State).filter(
646
                            State.idsupitem == cause.idsupitem,
647
                        ).update({
648
                            'state': StateName.statename_to_value(u'OK'),
649
                        })
650

    
651
                    history = EventHistory(
652
                            type_action="Forced change state",
653
                            idevent=event.idcause,
654
                            value=u'OK',
655
                            text="Forced state to 'OK'",
656
                            username=user.user_name,
657
                            timestamp=datetime.now(),
658
                        )
659
                    DBSession.add(history)
660

    
661
                history = EventHistory(
662
                        type_action=u"Acknowledgement change state",
663
                        idevent=event.idcause,
664
                        value=ack,
665
                        text="Changed acknowledgement status "
666
                            "from '%s' to '%s'" % (
667
                            event.status, changed_ack
668
                        ),
669
                        username=user.user_name,
670
                        timestamp=datetime.now(),
671
                    )
672
                DBSession.add(history)
673
                event.status = changed_ack
674

    
675
        DBSession.flush()
676
        flash(_('Updated successfully'))
677
        redirect(request.environ.get('HTTP_REFERER', '/'))
678

    
679

    
680
    class GetPluginValueSchema(schema.Schema):
681
        """Schéma de validation de la méthode get_plugin_value."""
682
        idcorrevent = validators.Int(not_empty=True)
683
        plugin_name = validators.String(not_empty=True)
684
        # Permet de passer des paramètres supplémentaires au plugin.
685
        allow_extra_fields = True
686

    
687
    @validate(
688
        validators=GetPluginValueSchema(),
689
        error_handler = handle_validation_errors_json)
690
    @expose('json')
691
    @require(access_restriction)
692
    def plugin_json(self, idcorrevent, plugin_name, *arg, **krgv):
693
        """
694
        Permet de récupérer la valeur d'un plugin associée à un CorrEvent
695
        donné via JSON.
696
        """
697
        plugins = dict(config['columns_plugins'])
698
        if plugin_name not in plugins:
699
            raise HTTPNotFound(_("No such plugin '%s'") % plugin_name)
700

    
701
        # Permet de vérifier si l'utilisateur a bien les permissions
702
        # pour accéder à cet événement et si l'événement existe.
703
        user = get_current_user()
704
        events = VigiboardRequest(user, False)
705
        events.add_table(CorrEvent.idcorrevent)
706
        events.add_join((Event, CorrEvent.idcause == Event.idevent))
707
        events.add_join((events.items,
708
            Event.idsupitem == events.items.c.idsupitem))
709
        events.add_filter(CorrEvent.idcorrevent == idcorrevent)
710

    
711
        # Pas d'événement ou permission refusée. On ne distingue pas
712
        # les 2 cas afin d'éviter la divulgation d'informations.
713
        if not events.num_rows():
714
            raise HTTPNotFound(_('No such incident or insufficient '
715
                                'permissions'))
716

    
717
        return plugins[plugin_name].get_json_data(idcorrevent, *arg, **krgv)
718

    
719
    @validate(validators={
720
        "fontsize": validators.Regex(
721
            r'[0-9]+(pt|px|em|%)',
722
            regexOps = ('I',)
723
        )}, error_handler = handle_validation_errors_json)
724
    @expose('json')
725
    def set_fontsize(self, fontsize):
726
        """Enregistre la taille de la police dans les préférences."""
727
        session['fontsize'] = fontsize
728
        session.save()
729
        return dict()
730

    
731
    @validate(validators={"refresh": validators.Int()},
732
            error_handler = handle_validation_errors_json)
733
    @expose('json')
734
    def set_refresh(self, refresh):
735
        """Enregistre le temps de rafraichissement dans les préférences."""
736
        session['refresh'] = refresh
737
        session.save()
738
        return dict()
739

    
740
    @expose('json')
741
    def set_theme(self, theme):
742
        """Enregistre le thème à utiliser dans les préférences."""
743
        # On sauvegarde l'ID du thème sans vérifications
744
        # car les thèmes (styles CSS) sont définies dans
745
        # les packages de thèmes (ex: vigilo-themes-default).
746
        # La vérification de la valeur est faite dans les templates.
747
        session['theme'] = theme
748
        session.save()
749
        return dict()
750

    
751
    @require(access_restriction)
752
    @expose('json')
753
    def get_groups(self, parent_id=None):
754
        """
755
        Affiche un étage de l'arbre de
756
        sélection des hôtes et groupes d'hôtes.
757

758
        @param parent_id: identifiant du groupe d'hôte parent
759
        @type  parent_id: C{int} or None
760
        """
761

    
762
        # Si l'identifiant du groupe parent n'est pas
763
        # spécifié, on retourne la liste des groupes racines,
764
        # fournie par la méthode get_root_hosts_groups.
765
        if parent_id is None:
766
            return self.get_root_host_groups()
767

    
768
        # TODO: Utiliser un schéma de validation
769
        parent_id = int(parent_id)
770

    
771
        # On récupère la liste des groupes de supitems dont
772
        # l'identifiant du parent est passé en paramètre.
773
        supitem_groups = DBSession.query(
774
                SupItemGroup.idgroup,
775
                SupItemGroup.name,
776
            ).join(
777
                (GroupHierarchy,
778
                    GroupHierarchy.idchild == SupItemGroup.idgroup),
779
            ).filter(GroupHierarchy.idparent == parent_id
780
            ).filter(GroupHierarchy.idchild != parent_id)
781

    
782
        # Si l'utilisateur n'appartient pas au groupe 'managers',
783
        # on filtre les résultats en fonction de ses permissions.
784
        is_manager = in_group('managers').is_met(request.environ)
785
        if not is_manager:
786
            user = get_current_user()
787
            GroupHierarchy_aliased = aliased(GroupHierarchy,
788
                name='GroupHierarchy_aliased')
789
            supitem_groups.join(
790
                (GroupHierarchy_aliased,
791
                    GroupHierarchy_aliased.idchild == SupItemGroup.idgroup),
792
                (DataPermission,
793
                    DataPermission.idgroup == GroupHierarchy_aliased.idparent),
794
                (USER_GROUP_TABLE, USER_GROUP_TABLE.c.idgroup == \
795
                    DataPermission.idusergroup),
796
            ).filter(USER_GROUP_TABLE.c.username == user.user_name)
797

    
798
        groups = []
799
        for group in supitem_groups.distinct().all():
800
            groups.append({
801
                'id'   : group.idgroup,
802
                'name' : group.name,
803
            })
804

    
805
        return dict(groups = groups, leaves = [])
806

    
807
    def get_root_host_groups(self):
808
        """
809
        Retourne tous les groupes racines (c'est à dire n'ayant
810
        aucun parent) d'hôtes auquel l'utilisateur a accès.
811

812
        @return: Un dictionnaire contenant la liste de ces groupes.
813
        @rtype : C{dict} of C{list} of C{dict} of C{mixed}
814
        """
815

    
816
        # On récupère tous les groupes qui ont un parent.
817
        children = DBSession.query(
818
            SupItemGroup,
819
        ).distinct(
820
        ).join(
821
            (GroupHierarchy, GroupHierarchy.idchild == SupItemGroup.idgroup)
822
        ).filter(GroupHierarchy.hops > 0)
823

    
824
        # Ensuite on les exclut de la liste des groupes,
825
        # pour ne garder que ceux qui sont au sommet de
826
        # l'arbre et qui constituent nos "root groups".
827
        root_groups = DBSession.query(
828
            SupItemGroup,
829
        ).except_(children
830
        ).order_by(SupItemGroup.name)
831

    
832
        # On filtre ces groupes racines afin de ne
833
        # retourner que ceux auquels l'utilisateur a accès
834
        user = get_current_user()
835
        is_manager = in_group('managers').is_met(request.environ)
836
        if not is_manager:
837

    
838
            root_groups = root_groups.join(
839
                (GroupHierarchy,
840
                    GroupHierarchy.idchild == SupItemGroup.idgroup),
841
                (DataPermission,
842
                    DataPermission.idgroup == GroupHierarchy.idparent),
843
                (USER_GROUP_TABLE, USER_GROUP_TABLE.c.idgroup == \
844
                    DataPermission.idusergroup),
845
            ).filter(USER_GROUP_TABLE.c.username == user.user_name)
846

    
847
        groups = []
848
        for group in root_groups.all():
849
            groups.append({
850
                'id'   : group.idgroup,
851
                'name' : group.name,
852
            })
853

    
854
        return dict(groups = groups, leaves=[])
855

    
856
def get_last_modification_timestamp(event_id_list,
857
                                    value_if_none=datetime.now()):
858
    """
859
    Récupère le timestamp de la dernière modification
860
    opérée sur l'un des événements dont l'identifiant
861
    fait partie de la liste passée en paramètre.
862
    """
863
    last_modification_timestamp = DBSession.query(
864
                                func.max(EventHistory.timestamp),
865
                         ).filter(EventHistory.idevent.in_(event_id_list)
866
                         ).scalar()
867
    if not last_modification_timestamp:
868
        if not value_if_none:
869
            return None
870
        else:
871
            last_modification_timestamp = value_if_none
872
    return datetime.fromtimestamp(mktime(
873
        last_modification_timestamp.timetuple()))