Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

vigiboard / vigiboard / controllers / root.py @ f6ecd27a

History | View | Annotate | Download (39.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.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.hostname,
176
            aggregates.items.c.servicename
177
        )
178
        aggregates.add_join((Event, CorrEvent.idcause == Event.idevent))
179
        aggregates.add_contains_eager(CorrEvent.cause)
180
        aggregates.add_group_by(Event)
181
        aggregates.add_join((aggregates.items,
182
            Event.idsupitem == aggregates.items.c.idsupitem))
183
        aggregates.add_order_by(asc(aggregates.items.c.hostname))
184

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

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

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

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

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

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

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

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

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

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

    
254

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

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

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

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

    
279
        user = get_current_user()
280

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

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

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

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

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

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

    
340

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

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

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

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

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

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

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

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

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

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

    
407

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

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

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

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

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

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

    
448
        # Auto-supervision
449
        self.get_failures()
450

    
451
        idsupitem = SupItem.get_supitem(host, service)
452
        if not idsupitem:
453
            flash(_('No such host/service'), 'error')
454
            redirect('/')
455

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

    
468
        # Pagination des résultats
469
        aggregates.generate_request()
470
        items_per_page = int(session.get('items_per_page', config['vigiboard_items_per_page']))
471
        page = paginate.Page(aggregates.req, page=page,
472
            items_per_page=items_per_page)
473

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

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

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

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

    
492
        return dict(
493
            hostname = host,
494
            servicename = service,
495
            plugins_data = plugins_data,
496
            page = page,
497
            sort = sort,
498
            order = order,
499
            event_edit_status_options = edit_event_status_options,
500
            search_form = create_search_form,
501
            search = {},
502
            fixed_search = {},
503
        )
504

    
505

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
730

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
920
        return result
921

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

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

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

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

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

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

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

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

    
985
    if not last_modification_timestamp:
986
        if not value_if_none:
987
            return None
988
        else:
989
            last_modification_timestamp = value_if_none
990
    return datetime.fromtimestamp(mktime(
991
        last_modification_timestamp.timetuple()))