Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

vigiboard / vigiboard / controllers / root.py @ 011743be

History | View | Annotate | Download (39.4 KB)

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

    
6
"""VigiBoard Controller"""
7

    
8
import calendar
9
import gettext
10
import os.path
11
from datetime import datetime
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.i18n import I18nController
43
from vigilo.turbogears.controllers.api.root import ApiRootController
44
from vigilo.turbogears.helpers import get_current_user
45

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

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

    
56
LOGGER = logging.getLogger(__name__)
57

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

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

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

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

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

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

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

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

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

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

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

    
138
        allow_extra_fields = True
139

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

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

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

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

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

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

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

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

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

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

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

    
223
        # Ajout des formulaires et préparation
224
        # des données pour ces formulaires.
225
        tmpl_context.last_modification = calendar.timegm(
226
            get_last_modification_timestamp(ids_events).timetuple())
227

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

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

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

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

    
255

    
256
    class MaskedEventsSchema(schema.Schema):
257
        """Schéma de validation de la méthode masked_events."""
258
        idcorrevent = validators.Int(not_empty=True)
259
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
260

    
261
    @validate(
262
        validators=MaskedEventsSchema(),
263
        error_handler = process_form_errors)
264
    @expose('raw_events_table.html')
265
    @require(access_restriction)
266
    def masked_events(self, idcorrevent, page=1):
267
        """
268
        Affichage de la liste des événements bruts masqués d'un événement
269
        corrélé (événements agrégés dans l'événement corrélé).
270

271
        @param page: numéro de la page à afficher.
272
        @type  page: C{int}
273
        @param idcorrevent: identifiant de l'événement corrélé souhaité.
274
        @type  idcorrevent: C{int}
275
        """
276

    
277
        # Auto-supervision
278
        self.get_failures()
279

    
280
        user = get_current_user()
281

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

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

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

    
319
        # Pagination des résultats
320
        events.generate_request()
321
        items_per_page = int(session.get('items_per_page', config['vigiboard_items_per_page']))
322
        page = paginate.Page(events.req, page=page,
323
            items_per_page=items_per_page)
324

    
325
        # Vérification que l'événement existe
326
        if not page.item_count:
327
            flash(_('No masked event or access denied'), 'error')
328
            redirect('/')
329

    
330
        return dict(
331
            idcorrevent = idcorrevent,
332
            hostname = hostname,
333
            servicename = servicename,
334
            plugins_data = {},
335
            page = page,
336
            search_form = create_search_form,
337
            search = {},
338
            fixed_search = {},
339
        )
340

    
341

    
342
    class EventSchema(schema.Schema):
343
        """Schéma de validation de la méthode event."""
344
        idevent = validators.Int(not_empty=True)
345
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
346

    
347
    @validate(
348
        validators=EventSchema(),
349
        error_handler = process_form_errors)
350
    @expose('history_table.html')
351
    @require(access_restriction)
352
    def event(self, idevent, page=1):
353
        """
354
        Affichage de l'historique d'un événement brut.
355
        Pour accéder à cette page, l'utilisateur doit être authentifié.
356

357
        @param idevent: identifiant de l'événement brut souhaité.
358
        @type idevent: C{int}
359
        @param page: numéro de la page à afficher.
360
        @type page: C{int}
361

362
        Cette méthode permet de satisfaire l'exigence
363
        VIGILO_EXIG_VIGILO_BAC_0080.
364
        """
365

    
366
        # Auto-supervision
367
        self.get_failures()
368

    
369
        user = get_current_user()
370
        events = VigiboardRequest(user, False)
371
        events.add_table(
372
            Event,
373
            events.items.c.hostname.label('hostname'),
374
            events.items.c.servicename.label('servicename'),
375
        )
376
        events.add_join((EVENTSAGGREGATE_TABLE, \
377
            EVENTSAGGREGATE_TABLE.c.idevent == Event.idevent))
378
        events.add_join((CorrEvent, CorrEvent.idcorrevent == \
379
            EVENTSAGGREGATE_TABLE.c.idcorrevent))
380
        events.add_join((events.items,
381
            Event.idsupitem == events.items.c.idsupitem))
382
        events.add_filter(Event.idevent == idevent)
383

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

    
388
        events.format_events(0, 1)
389
        events.generate_tmpl_context()
390
        history = events.format_history()
391

    
392
        # Pagination des résultats
393
        items_per_page = int(session.get('items_per_page', config['vigiboard_items_per_page']))
394
        page = paginate.Page(history, page=page, items_per_page=items_per_page)
395
        event = events.req[0]
396

    
397
        return dict(
398
            idevent = idevent,
399
            hostname = event.hostname,
400
            servicename = event.servicename,
401
            plugins_data = {},
402
            page = page,
403
            search_form = create_search_form,
404
            search = {},
405
            fixed_search = {},
406
        )
407

    
408

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

    
415
        # Paramètres de tri
416
        sort = validators.String(if_missing=None)
417
        order = validators.OneOf(['asc', 'desc'], if_missing='asc')
418

    
419
        # L'hôte / service dont on doit afficher les évènements
420
        host = validators.String(not_empty=True)
421
        service = validators.String(if_missing=None)
422

    
423
    @validate(
424
        validators=ItemSchema(),
425
        error_handler = process_form_errors)
426
    @expose('events_table.html')
427
    @require(access_restriction)
428
    def item(self, page=1, host=None, service=None, sort=None, order=None):
429
        """
430
        Affichage de l'historique de l'ensemble des événements corrélés
431
        jamais ouverts sur l'hôte / service demandé.
432
        Pour accéder à cette page, l'utilisateur doit être authentifié.
433

434
        @param page: Numéro de la page à afficher.
435
        @type: C{int}
436
        @param host: Nom de l'hôte souhaité.
437
        @type: C{str}
438
        @param service: Nom du service souhaité
439
        @type: C{str}
440
        @param sort: Colonne de tri
441
        @type: C{str} or C{None}
442
        @param order: Ordre du tri (asc ou desc)
443
        @type: C{str} or C{None}
444

445
        Cette méthode permet de satisfaire l'exigence
446
        VIGILO_EXIG_VIGILO_BAC_0080.
447
        """
448

    
449
        # Auto-supervision
450
        self.get_failures()
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, sort=sort, order=order)
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
        # Pagination des résultats
470
        aggregates.generate_request()
471
        items_per_page = int(session.get('items_per_page', config['vigiboard_items_per_page']))
472
        page = paginate.Page(aggregates.req, page=page,
473
            items_per_page=items_per_page)
474

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

    
480
        # Ajout des formulaires et préparation
481
        # des données pour ces formulaires.
482
        ids_events = [event[0].idcause for event in page.items]
483
        tmpl_context.last_modification = calendar.timegm(
484
            get_last_modification_timestamp(ids_events).timetuple())
485

    
486
        tmpl_context.edit_event_form = EditEventForm("edit_event_form",
487
            submit_text=_('Apply'), action=url('/update'))
488

    
489
        plugins_data = {}
490
        for plugin in dict(config['columns_plugins']):
491
            plugins_data[plugin] = {}
492

    
493
        return dict(
494
            hostname = host,
495
            servicename = service,
496
            plugins_data = plugins_data,
497
            page = page,
498
            sort = sort,
499
            order = order,
500
            event_edit_status_options = edit_event_status_options,
501
            search_form = create_search_form,
502
            search = {},
503
            fixed_search = {},
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(config.is_manager,
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 = [ int(i) for i in id.strip(',').split(',') ]
553

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

    
567
        events.generate_request()
568
        idevents = [event[0].idcause for event in events.req]
569

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

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

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

    
597
        # Si un module de gestion de ticket est utilisé,
598
        # il a la possibilité de changer à la volée le libellé du ticket.
599
        if self._tickets:
600
            trouble_ticket = self._tickets.createTicket(events.req, trouble_ticket)
601

    
602
        # Définit 2 mappings dont les ensembles sont disjoincts
603
        # pour basculer entre la représentation en base de données
604
        # et la représentation "humaine" du bac à événements.
605
        ack_mapping = {
606
            # Permet d'associer la valeur dans le widget ToscaWidgets
607
            # (cf. vigiboard.widgets.edit_event.edit_event_status_options)
608
            # avec la valeur dans la base de données.
609
            u'None': CorrEvent.ACK_NONE,
610
            u'Acknowledged': CorrEvent.ACK_KNOWN,
611
            u'AAClosed': CorrEvent.ACK_CLOSED,
612

    
613
            # Permet d'afficher un libellé plus sympathique pour l'utilisateur
614
            # représentant l'état d'acquittement stocké en base de données.
615
            CorrEvent.ACK_NONE: l_('None'),
616
            CorrEvent.ACK_KNOWN: l_('Acknowledged'),
617
            CorrEvent.ACK_CLOSED: l_('Acknowledged and closed'),
618
        }
619

    
620
        # Modification des événements et création d'un historique
621
        # chaque fois que cela est nécessaire.
622
        for data in events.req:
623
            event = data[0]
624
            if trouble_ticket and trouble_ticket != event.trouble_ticket:
625
                history = EventHistory(
626
                        type_action=u"Ticket change",
627
                        idevent=event.idcause,
628
                        value=unicode(trouble_ticket),
629
                        text="Changed trouble ticket from '%(from)s' "
630
                             "to '%(to)s'" % {
631
                            'from': event.trouble_ticket,
632
                            'to': trouble_ticket,
633
                        },
634
                        username=user.user_name,
635
                        timestamp=datetime.utcnow(),
636
                    )
637
                DBSession.add(history)
638
                LOGGER.info(_('User "%(user)s" (%(address)s) changed the '
639
                            'trouble ticket from "%(previous)s" to "%(new)s" '
640
                            'on event #%(idevent)d') % {
641
                                'user': request.identity['repoze.who.userid'],
642
                                'address': request.remote_addr,
643
                                'previous': event.trouble_ticket,
644
                                'new': trouble_ticket,
645
                                'idevent': event.idcause,
646
                            })
647
                event.trouble_ticket = trouble_ticket
648

    
649
            # Changement du statut d'acquittement.
650
            if ack != u'NoChange':
651
                changed_ack = ack
652
                # Pour forcer l'acquittement d'un événement,
653
                # il faut en plus avoir la permission
654
                # "vigiboard-admin".
655
                if ack == u'Forced':
656
                    changed_ack = u'AAClosed'
657
                    cause = event.cause
658
                    # On met systématiquement l'événement à l'état "OK",
659
                    # même s'il s'agit d'un hôte.
660
                    # Techniquement, c'est incorrect, mais on fait ça
661
                    # pour masquer l'événement de toutes façons...
662
                    cause.current_state = \
663
                        StateName.statename_to_value(u'OK')
664

    
665
                    # Mise à jour de l'état dans State, pour que
666
                    # VigiMap soit également mis à jour.
667
                    DBSession.query(State).filter(
668
                            State.idsupitem == cause.idsupitem,
669
                        ).update({
670
                            'state': StateName.statename_to_value(u'OK'),
671
                        })
672

    
673
                    history = EventHistory(
674
                            type_action=u"Forced change state",
675
                            idevent=event.idcause,
676
                            value=u'OK',
677
                            text="Forced state to 'OK'",
678
                            username=user.user_name,
679
                            timestamp=datetime.utcnow(),
680
                            state=StateName.statename_to_value(u'OK'),
681
                        )
682
                    DBSession.add(history)
683
                    LOGGER.info(_('User "%(user)s" (%(address)s) forcefully '
684
                                'closed event #%(idevent)d') % {
685
                                    'user': request. \
686
                                            identity['repoze.who.userid'],
687
                                    'address': request.remote_addr,
688
                                    'idevent': event.idcause,
689
                                })
690

    
691
                # Convertit la valeur du widget ToscaWidgets
692
                # vers le code interne puis vers un libellé
693
                # "humain".
694
                ack_label = ack_mapping[ack_mapping[changed_ack]]
695

    
696
                # Si le changement a été forcé,
697
                # on veut le mettre en évidence.
698
                if ack == u'Forced':
699
                    history_label = u'Forced'
700
                else:
701
                    history_label = ack_label
702

    
703
                history = EventHistory(
704
                        type_action=u"Acknowledgement change state",
705
                        idevent=event.idcause,
706
                        value=unicode(history_label),
707
                        text=u"Changed acknowledgement status "
708
                            u"from '%s' to '%s'" % (
709
                            ack_mapping[event.ack],
710
                            ack_label,
711
                        ),
712
                        username=user.user_name,
713
                        timestamp=datetime.utcnow(),
714
                    )
715
                DBSession.add(history)
716
                LOGGER.info(_('User "%(user)s" (%(address)s) changed the state '
717
                            'from "%(previous)s" to "%(new)s" on event '
718
                            '#%(idevent)d') % {
719
                                'user': request.identity['repoze.who.userid'],
720
                                'address': request.remote_addr,
721
                                'previous': _(ack_mapping[event.ack]),
722
                                'new': _(ack_label),
723
                                'idevent': event.idcause,
724
                            })
725
                event.ack = ack_mapping[changed_ack]
726

    
727
        DBSession.flush()
728
        flash(_('Updated successfully'))
729
        redirect(request.environ.get('HTTP_REFERER', '/'))
730

    
731

    
732
    class GetPluginValueSchema(schema.Schema):
733
        """Schéma de validation de la méthode get_plugin_value."""
734
        idcorrevent = validators.Int(not_empty=True)
735
        plugin_name = validators.String(not_empty=True)
736
        # Permet de passer des paramètres supplémentaires au plugin.
737
        allow_extra_fields = True
738

    
739
    @validate(
740
        validators=GetPluginValueSchema(),
741
        error_handler = handle_validation_errors_json)
742
    @expose('json')
743
    @require(access_restriction)
744
    def plugin_json(self, idcorrevent, plugin_name, *arg, **krgv):
745
        """
746
        Permet de récupérer la valeur d'un plugin associée à un CorrEvent
747
        donné via JSON.
748
        """
749

    
750
        # Vérification de l'existence du plugin
751
        plugins = dict(config['columns_plugins'])
752
        if plugin_name not in plugins:
753
            raise HTTPNotFound(_("No such plugin '%s'") % plugin_name)
754

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

    
758
        # Filtrage des évènements en fonction des permissions de
759
        # l'utilisateur (s'il n'appartient pas au groupe 'managers')
760
        if not config.is_manager.is_met(request.environ):
761
            user = get_current_user()
762

    
763
            events = events.join(
764
                (Event, Event.idevent == CorrEvent.idcause),
765
            ).outerjoin(
766
                (LowLevelService, LowLevelService.idservice == Event.idsupitem),
767
            ).join(
768
                (SUPITEM_GROUP_TABLE,
769
                    or_(
770
                        SUPITEM_GROUP_TABLE.c.idsupitem == \
771
                            LowLevelService.idhost,
772
                        SUPITEM_GROUP_TABLE.c.idsupitem == \
773
                            Event.idsupitem,
774
                    )
775
                ),
776
            ).join(
777
                (GroupHierarchy,
778
                    GroupHierarchy.idchild == SUPITEM_GROUP_TABLE.c.idgroup),
779
            ).join(
780
                (DataPermission,
781
                    DataPermission.idgroup == GroupHierarchy.idparent),
782
            ).join(
783
                (USER_GROUP_TABLE,
784
                    USER_GROUP_TABLE.c.idgroup == DataPermission.idusergroup),
785
            ).filter(USER_GROUP_TABLE.c.username == user.user_name)
786

    
787
        # Filtrage des évènements en fonction
788
        # de l'identifiant passé en paramètre
789
        events = events.filter(CorrEvent.idcorrevent == idcorrevent).count()
790

    
791
        # Pas d'événement ou permission refusée. On ne distingue pas
792
        # les 2 cas afin d'éviter la divulgation d'informations.
793
        if events == 0:
794
            raise HTTPNotFound(_('No such incident or insufficient '
795
                                'permissions'))
796

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

    
802
    @validate(validators={
803
        "fontsize": validators.Regex(
804
            r'[0-9]+(pt|px|em|%)',
805
            regexOps = ('I',)
806
        )}, error_handler = handle_validation_errors_json)
807
    @expose('json')
808
    def set_fontsize(self, fontsize):
809
        """Enregistre la taille de la police dans les préférences."""
810
        session['fontsize'] = fontsize
811
        session.save()
812
        return dict()
813

    
814
    @validate(validators={"refresh": validators.Int()},
815
            error_handler = handle_validation_errors_json)
816
    @expose('json')
817
    def set_refresh(self, refresh):
818
        """Enregistre le temps de rafraichissement dans les préférences."""
819
        session['refresh'] = bool(refresh)
820
        session.save()
821
        return dict()
822

    
823
    @expose('json')
824
    def set_theme(self, theme):
825
        """Enregistre le thème à utiliser dans les préférences."""
826
        # On sauvegarde l'ID du thème sans vérifications
827
        # car les thèmes (styles CSS) sont définies dans
828
        # les packages de thèmes (ex: vigilo-themes-default).
829
        # La vérification de la valeur est faite dans les templates.
830
        session['theme'] = theme
831
        session.save()
832
        return dict()
833

    
834
    @validate(validators={"items": validators.Int()},
835
            error_handler = handle_validation_errors_json)
836
    @expose('json')
837
    def set_items_per_page(self, items):
838
        """Enregistre le nombre d'alertes par page dans les préférences."""
839
        session['items_per_page'] = items
840
        session.save()
841
        return dict()
842

    
843
    @require(access_restriction)
844
    @expose('json')
845
    def get_groups(self, parent_id=None, onlytype="", offset=0, noCache=None):
846
        """
847
        Affiche un étage de l'arbre de
848
        sélection des hôtes et groupes d'hôtes.
849

850
        @param parent_id: identifiant du groupe d'hôte parent
851
        @type  parent_id: C{int} or None
852
        """
853

    
854
        # Si l'identifiant du groupe parent n'est pas
855
        # spécifié, on retourne la liste des groupes
856
        # racines, fournie par la méthode get_root_groups.
857
        if parent_id is None:
858
            return self.get_root_groups()
859

    
860
        # TODO: Utiliser un schéma de validation
861
        parent_id = int(parent_id)
862
        offset = int(offset)
863

    
864
        # On récupère la liste des groupes de supitems dont
865
        # l'identifiant du parent est passé en paramètre.
866
        supitem_groups = DBSession.query(
867
                SupItemGroup.idgroup,
868
                SupItemGroup.name,
869
            ).join(
870
                (GroupHierarchy,
871
                    GroupHierarchy.idchild == SupItemGroup.idgroup),
872
            ).filter(GroupHierarchy.idparent == parent_id
873
            ).filter(GroupHierarchy.hops == 1
874
            ).order_by(SupItemGroup.name)
875

    
876
        # Si l'utilisateur n'appartient pas au groupe 'managers',
877
        # on filtre les résultats en fonction de ses permissions.
878
        if not config.is_manager.is_met(request.environ):
879
            user = get_current_user()
880
            GroupHierarchy_aliased = aliased(GroupHierarchy,
881
                name='GroupHierarchy_aliased')
882
            supitem_groups = supitem_groups.join(
883
                (GroupHierarchy_aliased,
884
                    or_(
885
                        GroupHierarchy_aliased.idchild == SupItemGroup.idgroup,
886
                        GroupHierarchy_aliased.idparent == SupItemGroup.idgroup
887
                    )),
888
                (DataPermission,
889
                    or_(
890
                        DataPermission.idgroup == \
891
                            GroupHierarchy_aliased.idparent,
892
                        DataPermission.idgroup == \
893
                            GroupHierarchy_aliased.idchild,
894
                    )),
895
                (USER_GROUP_TABLE, USER_GROUP_TABLE.c.idgroup == \
896
                    DataPermission.idusergroup),
897
            ).filter(USER_GROUP_TABLE.c.username == user.user_name)
898

    
899
        limit = int(config.get("max_menu_entries", 20))
900
        result = {"groups": [], "items": []}
901
        num_children_left = supitem_groups.distinct().count() - offset
902
        if offset:
903
            result["continued_from"] = offset
904
            result["continued_type"] = "group"
905
        all_grs = supitem_groups.distinct().limit(limit).offset(offset).all()
906
        for group in all_grs:
907
            result["groups"].append({
908
                'id'   : group.idgroup,
909
                'name' : group.name,
910
                'type' : "group",
911
            })
912
        if num_children_left > limit:
913
            result["groups"].append({
914
                'name': _("Next %(limit)s") % {"limit": limit},
915
                'offset': offset + limit,
916
                'parent_id': parent_id,
917
                'type': 'continued',
918
                'for_type': 'group',
919
            })
920

    
921
        return result
922

    
923
    def get_root_groups(self):
924
        """
925
        Retourne tous les groupes racines (c'est à dire n'ayant
926
        aucun parent) d'hôtes auquel l'utilisateur a accès.
927

928
        @return: Un dictionnaire contenant la liste de ces groupes.
929
        @rtype : C{dict} of C{list} of C{dict} of C{mixed}
930
        """
931

    
932
        # On récupère tous les groupes qui ont un parent.
933
        children = DBSession.query(
934
            SupItemGroup,
935
        ).distinct(
936
        ).join(
937
            (GroupHierarchy, GroupHierarchy.idchild == SupItemGroup.idgroup)
938
        ).filter(GroupHierarchy.hops > 0)
939

    
940
        # Ensuite on les exclut de la liste des groupes,
941
        # pour ne garder que ceux qui sont au sommet de
942
        # l'arbre et qui constituent nos "root groups".
943
        root_groups = DBSession.query(
944
            SupItemGroup,
945
        ).except_(children
946
        ).order_by(SupItemGroup.name)
947

    
948
        # On filtre ces groupes racines afin de ne
949
        # retourner que ceux auquels l'utilisateur a accès
950
        user = get_current_user()
951
        if not config.is_manager.is_met(request.environ):
952
            root_groups = root_groups.join(
953
                (GroupHierarchy,
954
                    GroupHierarchy.idparent == SupItemGroup.idgroup),
955
                (DataPermission,
956
                    DataPermission.idgroup == GroupHierarchy.idchild),
957
                (USER_GROUP_TABLE, USER_GROUP_TABLE.c.idgroup == \
958
                    DataPermission.idusergroup),
959
            ).filter(USER_GROUP_TABLE.c.username == user.user_name)
960

    
961
        groups = []
962
        for group in root_groups.all():
963
            groups.append({
964
                'id'   : group.idgroup,
965
                'name' : group.name,
966
                'type' : "group",
967
            })
968

    
969
        return dict(groups=groups, items=[])
970

    
971
def get_last_modification_timestamp(event_id_list,
972
                                    value_if_none=datetime.utcnow):
973
    """
974
    Récupère le timestamp de la dernière modification
975
    opérée sur l'un des événements dont l'identifiant
976
    fait partie de la liste passée en paramètre.
977
    """
978
    if not event_id_list:
979
        last_modification_timestamp = None
980
    else:
981
        last_modification_timestamp = DBSession.query(
982
                                func.max(EventHistory.timestamp),
983
                         ).filter(EventHistory.idevent.in_(event_id_list)
984
                         ).scalar()
985

    
986
    if not last_modification_timestamp:
987
        if not callable(value_if_none):
988
            return value_if_none
989
        else:
990
            last_modification_timestamp = value_if_none()
991
    return last_modification_timestamp