Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

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

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

    
40
from vigilo.models.session import DBSession
41
from vigilo.models.tables import Event, EventHistory, CorrEvent, Host, \
42
                                    SupItem, SupItemGroup, LowLevelService, \
43
                                    StateName, State
44
from vigilo.models.tables.grouphierarchy import GroupHierarchy
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
            search['supitemgroup'] = supitemgroup
151
            aggregates.add_join((GroupHierarchy, GroupHierarchy.idchild ==
152
                aggregates.items.c.idsupitemgroup))
153
            aggregates.add_filter(GroupHierarchy.idparent == supitemgroup)
154

    
155
        if host:
156
            search['host_'] = host
157
            host = sql_escape_like(host)
158
            aggregates.add_filter(aggregates.items.c.hostname.ilike(
159
                '%s' % host))
160

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

    
167
        if output:
168
            search['output'] = output
169
            output = sql_escape_like(output)
170
            aggregates.add_filter(Event.message.ilike('%s' % output))
171

    
172
        if trouble_ticket:
173
            search['tt'] = trouble_ticket
174
            trouble_ticket = sql_escape_like(trouble_ticket)
175
            aggregates.add_filter(CorrEvent.trouble_ticket.ilike(
176
                '%s' % trouble_ticket))
177

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

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

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

    
212
        id_first_row = items_per_page * (page-1)
213
        id_last_row = min(id_first_row + items_per_page, total_rows)
214

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

    
220
        aggregates.format_events(id_first_row, id_last_row)
221
        aggregates.generate_tmpl_context()
222

    
223
        nb_pages = int(math.ceil(total_rows / (items_per_page + 0.0)))
224
        if not total_rows:
225
            id_first_row = 0
226
        else:
227
            id_first_row += 1
228

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

    
246

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

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

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

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

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

    
302
        if isinstance(cause_supitem, LowLevelService):
303
            hostname = cause_supitem.host.name
304
            servicename = cause_supitem.servicename
305
        elif isinstance(cause_supitem, Host):
306
            hostname = cause_supitem.name
307

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

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

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

    
321
        events.format_events(id_first_row, id_last_row)
322
        events.generate_tmpl_context()
323

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

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

    
347

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

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

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

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

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

    
390
        events.format_events(0, 1)
391
        events.generate_tmpl_context()
392
        history = events.format_history()
393

    
394
        total_rows = history.count()
395
        items_per_page = int(config['vigiboard_items_per_page'])
396

    
397
        id_first_row = items_per_page * (page-1)
398
        id_last_row = min(id_first_row + items_per_page, total_rows)
399

    
400
        history_entries = history[id_first_row : id_last_row]
401

    
402
        nb_pages = int(math.ceil(total_rows / (items_per_page + 0.0)))
403
        if not total_rows:
404
            id_first_row = 0
405
        else:
406
            id_first_row += 1
407

    
408
        event = events.req[0]
409

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

    
427

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

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

445
        @param page: Numéro de la page à afficher.
446
        @param host: Nom de l'hôte souhaité.
447
        @param service: Nom du service souhaité
448

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

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

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

    
475
        items_per_page = int(config['vigiboard_items_per_page'])
476

    
477
        id_first_row = items_per_page * (page-1)
478
        id_last_row = min(id_first_row + items_per_page, total_rows)
479

    
480
        aggregates.format_events(id_first_row, id_last_row)
481
        aggregates.generate_tmpl_context()
482

    
483
        nb_pages = int(math.ceil(total_rows / (items_per_page + 0.0)))
484
        if not total_rows:
485
            id_first_row = 0
486
        else:
487
            id_first_row += 1
488

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

    
506

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

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

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

538
        Cette méthode permet de satisfaire les exigences suivantes :
539
            - VIGILO_EXIG_VIGILO_BAC_0020,
540
            - VIGILO_EXIG_VIGILO_BAC_0060,
541
            - VIGILO_EXIG_VIGILO_BAC_0110.
542
        """
543

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

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

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

    
562
        events.generate_request()
563
        idevents = [cause.idcause for cause in events.req]
564

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

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

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

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

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

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

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

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

    
659
        DBSession.flush()
660
        flash(_('Updated successfully'))
661
        redirect(request.environ.get('HTTP_REFERER', '/'))
662

    
663

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

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

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

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

    
701
        return plugins[plugin_name].get_value(idcorrevent, *arg, **krgv)
702

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

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

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

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

742
        @param parent_id: identifiant du groupe d'hôte parent
743
        @type  parent_id: C{int} or None
744
        """
745

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

    
752
        # TODO: Utiliser un schéma de validation
753
        parent_id = int(parent_id)
754

    
755
        # On vérifie si le groupe parent fait partie des
756
        # groupes auxquel l'utilisateur a accès, et on
757
        # retourne une liste vide dans le cas contraire
758
        is_manager = in_group('managers').is_met(request.environ)
759
        if not is_manager:
760
            direct_access = False
761
            user = get_current_user()
762
            user_groups = dict(user.supitemgroups())
763
            # On regarde d'abord si le groupe fait partie de ceux
764
            # auquels l'utilisateur a explicitement accès, ou s'il
765
            # est un parent des groupes auxquels l'utilisateur a accès
766
            if parent_id in user_groups.keys():
767
                direct_access = user_groups[parent_id]
768
            # Dans le cas contraire, on vérifie si le groupe est un
769
            # sous-groupe des groupes auxquels l'utilisateur a accès
770
            else:
771
                id_list = [ug for ug in user_groups.keys() if user_groups[ug]]
772
                child_groups = DBSession.query(SupItemGroup.idgroup
773
                    ).distinct(
774
                    ).join(
775
                        (GroupHierarchy,
776
                            GroupHierarchy.idchild == SupItemGroup.idgroup),
777
                    ).filter(GroupHierarchy.idparent.in_(id_list)
778
                    ).filter(GroupHierarchy.hops > 0
779
                    ).all()
780
                for ucg in child_groups:
781
                    if ucg.idgroup == parent_id:
782
                        direct_access = True
783
                        break
784
                # Sinon, l'utilisateur n'a pas accès à ce groupe
785
                else:
786
                    return dict(groups = [], leaves = [])
787

    
788
        # On récupère la liste des groupes dont
789
        # l'identifiant du parent est passé en paramètre
790
        db_groups = DBSession.query(
791
            SupItemGroup
792
        ).join(
793
            (GroupHierarchy, GroupHierarchy.idchild == \
794
                SupItemGroup.idgroup),
795
        ).filter(GroupHierarchy.hops == 1
796
        ).filter(GroupHierarchy.idparent == parent_id
797
        ).order_by(SupItemGroup.name.asc())
798
        if not is_manager and not direct_access:
799
            id_list = [ug for ug in user_groups.keys()]
800
            db_groups = db_groups.filter(
801
                SupItemGroup.idgroup.in_(id_list))
802
        groups = []
803
        for group in db_groups.all():
804
            groups.append({
805
                'id'   : group.idgroup,
806
                'name' : group.name,
807
            })
808

    
809
        return dict(groups = groups, leaves = [])
810

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

816
        @return: Un dictionnaire contenant la liste de ces groupes.
817
        @rtype : C{dict} of C{list} of C{dict} of C{mixed}
818
        """
819

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

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

    
836
        # On filtre ces groupes racines afin de ne
837
        # retourner que ceux auquels l'utilisateur a accès
838
        user = get_current_user()
839
        is_manager = in_group('managers').is_met(request.environ)
840
        if not is_manager:
841
            user_groups = [ug[0] for ug in user.supitemgroups()]
842
            root_groups = root_groups.filter(
843
                SupItemGroup.idgroup.in_(user_groups))
844

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

    
852
        return dict(groups = groups, leaves=[])
853

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