Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

vigiboard / vigiboard / controllers / root.py @ c6e081ce

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
        modules = (
265
            (conf['package'].__name__, localedir),
266
            ('vigilo-themes', resource_filename('vigilo.themes.i18n', '')),
267
        )
268

    
269
        # Charge et installe le fichier JS de traduction de chaque module
270
        translations = "babel.Translations.load("
271
        for domain, directory in modules:
272
            try:
273
                mofile = gettext.find(domain, directory, languages=lang)
274
                if mofile is None:
275
                    continue
276

    
277
                fhandle = open(mofile[:-3] + '.js', 'r')
278
                translations += fhandle.read()
279
                fhandle.close()
280
                translations += ").load("
281
            except ImportError:
282
                pass
283
        translations += "{}).install()"
284

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

    
288

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

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

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

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

    
313
        user = get_current_user()
314

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

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

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

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

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

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

    
374

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

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

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

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

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

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

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

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

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

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

    
441

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
539

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
764

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
954
        return result
955

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

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

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

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

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

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

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

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

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