Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (40.4 KB)

1
# -*- coding: utf-8 -*-
2
# vim:set expandtab tabstop=4 shiftwidth=4:
3
# Copyright (C) 2007-2016 CS-SI
4
# License: GNU GPL v2 <http://www.gnu.org/licenses/gpl-2.0.html>
5

    
6
"""VigiBoard Controller"""
7

    
8
import gettext
9
import os.path
10
from datetime import datetime
11
from time import mktime
12

    
13
from pkg_resources import resource_filename, working_set
14

    
15
from tg.exceptions import HTTPNotFound
16
from tg import expose, validate, require, flash, url, \
17
    tmpl_context, request, response, config, session, redirect
18
from tg.support import paginate
19
from formencode import validators, schema
20
from tg.i18n import ugettext as _, lazy_ugettext as l_, get_lang
21
from sqlalchemy import asc
22
from sqlalchemy.sql import func
23
from sqlalchemy.orm import aliased
24
from sqlalchemy.sql.expression import or_
25
from tg.predicates import Any, All, NotAuthorizedError, \
26
                            has_permission, not_anonymous
27

    
28
from vigilo.models.session import DBSession
29
from vigilo.models.tables import Event, EventHistory, CorrEvent, Host, \
30
                                    SupItem, SupItemGroup, LowLevelService, \
31
                                    StateName, State, DataPermission
32
from vigilo.models.tables.grouphierarchy import GroupHierarchy
33
from vigilo.models.tables.secondary_tables import EVENTSAGGREGATE_TABLE, \
34
        USER_GROUP_TABLE, SUPITEM_GROUP_TABLE
35

    
36
from vigilo.turbogears.controllers.auth import AuthController
37
from vigilo.turbogears.controllers.selfmonitoring import SelfMonitoringController
38
from vigilo.turbogears.controllers.custom import CustomController
39
from vigilo.turbogears.controllers.error import ErrorController
40
from vigilo.turbogears.controllers.autocomplete import AutoCompleteController
41
from vigilo.turbogears.controllers.proxy import ProxyController
42
from vigilo.turbogears.controllers.api.root import ApiRootController
43
from vigilo.turbogears.helpers import get_current_user
44

    
45
from vigiboard.controllers.vigiboardrequest import VigiboardRequest
46
from vigiboard.controllers.feeds import FeedsController
47
from vigiboard.controllers.silence import SilenceController
48

    
49
from vigiboard.lib import export_csv, dateformat
50
from vigiboard.widgets.edit_event import edit_event_status_options, \
51
                                            EditEventForm
52
from vigiboard.widgets.search_form import create_search_form
53
import logging
54

    
55
LOGGER = logging.getLogger(__name__)
56

    
57
__all__ = ('RootController', 'get_last_modification_timestamp',
58
           'date_to_timestamp')
59

    
60
# pylint: disable-msg=R0201,W0613,W0622
61
# R0201: Method could be a function
62
# W0613: Unused arguments: les arguments sont la query-string
63
# W0622: Redefining built-in 'id': élément de la query-string
64

    
65
class RootController(AuthController, SelfMonitoringController):
66
    """
67
    Le controller général de vigiboard
68
    """
69
    _tickets = None
70
    _use_index_fallback = True
71

    
72
    error = ErrorController()
73
    autocomplete = AutoCompleteController()
74
    nagios = ProxyController('nagios', '/nagios/',
75
        not_anonymous(l_('You need to be authenticated')))
76
    api = ApiRootController()
77
    feeds = FeedsController()
78
    silence = SilenceController()
79
    custom = CustomController()
80

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

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

    
100
    @expose('json')
101
    def handle_validation_errors_json(self, *args, **kwargs):
102
        kwargs['errors'] = tmpl_context.form_errors
103
        return dict(kwargs)
104

    
105
    def __init__(self, *args, **kwargs):
106
        """Initialisation du contrôleur."""
107
        super(RootController, self).__init__(*args, **kwargs)
108
        # Si un module de gestion des tickets a été indiqué dans
109
        # le fichier de configuration, on tente de le charger.
110
        if config.get('tickets.plugin'):
111
            plugins = working_set.iter_entry_points('vigiboard.tickets', config['tickets.plugin'])
112
            if plugins:
113
                # La classe indiquée par la première valeur de l'itérateur
114
                # correspond au plugin que l'on veut instancier.
115
                pluginCls = plugins.next().load()
116
                self._tickets = pluginCls()
117

    
118
    class IndexSchema(schema.Schema):
119
        """Schéma de validation de la méthode index."""
120
        # Si on ne passe pas le paramètre "page" ou qu'on passe une valeur
121
        # invalide ou pas de valeur du tout, alors on affiche la 1ère page.
122
        page = validators.Int(
123
            min=1,
124
            if_missing=1,
125
            if_invalid=1,
126
            not_empty=True
127
        )
128

    
129
        # Paramètres de tri
130
        sort = validators.UnicodeString(if_missing='')
131
        order = validators.OneOf(['asc', 'desc'], if_missing='asc')
132

    
133
        # Le fait de chaîner la validation avec le formulaire de recherche
134
        # permet de convertir les critères de recherche vers leur type.
135
        chained_validators = [create_search_form.validator]
136

    
137
        allow_extra_fields = True
138

    
139
    @validate(
140
        validators=IndexSchema(),
141
        error_handler = process_form_errors)
142
    @expose('events_table.html')
143
    @require(access_restriction)
144
    def index(self, page=None, sort=None, order=None, **search):
145
        """
146
        Page d'accueil de Vigiboard. Elle affiche, suivant la page demandée
147
        (page 1 par defaut), la liste des événements, rangés par ordre de prise
148
        en compte, puis de sévérité.
149
        Pour accéder à cette page, l'utilisateur doit être authentifié.
150

151
        @param page: Numéro de la page souhaitée, commence à 1
152
        @type page: C{int}
153
        @param sort: Colonne de tri
154
        @type sort: C{str} or C{None}
155
        @param order: Ordre du tri (asc ou desc)
156
        @type order: C{str} or C{None}
157
        @param search: Dictionnaire contenant les critères de recherche.
158
        @type search: C{dict}
159

160
        Cette méthode permet de satisfaire les exigences suivantes :
161
            - VIGILO_EXIG_VIGILO_BAC_0040,
162
            - VIGILO_EXIG_VIGILO_BAC_0070,
163
            - VIGILO_EXIG_VIGILO_BAC_0100,
164
        """
165

    
166
        # Auto-supervision
167
        self.get_failures()
168

    
169
        user = get_current_user()
170
        aggregates = VigiboardRequest(user, search=search, sort=sort, order=order)
171

    
172
        aggregates.add_table(
173
            CorrEvent,
174
            aggregates.items.c.hostname,
175
            aggregates.items.c.servicename
176
        )
177
        aggregates.add_join((Event, CorrEvent.idcause == Event.idevent))
178
        aggregates.add_contains_eager(CorrEvent.cause)
179
        aggregates.add_group_by(Event)
180
        aggregates.add_join((aggregates.items,
181
            Event.idsupitem == aggregates.items.c.idsupitem))
182
        aggregates.add_order_by(asc(aggregates.items.c.hostname))
183

    
184
        # On ne garde que les champs effectivement renseignés.
185
        for column in search.copy():
186
            if not search[column]:
187
                del search[column]
188

    
189
        # On sérialise les champs de type dict.
190
        def serialize_dict(dct, key):
191
            if isinstance(dct[key], dict):
192
                for subkey in dct[key]:
193
                    serialize_dict(dct[key], subkey)
194
                    dct['%s.%s' % (key, subkey)] = dct[key][subkey]
195
                del dct[key]
196
            elif isinstance(dct[key], datetime):
197
                dct[key] = dct[key].strftime(dateformat.get_date_format())
198
        fixed_search = search.copy()
199
        for column in fixed_search.copy():
200
            serialize_dict(fixed_search, column)
201

    
202
        # Pagination des résultats
203
        aggregates.generate_request()
204
        items_per_page = int(session.get('items_per_page', config['vigiboard_items_per_page']))
205
        page = paginate.Page(aggregates.req, page=page,
206
            items_per_page=items_per_page)
207

    
208
        # Récupération des données des plugins
209
        plugins_data = {}
210
        plugins = dict(config['columns_plugins'])
211

    
212
        ids_events = [event[0].idcause for event in page.items]
213
        ids_correvents = [event[0].idcorrevent for event in page.items]
214
        for plugin in plugins:
215
            plugin_data = plugins[plugin].get_bulk_data(ids_correvents)
216
            if plugin_data:
217
                plugins_data[plugin] = plugin_data
218
            else:
219
                plugins_data[plugin] = {}
220

    
221
        # Ajout des formulaires et préparation
222
        # des données pour ces formulaires.
223
        tmpl_context.last_modification = \
224
            mktime(get_last_modification_timestamp(ids_events).timetuple())
225

    
226
        tmpl_context.edit_event_form = EditEventForm("edit_event_form",
227
            submit_text=_('Apply'), action=url('/update'))
228

    
229
        if request.response_type == 'text/csv':
230
            # Sans les 2 en-têtes suivants qui désactivent la mise en cache,
231
            # Internet Explorer refuse de télécharger le fichier CSV (cf. #961).
232
            response.headers['Pragma'] = 'public'           # Nécessaire pour IE.
233
            response.headers['Cache-Control'] = 'max-age=0' # Nécessaire pour IE.
234

    
235
            response.headers["Content-Type"] = "text/csv"
236
            response.headers['Content-Disposition'] = \
237
                            'attachment;filename="alerts.csv"'
238
            return export_csv.export(page, plugins_data)
239

    
240
        return dict(
241
            hostname = None,
242
            servicename = None,
243
            plugins_data = plugins_data,
244
            page = page,
245
            sort = sort,
246
            order = order,
247
            event_edit_status_options = edit_event_status_options,
248
            search_form = create_search_form,
249
            search = search,
250
            fixed_search = fixed_search,
251
        )
252

    
253

    
254
    @expose()
255
    def i18n(self):
256
        # Repris de tg.i18n:_get_translator.
257
        conf = config.current_conf()
258
        try:
259
            localedir = conf['localedir']
260
        except KeyError:
261
            localedir = os.path.join(conf['paths']['root'], 'i18n')
262

    
263
        lang = get_lang()
264

    
265
        # Localise le fichier *.mo actuellement chargé
266
        # et génère le chemin jusqu'au *.js correspondant.
267
        filename = gettext.find(conf['package'].__name__, localedir,
268
            languages=lang)
269
        js = filename[:-3] + '.js'
270

    
271
        themes_filename = gettext.find(
272
            'vigilo-themes',
273
            resource_filename('vigilo.themes.i18n', ''),
274
            languages=lang)
275
        themes_js = themes_filename[:-3] + '.js'
276

    
277
        # Récupère et envoie le contenu du fichier de traduction *.js.
278
        fhandle = open(js, 'r')
279
        translations = fhandle.read()
280
        fhandle.close()
281

    
282
        fhandle = open(themes_js, 'r')
283
        translations += fhandle.read()
284
        fhandle.close()
285

    
286
        response.headers['Content-Type'] = 'text/javascript; charset=utf-8'
287
        return translations
288

    
289

    
290
    class MaskedEventsSchema(schema.Schema):
291
        """Schéma de validation de la méthode masked_events."""
292
        idcorrevent = validators.Int(not_empty=True)
293
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
294

    
295
    @validate(
296
        validators=MaskedEventsSchema(),
297
        error_handler = process_form_errors)
298
    @expose('raw_events_table.html')
299
    @require(access_restriction)
300
    def masked_events(self, idcorrevent, page=1):
301
        """
302
        Affichage de la liste des événements bruts masqués d'un événement
303
        corrélé (événements agrégés dans l'événement corrélé).
304

305
        @param page: numéro de la page à afficher.
306
        @type  page: C{int}
307
        @param idcorrevent: identifiant de l'événement corrélé souhaité.
308
        @type  idcorrevent: C{int}
309
        """
310

    
311
        # Auto-supervision
312
        self.get_failures()
313

    
314
        user = get_current_user()
315

    
316
        # Récupère la liste des événements masqués de l'événement
317
        # corrélé donné par idcorrevent.
318
        events = VigiboardRequest(user, False)
319
        events.add_table(
320
            Event,
321
            events.items.c.hostname,
322
            events.items.c.servicename,
323
        )
324
        events.add_join((EVENTSAGGREGATE_TABLE, \
325
            EVENTSAGGREGATE_TABLE.c.idevent == Event.idevent))
326
        events.add_join((CorrEvent, CorrEvent.idcorrevent == \
327
            EVENTSAGGREGATE_TABLE.c.idcorrevent))
328
        events.add_join((events.items,
329
            Event.idsupitem == events.items.c.idsupitem))
330
        events.add_filter(Event.idevent != CorrEvent.idcause)
331
        events.add_filter(CorrEvent.idcorrevent == idcorrevent)
332

    
333
        # Récupère l'instance de SupItem associé à la cause de
334
        # l'événement corrélé. Cette instance est utilisé pour
335
        # obtenir le nom d'hôte/service auquel la cause est
336
        # rattachée (afin de fournir un contexte à l'utilisateur).
337
        hostname = None
338
        servicename = None
339
        cause_supitem = DBSession.query(
340
                SupItem,
341
            ).join(
342
                (Event, Event.idsupitem == SupItem.idsupitem),
343
                (CorrEvent, Event.idevent == CorrEvent.idcause),
344
            ).filter(CorrEvent.idcorrevent == idcorrevent
345
            ).one()
346

    
347
        if isinstance(cause_supitem, LowLevelService):
348
            hostname = cause_supitem.host.name
349
            servicename = cause_supitem.servicename
350
        elif isinstance(cause_supitem, Host):
351
            hostname = cause_supitem.name
352

    
353
        # Pagination des résultats
354
        events.generate_request()
355
        items_per_page = int(session.get('items_per_page', config['vigiboard_items_per_page']))
356
        page = paginate.Page(events.req, page=page,
357
            items_per_page=items_per_page)
358

    
359
        # Vérification que l'événement existe
360
        if not page.item_count:
361
            flash(_('No masked event or access denied'), 'error')
362
            redirect('/')
363

    
364
        return dict(
365
            idcorrevent = idcorrevent,
366
            hostname = hostname,
367
            servicename = servicename,
368
            plugins_data = {},
369
            page = page,
370
            search_form = create_search_form,
371
            search = {},
372
            fixed_search = {},
373
        )
374

    
375

    
376
    class EventSchema(schema.Schema):
377
        """Schéma de validation de la méthode event."""
378
        idevent = validators.Int(not_empty=True)
379
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
380

    
381
    @validate(
382
        validators=EventSchema(),
383
        error_handler = process_form_errors)
384
    @expose('history_table.html')
385
    @require(access_restriction)
386
    def event(self, idevent, page=1):
387
        """
388
        Affichage de l'historique d'un événement brut.
389
        Pour accéder à cette page, l'utilisateur doit être authentifié.
390

391
        @param idevent: identifiant de l'événement brut souhaité.
392
        @type idevent: C{int}
393
        @param page: numéro de la page à afficher.
394
        @type page: C{int}
395

396
        Cette méthode permet de satisfaire l'exigence
397
        VIGILO_EXIG_VIGILO_BAC_0080.
398
        """
399

    
400
        # Auto-supervision
401
        self.get_failures()
402

    
403
        user = get_current_user()
404
        events = VigiboardRequest(user, False)
405
        events.add_table(
406
            Event,
407
            events.items.c.hostname.label('hostname'),
408
            events.items.c.servicename.label('servicename'),
409
        )
410
        events.add_join((EVENTSAGGREGATE_TABLE, \
411
            EVENTSAGGREGATE_TABLE.c.idevent == Event.idevent))
412
        events.add_join((CorrEvent, CorrEvent.idcorrevent == \
413
            EVENTSAGGREGATE_TABLE.c.idcorrevent))
414
        events.add_join((events.items,
415
            Event.idsupitem == events.items.c.idsupitem))
416
        events.add_filter(Event.idevent == idevent)
417

    
418
        if events.num_rows() != 1:
419
            flash(_('No such event or access denied'), 'error')
420
            redirect('/')
421

    
422
        events.format_events(0, 1)
423
        events.generate_tmpl_context()
424
        history = events.format_history()
425

    
426
        # Pagination des résultats
427
        items_per_page = int(session.get('items_per_page', config['vigiboard_items_per_page']))
428
        page = paginate.Page(history, page=page, items_per_page=items_per_page)
429
        event = events.req[0]
430

    
431
        return dict(
432
            idevent = idevent,
433
            hostname = event.hostname,
434
            servicename = event.servicename,
435
            plugins_data = {},
436
            page = page,
437
            search_form = create_search_form,
438
            search = {},
439
            fixed_search = {},
440
        )
441

    
442

    
443
    class ItemSchema(schema.Schema):
444
        """Schéma de validation de la méthode item."""
445
        # Si on ne passe pas le paramètre "page" ou qu'on passe une valeur
446
        # invalide ou pas de valeur du tout, alors on affiche la 1ère page.
447
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
448

    
449
        # Paramètres de tri
450
        sort = validators.String(if_missing=None)
451
        order = validators.OneOf(['asc', 'desc'], if_missing='asc')
452

    
453
        # L'hôte / service dont on doit afficher les évènements
454
        host = validators.String(not_empty=True)
455
        service = validators.String(if_missing=None)
456

    
457
    @validate(
458
        validators=ItemSchema(),
459
        error_handler = process_form_errors)
460
    @expose('events_table.html')
461
    @require(access_restriction)
462
    def item(self, page=1, host=None, service=None, sort=None, order=None):
463
        """
464
        Affichage de l'historique de l'ensemble des événements corrélés
465
        jamais ouverts sur l'hôte / service demandé.
466
        Pour accéder à cette page, l'utilisateur doit être authentifié.
467

468
        @param page: Numéro de la page à afficher.
469
        @type: C{int}
470
        @param host: Nom de l'hôte souhaité.
471
        @type: C{str}
472
        @param service: Nom du service souhaité
473
        @type: C{str}
474
        @param sort: Colonne de tri
475
        @type: C{str} or C{None}
476
        @param order: Ordre du tri (asc ou desc)
477
        @type: C{str} or C{None}
478

479
        Cette méthode permet de satisfaire l'exigence
480
        VIGILO_EXIG_VIGILO_BAC_0080.
481
        """
482

    
483
        # Auto-supervision
484
        self.get_failures()
485

    
486
        idsupitem = SupItem.get_supitem(host, service)
487
        if not idsupitem:
488
            flash(_('No such host/service'), 'error')
489
            redirect('/')
490

    
491
        user = get_current_user()
492
        aggregates = VigiboardRequest(user, False, sort=sort, order=order)
493
        aggregates.add_table(
494
            CorrEvent,
495
            aggregates.items.c.hostname,
496
            aggregates.items.c.servicename,
497
        )
498
        aggregates.add_join((Event, CorrEvent.idcause == Event.idevent))
499
        aggregates.add_join((aggregates.items,
500
            Event.idsupitem == aggregates.items.c.idsupitem))
501
        aggregates.add_filter(aggregates.items.c.idsupitem == idsupitem)
502

    
503
        # Pagination des résultats
504
        aggregates.generate_request()
505
        items_per_page = int(session.get('items_per_page', config['vigiboard_items_per_page']))
506
        page = paginate.Page(aggregates.req, page=page,
507
            items_per_page=items_per_page)
508

    
509
        # Vérification qu'il y a au moins 1 événement qui correspond
510
        if not page.item_count:
511
            flash(_('No access to this host/service or no event yet'), 'error')
512
            redirect('/')
513

    
514
        # Ajout des formulaires et préparation
515
        # des données pour ces formulaires.
516
        ids_events = [event[0].idcause for event in page.items]
517
        tmpl_context.last_modification = \
518
            mktime(get_last_modification_timestamp(ids_events).timetuple())
519

    
520
        tmpl_context.edit_event_form = EditEventForm("edit_event_form",
521
            submit_text=_('Apply'), action=url('/update'))
522

    
523
        plugins_data = {}
524
        for plugin in dict(config['columns_plugins']):
525
            plugins_data[plugin] = {}
526

    
527
        return dict(
528
            hostname = host,
529
            servicename = service,
530
            plugins_data = plugins_data,
531
            page = page,
532
            sort = sort,
533
            order = order,
534
            event_edit_status_options = edit_event_status_options,
535
            search_form = create_search_form,
536
            search = {},
537
            fixed_search = {},
538
        )
539

    
540

    
541
    class UpdateSchema(schema.Schema):
542
        """Schéma de validation de la méthode update."""
543
        id = validators.Regex(r'^[0-9]+(,[0-9]+)*,?$')
544
        last_modification = validators.Number(not_empty=True)
545
        trouble_ticket = validators.String(if_missing='')
546
        ack = validators.OneOf(
547
            [unicode(s[0]) for s in edit_event_status_options],
548
            not_empty=True)
549

    
550
    @validate(
551
        validators=UpdateSchema(),
552
        error_handler = process_form_errors)
553
    @require(
554
        All(
555
            not_anonymous(msg=l_("You need to be authenticated")),
556
            Any(config.is_manager,
557
                has_permission('vigiboard-update'),
558
                msg=l_("You don't have write access to VigiBoard"))
559
        ))
560
    @expose()
561
    def update(self, id, last_modification, trouble_ticket, ack):
562
        """
563
        Mise à jour d'un événement suivant les arguments passés.
564
        Cela peut être un changement de ticket ou un changement de statut.
565

566
        @param id: Le ou les identifiants des événements à traiter
567
        @param last_modification: La date de la dernière modification
568
            dont l'utilisateur est au courant.
569
        @param trouble_ticket: Nouveau numéro du ticket associé.
570
        @param ack: Nouvel état d'acquittement des événements sélectionnés.
571

572
        Cette méthode permet de satisfaire les exigences suivantes :
573
            - VIGILO_EXIG_VIGILO_BAC_0020,
574
            - VIGILO_EXIG_VIGILO_BAC_0060,
575
            - VIGILO_EXIG_VIGILO_BAC_0110.
576
        """
577

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

    
584
        # On récupère la liste de tous les identifiants des événements
585
        # à mettre à jour.
586
        ids = [ int(i) for i in id.strip(',').split(',') ]
587

    
588
        user = get_current_user()
589
        events = VigiboardRequest(user)
590
        events.add_table(
591
            CorrEvent,
592
            Event,
593
            events.items.c.hostname,
594
            events.items.c.servicename,
595
        )
596
        events.add_join((Event, CorrEvent.idcause == Event.idevent))
597
        events.add_join((events.items,
598
            Event.idsupitem == events.items.c.idsupitem))
599
        events.add_filter(CorrEvent.idcorrevent.in_(ids))
600

    
601
        events.generate_request()
602
        idevents = [event[0].idcause for event in events.req]
603

    
604
        # Si des changements sont survenus depuis que la
605
        # page est affichée, on en informe l'utilisateur.
606
        last_modification = datetime.fromtimestamp(last_modification)
607
        cur_last_modification = get_last_modification_timestamp(idevents, None)
608
        if cur_last_modification and last_modification < cur_last_modification:
609
            flash(_('Changes have occurred since the page was last displayed, '
610
                    'your changes HAVE NOT been saved.'), 'warning')
611
            raise redirect(request.environ.get('HTTP_REFERER', '/'))
612

    
613
        # Vérification que au moins un des identifiants existe et est éditable
614
        if not events.num_rows():
615
            flash(_('No access to this event'), 'error')
616
            redirect('/')
617

    
618
        if ack == u'Forced':
619
            condition = Any(
620
                config.is_manager,
621
                has_permission('vigiboard-admin'),
622
                msg=l_("You don't have administrative access "
623
                        "to VigiBoard"))
624
            try:
625
                condition.check_authorization(request.environ)
626
            except NotAuthorizedError as e:
627
                reason = unicode(e)
628
                flash(reason, 'error')
629
                raise redirect(request.environ.get('HTTP_REFERER', '/'))
630

    
631
        # Si un module de gestion de ticket est utilisé,
632
        # il a la possibilité de changer à la volée le libellé du ticket.
633
        if self._tickets:
634
            trouble_ticket = self._tickets.createTicket(events.req, trouble_ticket)
635

    
636
        # Définit 2 mappings dont les ensembles sont disjoincts
637
        # pour basculer entre la représentation en base de données
638
        # et la représentation "humaine" du bac à événements.
639
        ack_mapping = {
640
            # Permet d'associer la valeur dans le widget ToscaWidgets
641
            # (cf. vigiboard.widgets.edit_event.edit_event_status_options)
642
            # avec la valeur dans la base de données.
643
            u'None': CorrEvent.ACK_NONE,
644
            u'Acknowledged': CorrEvent.ACK_KNOWN,
645
            u'AAClosed': CorrEvent.ACK_CLOSED,
646

    
647
            # Permet d'afficher un libellé plus sympathique pour l'utilisateur
648
            # représentant l'état d'acquittement stocké en base de données.
649
            CorrEvent.ACK_NONE: l_('None'),
650
            CorrEvent.ACK_KNOWN: l_('Acknowledged'),
651
            CorrEvent.ACK_CLOSED: l_('Acknowledged and closed'),
652
        }
653

    
654
        # Modification des événements et création d'un historique
655
        # chaque fois que cela est nécessaire.
656
        for data in events.req:
657
            event = data[0]
658
            if trouble_ticket and trouble_ticket != event.trouble_ticket:
659
                history = EventHistory(
660
                        type_action=u"Ticket change",
661
                        idevent=event.idcause,
662
                        value=unicode(trouble_ticket),
663
                        text="Changed trouble ticket from '%(from)s' "
664
                             "to '%(to)s'" % {
665
                            'from': event.trouble_ticket,
666
                            'to': trouble_ticket,
667
                        },
668
                        username=user.user_name,
669
                        timestamp=datetime.now(),
670
                    )
671
                DBSession.add(history)
672
                LOGGER.info(_('User "%(user)s" (%(address)s) changed the '
673
                            'trouble ticket from "%(previous)s" to "%(new)s" '
674
                            'on event #%(idevent)d') % {
675
                                'user': request.identity['repoze.who.userid'],
676
                                'address': request.remote_addr,
677
                                'previous': event.trouble_ticket,
678
                                'new': trouble_ticket,
679
                                'idevent': event.idcause,
680
                            })
681
                event.trouble_ticket = trouble_ticket
682

    
683
            # Changement du statut d'acquittement.
684
            if ack != u'NoChange':
685
                changed_ack = ack
686
                # Pour forcer l'acquittement d'un événement,
687
                # il faut en plus avoir la permission
688
                # "vigiboard-admin".
689
                if ack == u'Forced':
690
                    changed_ack = u'AAClosed'
691
                    cause = event.cause
692
                    # On met systématiquement l'événement à l'état "OK",
693
                    # même s'il s'agit d'un hôte.
694
                    # Techniquement, c'est incorrect, mais on fait ça
695
                    # pour masquer l'événement de toutes façons...
696
                    cause.current_state = \
697
                        StateName.statename_to_value(u'OK')
698

    
699
                    # Mise à jour de l'état dans State, pour que
700
                    # VigiMap soit également mis à jour.
701
                    DBSession.query(State).filter(
702
                            State.idsupitem == cause.idsupitem,
703
                        ).update({
704
                            'state': StateName.statename_to_value(u'OK'),
705
                        })
706

    
707
                    history = EventHistory(
708
                            type_action=u"Forced change state",
709
                            idevent=event.idcause,
710
                            value=u'OK',
711
                            text="Forced state to 'OK'",
712
                            username=user.user_name,
713
                            timestamp=datetime.now(),
714
                            state=StateName.statename_to_value(u'OK'),
715
                        )
716
                    DBSession.add(history)
717
                    LOGGER.info(_('User "%(user)s" (%(address)s) forcefully '
718
                                'closed event #%(idevent)d') % {
719
                                    'user': request. \
720
                                            identity['repoze.who.userid'],
721
                                    'address': request.remote_addr,
722
                                    'idevent': event.idcause,
723
                                })
724

    
725
                # Convertit la valeur du widget ToscaWidgets
726
                # vers le code interne puis vers un libellé
727
                # "humain".
728
                ack_label = ack_mapping[ack_mapping[changed_ack]]
729

    
730
                # Si le changement a été forcé,
731
                # on veut le mettre en évidence.
732
                if ack == u'Forced':
733
                    history_label = u'Forced'
734
                else:
735
                    history_label = ack_label
736

    
737
                history = EventHistory(
738
                        type_action=u"Acknowledgement change state",
739
                        idevent=event.idcause,
740
                        value=unicode(history_label),
741
                        text=u"Changed acknowledgement status "
742
                            u"from '%s' to '%s'" % (
743
                            ack_mapping[event.ack],
744
                            ack_label,
745
                        ),
746
                        username=user.user_name,
747
                        timestamp=datetime.now(),
748
                    )
749
                DBSession.add(history)
750
                LOGGER.info(_('User "%(user)s" (%(address)s) changed the state '
751
                            'from "%(previous)s" to "%(new)s" on event '
752
                            '#%(idevent)d') % {
753
                                'user': request.identity['repoze.who.userid'],
754
                                'address': request.remote_addr,
755
                                'previous': _(ack_mapping[event.ack]),
756
                                'new': _(ack_label),
757
                                'idevent': event.idcause,
758
                            })
759
                event.ack = ack_mapping[changed_ack]
760

    
761
        DBSession.flush()
762
        flash(_('Updated successfully'))
763
        redirect(request.environ.get('HTTP_REFERER', '/'))
764

    
765

    
766
    class GetPluginValueSchema(schema.Schema):
767
        """Schéma de validation de la méthode get_plugin_value."""
768
        idcorrevent = validators.Int(not_empty=True)
769
        plugin_name = validators.String(not_empty=True)
770
        # Permet de passer des paramètres supplémentaires au plugin.
771
        allow_extra_fields = True
772

    
773
    @validate(
774
        validators=GetPluginValueSchema(),
775
        error_handler = handle_validation_errors_json)
776
    @expose('json')
777
    @require(access_restriction)
778
    def plugin_json(self, idcorrevent, plugin_name, *arg, **krgv):
779
        """
780
        Permet de récupérer la valeur d'un plugin associée à un CorrEvent
781
        donné via JSON.
782
        """
783

    
784
        # Vérification de l'existence du plugin
785
        plugins = dict(config['columns_plugins'])
786
        if plugin_name not in plugins:
787
            raise HTTPNotFound(_("No such plugin '%s'") % plugin_name)
788

    
789
        # Récupération de la liste des évènements corrélés
790
        events = DBSession.query(CorrEvent.idcorrevent)
791

    
792
        # Filtrage des évènements en fonction des permissions de
793
        # l'utilisateur (s'il n'appartient pas au groupe 'managers')
794
        if not config.is_manager.is_met(request.environ):
795
            user = get_current_user()
796

    
797
            events = events.join(
798
                (Event, Event.idevent == CorrEvent.idcause),
799
            ).outerjoin(
800
                (LowLevelService, LowLevelService.idservice == Event.idsupitem),
801
            ).join(
802
                (SUPITEM_GROUP_TABLE,
803
                    or_(
804
                        SUPITEM_GROUP_TABLE.c.idsupitem == \
805
                            LowLevelService.idhost,
806
                        SUPITEM_GROUP_TABLE.c.idsupitem == \
807
                            Event.idsupitem,
808
                    )
809
                ),
810
            ).join(
811
                (GroupHierarchy,
812
                    GroupHierarchy.idchild == SUPITEM_GROUP_TABLE.c.idgroup),
813
            ).join(
814
                (DataPermission,
815
                    DataPermission.idgroup == GroupHierarchy.idparent),
816
            ).join(
817
                (USER_GROUP_TABLE,
818
                    USER_GROUP_TABLE.c.idgroup == DataPermission.idusergroup),
819
            ).filter(USER_GROUP_TABLE.c.username == user.user_name)
820

    
821
        # Filtrage des évènements en fonction
822
        # de l'identifiant passé en paramètre
823
        events = events.filter(CorrEvent.idcorrevent == idcorrevent).count()
824

    
825
        # Pas d'événement ou permission refusée. On ne distingue pas
826
        # les 2 cas afin d'éviter la divulgation d'informations.
827
        if events == 0:
828
            raise HTTPNotFound(_('No such incident or insufficient '
829
                                'permissions'))
830

    
831
        # L'évènement existe bien, et l'utilisateur dispose
832
        # des permissions appropriées. On fait alors appel au
833
        # plugin pour récupérer les informations à retourner.
834
        return plugins[plugin_name].get_json_data(idcorrevent, *arg, **krgv)
835

    
836
    @validate(validators={
837
        "fontsize": validators.Regex(
838
            r'[0-9]+(pt|px|em|%)',
839
            regexOps = ('I',)
840
        )}, error_handler = handle_validation_errors_json)
841
    @expose('json')
842
    def set_fontsize(self, fontsize):
843
        """Enregistre la taille de la police dans les préférences."""
844
        session['fontsize'] = fontsize
845
        session.save()
846
        return dict()
847

    
848
    @validate(validators={"refresh": validators.Int()},
849
            error_handler = handle_validation_errors_json)
850
    @expose('json')
851
    def set_refresh(self, refresh):
852
        """Enregistre le temps de rafraichissement dans les préférences."""
853
        session['refresh'] = bool(refresh)
854
        session.save()
855
        return dict()
856

    
857
    @expose('json')
858
    def set_theme(self, theme):
859
        """Enregistre le thème à utiliser dans les préférences."""
860
        # On sauvegarde l'ID du thème sans vérifications
861
        # car les thèmes (styles CSS) sont définies dans
862
        # les packages de thèmes (ex: vigilo-themes-default).
863
        # La vérification de la valeur est faite dans les templates.
864
        session['theme'] = theme
865
        session.save()
866
        return dict()
867

    
868
    @validate(validators={"items": validators.Int()},
869
            error_handler = handle_validation_errors_json)
870
    @expose('json')
871
    def set_items_per_page(self, items):
872
        """Enregistre le nombre d'alertes par page dans les préférences."""
873
        session['items_per_page'] = items
874
        session.save()
875
        return dict()
876

    
877
    @require(access_restriction)
878
    @expose('json')
879
    def get_groups(self, parent_id=None, onlytype="", offset=0, noCache=None):
880
        """
881
        Affiche un étage de l'arbre de
882
        sélection des hôtes et groupes d'hôtes.
883

884
        @param parent_id: identifiant du groupe d'hôte parent
885
        @type  parent_id: C{int} or None
886
        """
887

    
888
        # Si l'identifiant du groupe parent n'est pas
889
        # spécifié, on retourne la liste des groupes
890
        # racines, fournie par la méthode get_root_groups.
891
        if parent_id is None:
892
            return self.get_root_groups()
893

    
894
        # TODO: Utiliser un schéma de validation
895
        parent_id = int(parent_id)
896
        offset = int(offset)
897

    
898
        # On récupère la liste des groupes de supitems dont
899
        # l'identifiant du parent est passé en paramètre.
900
        supitem_groups = DBSession.query(
901
                SupItemGroup.idgroup,
902
                SupItemGroup.name,
903
            ).join(
904
                (GroupHierarchy,
905
                    GroupHierarchy.idchild == SupItemGroup.idgroup),
906
            ).filter(GroupHierarchy.idparent == parent_id
907
            ).filter(GroupHierarchy.hops == 1
908
            ).order_by(SupItemGroup.name)
909

    
910
        # Si l'utilisateur n'appartient pas au groupe 'managers',
911
        # on filtre les résultats en fonction de ses permissions.
912
        if not config.is_manager.is_met(request.environ):
913
            user = get_current_user()
914
            GroupHierarchy_aliased = aliased(GroupHierarchy,
915
                name='GroupHierarchy_aliased')
916
            supitem_groups = supitem_groups.join(
917
                (GroupHierarchy_aliased,
918
                    or_(
919
                        GroupHierarchy_aliased.idchild == SupItemGroup.idgroup,
920
                        GroupHierarchy_aliased.idparent == SupItemGroup.idgroup
921
                    )),
922
                (DataPermission,
923
                    or_(
924
                        DataPermission.idgroup == \
925
                            GroupHierarchy_aliased.idparent,
926
                        DataPermission.idgroup == \
927
                            GroupHierarchy_aliased.idchild,
928
                    )),
929
                (USER_GROUP_TABLE, USER_GROUP_TABLE.c.idgroup == \
930
                    DataPermission.idusergroup),
931
            ).filter(USER_GROUP_TABLE.c.username == user.user_name)
932

    
933
        limit = int(config.get("max_menu_entries", 20))
934
        result = {"groups": [], "items": []}
935
        num_children_left = supitem_groups.distinct().count() - offset
936
        if offset:
937
            result["continued_from"] = offset
938
            result["continued_type"] = "group"
939
        all_grs = supitem_groups.distinct().limit(limit).offset(offset).all()
940
        for group in all_grs:
941
            result["groups"].append({
942
                'id'   : group.idgroup,
943
                'name' : group.name,
944
                'type' : "group",
945
            })
946
        if num_children_left > limit:
947
            result["groups"].append({
948
                'name': _("Next %(limit)s") % {"limit": limit},
949
                'offset': offset + limit,
950
                'parent_id': parent_id,
951
                'type': 'continued',
952
                'for_type': 'group',
953
            })
954

    
955
        return result
956

    
957
    def get_root_groups(self):
958
        """
959
        Retourne tous les groupes racines (c'est à dire n'ayant
960
        aucun parent) d'hôtes auquel l'utilisateur a accès.
961

962
        @return: Un dictionnaire contenant la liste de ces groupes.
963
        @rtype : C{dict} of C{list} of C{dict} of C{mixed}
964
        """
965

    
966
        # On récupère tous les groupes qui ont un parent.
967
        children = DBSession.query(
968
            SupItemGroup,
969
        ).distinct(
970
        ).join(
971
            (GroupHierarchy, GroupHierarchy.idchild == SupItemGroup.idgroup)
972
        ).filter(GroupHierarchy.hops > 0)
973

    
974
        # Ensuite on les exclut de la liste des groupes,
975
        # pour ne garder que ceux qui sont au sommet de
976
        # l'arbre et qui constituent nos "root groups".
977
        root_groups = DBSession.query(
978
            SupItemGroup,
979
        ).except_(children
980
        ).order_by(SupItemGroup.name)
981

    
982
        # On filtre ces groupes racines afin de ne
983
        # retourner que ceux auquels l'utilisateur a accès
984
        user = get_current_user()
985
        if not config.is_manager.is_met(request.environ):
986
            root_groups = root_groups.join(
987
                (GroupHierarchy,
988
                    GroupHierarchy.idparent == SupItemGroup.idgroup),
989
                (DataPermission,
990
                    DataPermission.idgroup == GroupHierarchy.idchild),
991
                (USER_GROUP_TABLE, USER_GROUP_TABLE.c.idgroup == \
992
                    DataPermission.idusergroup),
993
            ).filter(USER_GROUP_TABLE.c.username == user.user_name)
994

    
995
        groups = []
996
        for group in root_groups.all():
997
            groups.append({
998
                'id'   : group.idgroup,
999
                'name' : group.name,
1000
                'type' : "group",
1001
            })
1002

    
1003
        return dict(groups=groups, items=[])
1004

    
1005
def get_last_modification_timestamp(event_id_list,
1006
                                    value_if_none=datetime.now()):
1007
    """
1008
    Récupère le timestamp de la dernière modification
1009
    opérée sur l'un des événements dont l'identifiant
1010
    fait partie de la liste passée en paramètre.
1011
    """
1012
    if not event_id_list:
1013
        last_modification_timestamp = None
1014
    else:
1015
        last_modification_timestamp = DBSession.query(
1016
                                func.max(EventHistory.timestamp),
1017
                         ).filter(EventHistory.idevent.in_(event_id_list)
1018
                         ).scalar()
1019

    
1020
    if not last_modification_timestamp:
1021
        if not value_if_none:
1022
            return None
1023
        else:
1024
            last_modification_timestamp = value_if_none
1025
    return datetime.fromtimestamp(mktime(
1026
        last_modification_timestamp.timetuple()))