Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

vigiboard / vigiboard / controllers / root.py @ d5a41c9b

History | View | Annotate | Download (41.1 KB)

1
# -*- coding: utf-8 -*-
2
# vim:set expandtab tabstop=4 shiftwidth=4:
3
################################################################################
4
#
5
# Copyright (C) 2007-2013 CS-SI
6
#
7
# This program is free software; you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License version 2 as
9
# published by the Free Software Foundation.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
19
################################################################################
20

    
21
"""VigiBoard Controller"""
22

    
23
from datetime import datetime
24
from time import mktime
25

    
26
from pkg_resources import resource_filename, working_set
27

    
28
from tg.exceptions import HTTPNotFound
29
from tg import expose, validate, require, flash, url, \
30
    tmpl_context, request, response, config, session, redirect
31
from webhelpers import paginate
32
from tw.forms import validators
33
from pylons.i18n import ugettext as _, lazy_ugettext as l_, get_lang
34
from sqlalchemy import asc
35
from sqlalchemy.sql import func
36
from sqlalchemy.orm import aliased
37
from sqlalchemy.sql.expression import or_
38
from repoze.what.predicates import Any, All, NotAuthorizedError, \
39
                                    has_permission, not_anonymous
40
from formencode import schema
41

    
42
from vigilo.models.session import DBSession
43
from vigilo.models.tables import Event, EventHistory, CorrEvent, Host, \
44
                                    SupItem, SupItemGroup, LowLevelService, \
45
                                    StateName, State, DataPermission
46
from vigilo.models.tables.grouphierarchy import GroupHierarchy
47
from vigilo.models.tables.secondary_tables import EVENTSAGGREGATE_TABLE, \
48
        USER_GROUP_TABLE, SUPITEM_GROUP_TABLE
49

    
50
from vigilo.turbogears.controllers.auth import AuthController
51
from vigilo.turbogears.controllers.selfmonitoring import SelfMonitoringController
52
from vigilo.turbogears.controllers.custom import CustomController
53
from vigilo.turbogears.controllers.error import ErrorController
54
from vigilo.turbogears.controllers.autocomplete import AutoCompleteController
55
from vigilo.turbogears.controllers.proxy import ProxyController
56
from vigilo.turbogears.controllers.api.root import ApiRootController
57
from vigilo.turbogears.helpers import get_current_user
58

    
59
from vigiboard.controllers.vigiboardrequest import VigiboardRequest
60
from vigiboard.controllers.feeds import FeedsController
61
from vigiboard.controllers.silence import SilenceController
62

    
63
from vigiboard.lib import export_csv, dateformat
64
from vigiboard.widgets.edit_event import edit_event_status_options, \
65
                                            EditEventForm
66
from vigiboard.widgets.search_form import create_search_form
67
import logging
68

    
69
LOGGER = logging.getLogger(__name__)
70

    
71
__all__ = ('RootController', 'get_last_modification_timestamp',
72
           'date_to_timestamp')
73

    
74
# pylint: disable-msg=R0201,W0613,W0622
75
# R0201: Method could be a function
76
# W0613: Unused arguments: les arguments sont la query-string
77
# W0622: Redefining built-in 'id': élément de la query-string
78

    
79
class RootController(AuthController, SelfMonitoringController):
80
    """
81
    Le controller général de vigiboard
82
    """
83
    _tickets = None
84

    
85
    error = ErrorController()
86
    autocomplete = AutoCompleteController()
87
    nagios = ProxyController('nagios', '/nagios/',
88
        not_anonymous(l_('You need to be authenticated')))
89
    api = ApiRootController()
90
    feeds = FeedsController()
91
    silence = SilenceController()
92
    custom = CustomController()
93

    
94
    # Prédicat pour la restriction de l'accès aux interfaces.
95
    # L'utilisateur doit avoir la permission "vigiboard-access"
96
    # ou appartenir au groupe "managers" pour accéder à VigiBoard.
97
    access_restriction = All(
98
        not_anonymous(msg=l_("You need to be authenticated")),
99
        Any(config.is_manager,
100
            has_permission('vigiboard-access'),
101
            msg=l_("You don't have access to VigiBoard"))
102
    )
103

    
104
    def process_form_errors(self, *argv, **kwargv):
105
        """
106
        Gestion des erreurs de validation : on affiche les erreurs
107
        puis on redirige vers la dernière page accédée.
108
        """
109
        for k in tmpl_context.form_errors:
110
            flash("'%s': %s" % (k, tmpl_context.form_errors[k]), 'error')
111
        redirect(request.environ.get('HTTP_REFERER', '/'))
112

    
113
    @expose('json')
114
    def handle_validation_errors_json(self, *args, **kwargs):
115
        kwargs['errors'] = tmpl_context.form_errors
116
        return dict(kwargs)
117

    
118
    def __init__(self, *args, **kwargs):
119
        """Initialisation du contrôleur."""
120
        super(RootController, self).__init__(*args, **kwargs)
121
        # Si un module de gestion des tickets a été indiqué dans
122
        # le fichier de configuration, on tente de le charger.
123
        if config.get('tickets.plugin'):
124
            plugins = working_set.iter_entry_points('vigiboard.tickets', config['tickets.plugin'])
125
            if plugins:
126
                # La classe indiquée par la première valeur de l'itérateur
127
                # correspond au plugin que l'on veut instancier.
128
                pluginCls = plugins.next().load()
129
                self._tickets = pluginCls()
130

    
131
    class IndexSchema(schema.Schema):
132
        """Schéma de validation de la méthode index."""
133
        # Si on ne passe pas le paramètre "page" ou qu'on passe une valeur
134
        # invalide ou pas de valeur du tout, alors on affiche la 1ère page.
135
        page = validators.Int(
136
            min=1,
137
            if_missing=1,
138
            if_invalid=1,
139
            not_empty=True
140
        )
141

    
142
        # Paramètres de tri
143
        sort = validators.String(if_missing=None)
144
        order = validators.OneOf(['asc', 'desc'], if_missing='asc')
145

    
146
        # Nécessaire pour que les critères de recherche soient conservés.
147
        allow_extra_fields = True
148

    
149
        # 2ème validation, cette fois avec les champs
150
        # du formulaire de recherche.
151
        chained_validators = [create_search_form.validator]
152

    
153
    @validate(
154
        validators=IndexSchema(),
155
        error_handler = process_form_errors)
156
    @expose('events_table.html')
157
    @expose('events_table.html', content_type='text/csv')
158
    @require(access_restriction)
159
    def index(self, page, sort=None, order=None, **search):
160
        """
161
        Page d'accueil de Vigiboard. Elle affiche, suivant la page demandée
162
        (page 1 par defaut), la liste des événements, rangés par ordre de prise
163
        en compte, puis de sévérité.
164
        Pour accéder à cette page, l'utilisateur doit être authentifié.
165

166
        @param page: Numéro de la page souhaitée, commence à 1
167
        @type page: C{int}
168
        @param sort: Colonne de tri
169
        @type sort: C{str} or C{None}
170
        @param order: Ordre du tri (asc ou desc)
171
        @type order: C{str} or C{None}
172
        @param search: Dictionnaire contenant les critères de recherche.
173
        @type search: C{dict}
174

175
        Cette méthode permet de satisfaire les exigences suivantes :
176
            - VIGILO_EXIG_VIGILO_BAC_0040,
177
            - VIGILO_EXIG_VIGILO_BAC_0070,
178
            - VIGILO_EXIG_VIGILO_BAC_0100,
179
        """
180

    
181
        # Auto-supervision
182
        self.get_failures()
183

    
184
        user = get_current_user()
185
        aggregates = VigiboardRequest(user, search=search, sort=sort, order=order)
186

    
187
        aggregates.add_table(
188
            CorrEvent,
189
            aggregates.items.c.hostname,
190
            aggregates.items.c.servicename
191
        )
192
        aggregates.add_join((Event, CorrEvent.idcause == Event.idevent))
193
        aggregates.add_contains_eager(CorrEvent.cause)
194
        aggregates.add_group_by(Event)
195
        aggregates.add_join((aggregates.items,
196
            Event.idsupitem == aggregates.items.c.idsupitem))
197
        aggregates.add_order_by(asc(aggregates.items.c.hostname))
198

    
199
        # Certains arguments sont réservés dans routes.util.url_for().
200
        # On effectue les substitutions adéquates.
201
        # Par exemple: "host" devient "host_".
202
        reserved = ('host', 'anchor', 'protocol', 'qualified')
203
        for column in search.copy():
204
            if column in reserved:
205
                search[column + '_'] = search[column]
206
                del search[column]
207

    
208
        # On ne garde que les champs effectivement renseignés.
209
        for column in search.copy():
210
            if not search[column]:
211
                del search[column]
212

    
213
        # On sérialise les champs de type dict.
214
        def serialize_dict(dct, key):
215
            if isinstance(dct[key], dict):
216
                for subkey in dct[key]:
217
                    serialize_dict(dct[key], subkey)
218
                    dct['%s.%s' % (key, subkey)] = dct[key][subkey]
219
                del dct[key]
220
            elif isinstance(dct[key], datetime):
221
                dct[key] = dct[key].strftime(dateformat.get_date_format())
222
        fixed_search = search.copy()
223
        for column in fixed_search.copy():
224
            serialize_dict(fixed_search, column)
225

    
226
        # Pagination des résultats
227
        aggregates.generate_request()
228
        items_per_page = int(config['vigiboard_items_per_page'])
229
        page = paginate.Page(aggregates.req, page=page,
230
            items_per_page=items_per_page)
231

    
232
        # Récupération des données des plugins
233
        plugins_data = {}
234
        plugins = dict(config['columns_plugins'])
235

    
236
        ids_events = [event[0].idcause for event in page.items]
237
        ids_correvents = [event[0].idcorrevent for event in page.items]
238
        for plugin in plugins:
239
            plugin_data = plugins[plugin].get_bulk_data(ids_correvents)
240
            if plugin_data:
241
                plugins_data[plugin] = plugin_data
242
            else:
243
                plugins_data[plugin] = {}
244

    
245
        # Ajout des formulaires et préparation
246
        # des données pour ces formulaires.
247
        tmpl_context.last_modification = \
248
            mktime(get_last_modification_timestamp(ids_events).timetuple())
249

    
250
        tmpl_context.edit_event_form = EditEventForm("edit_event_form",
251
            submit_text=_('Apply'), action=url('/update'))
252

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

    
259
            response.headers['Content-Disposition'] = \
260
                            'attachment;filename="alerts.csv"'
261
            return export_csv.export(page, plugins_data)
262

    
263
        return dict(
264
            hostname = None,
265
            servicename = None,
266
            plugins_data = plugins_data,
267
            page = page,
268
            sort = sort,
269
            order = order,
270
            event_edit_status_options = edit_event_status_options,
271
            search_form = create_search_form,
272
            search = search,
273
            fixed_search = fixed_search,
274
        )
275

    
276

    
277
    @expose()
278
    def i18n(self):
279
        import gettext
280
        import pylons
281
        import os.path
282

    
283
        # Repris de pylons.i18n.translation:_get_translator.
284
        conf = pylons.config.current_conf()
285
        try:
286
            rootdir = conf['pylons.paths']['root']
287
        except KeyError:
288
            rootdir = conf['pylons.paths'].get('root_path')
289
        localedir = os.path.join(rootdir, 'i18n')
290

    
291
        lang = get_lang()
292

    
293
        # Localise le fichier *.mo actuellement chargé
294
        # et génère le chemin jusqu'au *.js correspondant.
295
        filename = gettext.find(conf['pylons.package'], localedir,
296
            languages=lang)
297
        js = filename[:-3] + '.js'
298

    
299
        themes_filename = gettext.find(
300
            'vigilo-themes',
301
            resource_filename('vigilo.themes.i18n', ''),
302
            languages=lang)
303
        themes_js = themes_filename[:-3] + '.js'
304

    
305
        # Récupère et envoie le contenu du fichier de traduction *.js.
306
        fhandle = open(js, 'r')
307
        translations = fhandle.read()
308
        fhandle.close()
309

    
310
        fhandle = open(themes_js, 'r')
311
        translations += fhandle.read()
312
        fhandle.close()
313
        return translations
314

    
315

    
316
    class MaskedEventsSchema(schema.Schema):
317
        """Schéma de validation de la méthode masked_events."""
318
        idcorrevent = validators.Int(not_empty=True)
319
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
320

    
321
    @validate(
322
        validators=MaskedEventsSchema(),
323
        error_handler = process_form_errors)
324
    @expose('raw_events_table.html')
325
    @require(access_restriction)
326
    def masked_events(self, idcorrevent, page):
327
        """
328
        Affichage de la liste des événements bruts masqués d'un événement
329
        corrélé (événements agrégés dans l'événement corrélé).
330

331
        @param page: numéro de la page à afficher.
332
        @type  page: C{int}
333
        @param idcorrevent: identifiant de l'événement corrélé souhaité.
334
        @type  idcorrevent: C{int}
335
        """
336

    
337
        # Auto-supervision
338
        self.get_failures()
339

    
340
        user = get_current_user()
341

    
342
        # Récupère la liste des événements masqués de l'événement
343
        # corrélé donné par idcorrevent.
344
        events = VigiboardRequest(user, False)
345
        events.add_table(
346
            Event,
347
            events.items.c.hostname,
348
            events.items.c.servicename,
349
        )
350
        events.add_join((EVENTSAGGREGATE_TABLE, \
351
            EVENTSAGGREGATE_TABLE.c.idevent == Event.idevent))
352
        events.add_join((CorrEvent, CorrEvent.idcorrevent == \
353
            EVENTSAGGREGATE_TABLE.c.idcorrevent))
354
        events.add_join((events.items,
355
            Event.idsupitem == events.items.c.idsupitem))
356
        events.add_filter(Event.idevent != CorrEvent.idcause)
357
        events.add_filter(CorrEvent.idcorrevent == idcorrevent)
358

    
359
        # Récupère l'instance de SupItem associé à la cause de
360
        # l'événement corrélé. Cette instance est utilisé pour
361
        # obtenir le nom d'hôte/service auquel la cause est
362
        # rattachée (afin de fournir un contexte à l'utilisateur).
363
        hostname = None
364
        servicename = None
365
        cause_supitem = DBSession.query(
366
                SupItem,
367
            ).join(
368
                (Event, Event.idsupitem == SupItem.idsupitem),
369
                (CorrEvent, Event.idevent == CorrEvent.idcause),
370
            ).filter(CorrEvent.idcorrevent == idcorrevent
371
            ).one()
372

    
373
        if isinstance(cause_supitem, LowLevelService):
374
            hostname = cause_supitem.host.name
375
            servicename = cause_supitem.servicename
376
        elif isinstance(cause_supitem, Host):
377
            hostname = cause_supitem.name
378

    
379
        # Pagination des résultats
380
        events.generate_request()
381
        items_per_page = int(config['vigiboard_items_per_page'])
382
        page = paginate.Page(events.req, page=page,
383
            items_per_page=items_per_page)
384

    
385
        # Vérification que l'événement existe
386
        if not page.item_count:
387
            flash(_('No masked event or access denied'), 'error')
388
            redirect('/')
389

    
390
        return dict(
391
            idcorrevent = idcorrevent,
392
            hostname = hostname,
393
            servicename = servicename,
394
            plugins_data = {},
395
            page = page,
396
            search_form = create_search_form,
397
            search = {},
398
            fixed_search = {},
399
        )
400

    
401

    
402
    class EventSchema(schema.Schema):
403
        """Schéma de validation de la méthode event."""
404
        idevent = validators.Int(not_empty=True)
405
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
406

    
407
    @validate(
408
        validators=EventSchema(),
409
        error_handler = process_form_errors)
410
    @expose('history_table.html')
411
    @require(access_restriction)
412
    def event(self, idevent, page):
413
        """
414
        Affichage de l'historique d'un événement brut.
415
        Pour accéder à cette page, l'utilisateur doit être authentifié.
416

417
        @param idevent: identifiant de l'événement brut souhaité.
418
        @type idevent: C{int}
419
        @param page: numéro de la page à afficher.
420
        @type page: C{int}
421

422
        Cette méthode permet de satisfaire l'exigence
423
        VIGILO_EXIG_VIGILO_BAC_0080.
424
        """
425

    
426
        # Auto-supervision
427
        self.get_failures()
428

    
429
        user = get_current_user()
430
        events = VigiboardRequest(user, False)
431
        events.add_table(
432
            Event,
433
            events.items.c.hostname.label('hostname'),
434
            events.items.c.servicename.label('servicename'),
435
        )
436
        events.add_join((EVENTSAGGREGATE_TABLE, \
437
            EVENTSAGGREGATE_TABLE.c.idevent == Event.idevent))
438
        events.add_join((CorrEvent, CorrEvent.idcorrevent == \
439
            EVENTSAGGREGATE_TABLE.c.idcorrevent))
440
        events.add_join((events.items,
441
            Event.idsupitem == events.items.c.idsupitem))
442
        events.add_filter(Event.idevent == idevent)
443

    
444
        if events.num_rows() != 1:
445
            flash(_('No such event or access denied'), 'error')
446
            redirect('/')
447

    
448
        events.format_events(0, 1)
449
        events.generate_tmpl_context()
450
        history = events.format_history()
451

    
452
        # Pagination des résultats
453
        items_per_page = int(config['vigiboard_items_per_page'])
454
        page = paginate.Page(history, page=page, items_per_page=items_per_page)
455
        event = events.req[0]
456

    
457
        return dict(
458
            idevent = idevent,
459
            hostname = event.hostname,
460
            servicename = event.servicename,
461
            plugins_data = {},
462
            page = page,
463
            search_form = create_search_form,
464
            search = {},
465
            fixed_search = {},
466
        )
467

    
468

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

    
475
        # Paramètres de tri
476
        sort = validators.String(if_missing=None)
477
        order = validators.OneOf(['asc', 'desc'], if_missing='asc')
478

    
479
        # L'hôte / service dont on doit afficher les évènements
480
        host = validators.String(not_empty=True)
481
        service = validators.String(if_missing=None)
482

    
483
    @validate(
484
        validators=ItemSchema(),
485
        error_handler = process_form_errors)
486
    @expose('events_table.html')
487
    @require(access_restriction)
488
    def item(self, page, host, service, sort=None, order=None):
489
        """
490
        Affichage de l'historique de l'ensemble des événements corrélés
491
        jamais ouverts sur l'hôte / service demandé.
492
        Pour accéder à cette page, l'utilisateur doit être authentifié.
493

494
        @param page: Numéro de la page à afficher.
495
        @type: C{int}
496
        @param host: Nom de l'hôte souhaité.
497
        @type: C{str}
498
        @param service: Nom du service souhaité
499
        @type: C{str}
500
        @param sort: Colonne de tri
501
        @type: C{str} or C{None}
502
        @param order: Ordre du tri (asc ou desc)
503
        @type: C{str} or C{None}
504

505
        Cette méthode permet de satisfaire l'exigence
506
        VIGILO_EXIG_VIGILO_BAC_0080.
507
        """
508

    
509
        # Auto-supervision
510
        self.get_failures()
511

    
512
        idsupitem = SupItem.get_supitem(host, service)
513
        if not idsupitem:
514
            flash(_('No such host/service'), 'error')
515
            redirect('/')
516

    
517
        user = get_current_user()
518
        aggregates = VigiboardRequest(user, False, sort=sort, order=order)
519
        aggregates.add_table(
520
            CorrEvent,
521
            aggregates.items.c.hostname,
522
            aggregates.items.c.servicename,
523
        )
524
        aggregates.add_join((Event, CorrEvent.idcause == Event.idevent))
525
        aggregates.add_join((aggregates.items,
526
            Event.idsupitem == aggregates.items.c.idsupitem))
527
        aggregates.add_filter(aggregates.items.c.idsupitem == idsupitem)
528

    
529
        # Pagination des résultats
530
        aggregates.generate_request()
531
        items_per_page = int(config['vigiboard_items_per_page'])
532
        page = paginate.Page(aggregates.req, page=page,
533
            items_per_page=items_per_page)
534

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

    
540
        # Ajout des formulaires et préparation
541
        # des données pour ces formulaires.
542
        ids_events = [event[0].idcause for event in page.items]
543
        tmpl_context.last_modification = \
544
            mktime(get_last_modification_timestamp(ids_events).timetuple())
545

    
546
        tmpl_context.edit_event_form = EditEventForm("edit_event_form",
547
            submit_text=_('Apply'), action=url('/update'))
548

    
549
        plugins_data = {}
550
        for plugin in dict(config['columns_plugins']):
551
            plugins_data[plugin] = {}
552

    
553
        return dict(
554
            hostname = host,
555
            servicename = service,
556
            plugins_data = plugins_data,
557
            page = page,
558
            sort = sort,
559
            order = order,
560
            event_edit_status_options = edit_event_status_options,
561
            search_form = create_search_form,
562
            search = {},
563
            fixed_search = {},
564
        )
565

    
566

    
567
    class UpdateSchema(schema.Schema):
568
        """Schéma de validation de la méthode update."""
569
        id = validators.Regex(r'^[0-9]+(,[0-9]+)*,?$')
570
        last_modification = validators.Number(not_empty=True)
571
        trouble_ticket = validators.String(if_missing='')
572
        ack = validators.OneOf(
573
            [unicode(s[0]) for s in edit_event_status_options],
574
            not_empty=True)
575

    
576
    @validate(
577
        validators=UpdateSchema(),
578
        error_handler = process_form_errors)
579
    @require(
580
        All(
581
            not_anonymous(msg=l_("You need to be authenticated")),
582
            Any(config.is_manager,
583
                has_permission('vigiboard-update'),
584
                msg=l_("You don't have write access to VigiBoard"))
585
        ))
586
    @expose()
587
    def update(self, id, last_modification, trouble_ticket, ack):
588
        """
589
        Mise à jour d'un événement suivant les arguments passés.
590
        Cela peut être un changement de ticket ou un changement de statut.
591

592
        @param id: Le ou les identifiants des événements à traiter
593
        @param last_modification: La date de la dernière modification
594
            dont l'utilisateur est au courant.
595
        @param trouble_ticket: Nouveau numéro du ticket associé.
596
        @param ack: Nouvel état d'acquittement des événements sélectionnés.
597

598
        Cette méthode permet de satisfaire les exigences suivantes :
599
            - VIGILO_EXIG_VIGILO_BAC_0020,
600
            - VIGILO_EXIG_VIGILO_BAC_0060,
601
            - VIGILO_EXIG_VIGILO_BAC_0110.
602
        """
603

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

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

    
614
        user = get_current_user()
615
        events = VigiboardRequest(user)
616
        events.add_table(
617
            CorrEvent,
618
            Event,
619
            events.items.c.hostname,
620
            events.items.c.servicename,
621
        )
622
        events.add_join((Event, CorrEvent.idcause == Event.idevent))
623
        events.add_join((events.items,
624
            Event.idsupitem == events.items.c.idsupitem))
625
        events.add_filter(CorrEvent.idcorrevent.in_(ids))
626

    
627
        events.generate_request()
628
        idevents = [event[0].idcause for event in events.req]
629

    
630
        # Si des changements sont survenus depuis que la
631
        # page est affichée, on en informe l'utilisateur.
632
        last_modification = datetime.fromtimestamp(last_modification)
633
        cur_last_modification = get_last_modification_timestamp(idevents, None)
634
        if cur_last_modification and last_modification < cur_last_modification:
635
            flash(_('Changes have occurred since the page was last displayed, '
636
                    'your changes HAVE NOT been saved.'), 'warning')
637
            raise redirect(request.environ.get('HTTP_REFERER', '/'))
638

    
639
        # Vérification que au moins un des identifiants existe et est éditable
640
        if not events.num_rows():
641
            flash(_('No access to this event'), 'error')
642
            redirect('/')
643

    
644
        if ack == u'Forced':
645
            condition = Any(
646
                config.is_manager,
647
                has_permission('vigiboard-admin'),
648
                msg=l_("You don't have administrative access "
649
                        "to VigiBoard"))
650
            try:
651
                condition.check_authorization(request.environ)
652
            except NotAuthorizedError, e:
653
                reason = unicode(e)
654
                flash(reason, 'error')
655
                raise redirect(request.environ.get('HTTP_REFERER', '/'))
656

    
657
        # Si un module de gestion de ticket est utilisé,
658
        # il a la possibilité de changer à la volée le libellé du ticket.
659
        if self._tickets:
660
            trouble_ticket = self._tickets.createTicket(events.req, trouble_ticket)
661

    
662
        # Définit 2 mappings dont les ensembles sont disjoincts
663
        # pour basculer entre la représentation en base de données
664
        # et la représentation "humaine" du bac à événements.
665
        ack_mapping = {
666
            # Permet d'associer la valeur dans le widget ToscaWidgets
667
            # (cf. vigiboard.widgets.edit_event.edit_event_status_options)
668
            # avec la valeur dans la base de données.
669
            u'None': CorrEvent.ACK_NONE,
670
            u'Acknowledged': CorrEvent.ACK_KNOWN,
671
            u'AAClosed': CorrEvent.ACK_CLOSED,
672

    
673
            # Permet d'afficher un libellé plus sympathique pour l'utilisateur
674
            # représentant l'état d'acquittement stocké en base de données.
675
            CorrEvent.ACK_NONE: l_('None'),
676
            CorrEvent.ACK_KNOWN: l_('Acknowledged'),
677
            CorrEvent.ACK_CLOSED: l_('Acknowledged and closed'),
678
        }
679

    
680
        # Modification des événements et création d'un historique
681
        # chaque fois que cela est nécessaire.
682
        for data in events.req:
683
            event = data[0]
684
            if trouble_ticket and trouble_ticket != event.trouble_ticket:
685
                history = EventHistory(
686
                        type_action=u"Ticket change",
687
                        idevent=event.idcause,
688
                        value=unicode(trouble_ticket),
689
                        text="Changed trouble ticket from '%(from)s' "
690
                             "to '%(to)s'" % {
691
                            'from': event.trouble_ticket,
692
                            'to': trouble_ticket,
693
                        },
694
                        username=user.user_name,
695
                        timestamp=datetime.now(),
696
                    )
697
                DBSession.add(history)
698
                LOGGER.info(_('User "%(user)s" (%(address)s) changed the '
699
                            'trouble ticket from "%(previous)s" to "%(new)s" '
700
                            'on event #%(idevent)d') % {
701
                                'user': request.identity['repoze.who.userid'],
702
                                'address': request.remote_addr,
703
                                'previous': event.trouble_ticket,
704
                                'new': trouble_ticket,
705
                                'idevent': event.idcause,
706
                            })
707
                event.trouble_ticket = trouble_ticket
708

    
709
            # Changement du statut d'acquittement.
710
            if ack != u'NoChange':
711
                changed_ack = ack
712
                # Pour forcer l'acquittement d'un événement,
713
                # il faut en plus avoir la permission
714
                # "vigiboard-admin".
715
                if ack == u'Forced':
716
                    changed_ack = u'AAClosed'
717
                    cause = event.cause
718
                    # On met systématiquement l'événement à l'état "OK",
719
                    # même s'il s'agit d'un hôte.
720
                    # Techniquement, c'est incorrect, mais on fait ça
721
                    # pour masquer l'événement de toutes façons...
722
                    cause.current_state = \
723
                        StateName.statename_to_value(u'OK')
724

    
725
                    # Mise à jour de l'état dans State, pour que
726
                    # VigiMap soit également mis à jour.
727
                    DBSession.query(State).filter(
728
                            State.idsupitem == cause.idsupitem,
729
                        ).update({
730
                            'state': StateName.statename_to_value(u'OK'),
731
                        })
732

    
733
                    history = EventHistory(
734
                            type_action=u"Forced change state",
735
                            idevent=event.idcause,
736
                            value=u'OK',
737
                            text="Forced state to 'OK'",
738
                            username=user.user_name,
739
                            timestamp=datetime.now(),
740
                            state=StateName.statename_to_value(u'OK'),
741
                        )
742
                    DBSession.add(history)
743
                    LOGGER.info(_('User "%(user)s" (%(address)s) forcefully '
744
                                'closed event #%(idevent)d') % {
745
                                    'user': request. \
746
                                            identity['repoze.who.userid'],
747
                                    'address': request.remote_addr,
748
                                    'idevent': event.idcause,
749
                                })
750

    
751
                # Convertit la valeur du widget ToscaWidgets
752
                # vers le code interne puis vers un libellé
753
                # "humain".
754
                ack_label = ack_mapping[ack_mapping[changed_ack]]
755

    
756
                # Si le changement a été forcé,
757
                # on veut le mettre en évidence.
758
                if ack == u'Forced':
759
                    history_label = u'Forced'
760
                else:
761
                    history_label = ack_label
762

    
763
                history = EventHistory(
764
                        type_action=u"Acknowledgement change state",
765
                        idevent=event.idcause,
766
                        value=unicode(history_label),
767
                        text=u"Changed acknowledgement status "
768
                            u"from '%s' to '%s'" % (
769
                            ack_mapping[event.ack],
770
                            ack_label,
771
                        ),
772
                        username=user.user_name,
773
                        timestamp=datetime.now(),
774
                    )
775
                DBSession.add(history)
776
                LOGGER.info(_('User "%(user)s" (%(address)s) changed the state '
777
                            'from "%(previous)s" to "%(new)s" on event '
778
                            '#%(idevent)d') % {
779
                                'user': request.identity['repoze.who.userid'],
780
                                'address': request.remote_addr,
781
                                'previous': _(ack_mapping[event.ack]),
782
                                'new': _(ack_label),
783
                                'idevent': event.idcause,
784
                            })
785
                event.ack = ack_mapping[changed_ack]
786

    
787
        DBSession.flush()
788
        flash(_('Updated successfully'))
789
        redirect(request.environ.get('HTTP_REFERER', '/'))
790

    
791

    
792
    class GetPluginValueSchema(schema.Schema):
793
        """Schéma de validation de la méthode get_plugin_value."""
794
        idcorrevent = validators.Int(not_empty=True)
795
        plugin_name = validators.String(not_empty=True)
796
        # Permet de passer des paramètres supplémentaires au plugin.
797
        allow_extra_fields = True
798

    
799
    @validate(
800
        validators=GetPluginValueSchema(),
801
        error_handler = handle_validation_errors_json)
802
    @expose('json')
803
    @require(access_restriction)
804
    def plugin_json(self, idcorrevent, plugin_name, *arg, **krgv):
805
        """
806
        Permet de récupérer la valeur d'un plugin associée à un CorrEvent
807
        donné via JSON.
808
        """
809

    
810
        # Vérification de l'existence du plugin
811
        plugins = dict(config['columns_plugins'])
812
        if plugin_name not in plugins:
813
            raise HTTPNotFound(_("No such plugin '%s'") % plugin_name)
814

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

    
818
        # Filtrage des évènements en fonction des permissions de
819
        # l'utilisateur (s'il n'appartient pas au groupe 'managers')
820
        if not config.is_manager.is_met(request.environ):
821
            user = get_current_user()
822

    
823
            events = events.join(
824
                (Event, Event.idevent == CorrEvent.idcause),
825
            ).outerjoin(
826
                (LowLevelService, LowLevelService.idservice == Event.idsupitem),
827
            ).join(
828
                (SUPITEM_GROUP_TABLE,
829
                    or_(
830
                        SUPITEM_GROUP_TABLE.c.idsupitem == \
831
                            LowLevelService.idhost,
832
                        SUPITEM_GROUP_TABLE.c.idsupitem == \
833
                            Event.idsupitem,
834
                    )
835
                ),
836
            ).join(
837
                (GroupHierarchy,
838
                    GroupHierarchy.idchild == SUPITEM_GROUP_TABLE.c.idgroup),
839
            ).join(
840
                (DataPermission,
841
                    DataPermission.idgroup == GroupHierarchy.idparent),
842
            ).join(
843
                (USER_GROUP_TABLE,
844
                    USER_GROUP_TABLE.c.idgroup == DataPermission.idusergroup),
845
            ).filter(USER_GROUP_TABLE.c.username == user.user_name)
846

    
847
        # Filtrage des évènements en fonction
848
        # de l'identifiant passé en paramètre
849
        events = events.filter(CorrEvent.idcorrevent == idcorrevent).count()
850

    
851
        # Pas d'événement ou permission refusée. On ne distingue pas
852
        # les 2 cas afin d'éviter la divulgation d'informations.
853
        if events == 0:
854
            raise HTTPNotFound(_('No such incident or insufficient '
855
                                'permissions'))
856

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

    
862
    @validate(validators={
863
        "fontsize": validators.Regex(
864
            r'[0-9]+(pt|px|em|%)',
865
            regexOps = ('I',)
866
        )}, error_handler = handle_validation_errors_json)
867
    @expose('json')
868
    def set_fontsize(self, fontsize):
869
        """Enregistre la taille de la police dans les préférences."""
870
        session['fontsize'] = fontsize
871
        session.save()
872
        return dict()
873

    
874
    @validate(validators={"refresh": validators.Int()},
875
            error_handler = handle_validation_errors_json)
876
    @expose('json')
877
    def set_refresh(self, refresh):
878
        """Enregistre le temps de rafraichissement dans les préférences."""
879
        session['refresh'] = bool(refresh)
880
        session.save()
881
        return dict()
882

    
883
    @expose('json')
884
    def set_theme(self, theme):
885
        """Enregistre le thème à utiliser dans les préférences."""
886
        # On sauvegarde l'ID du thème sans vérifications
887
        # car les thèmes (styles CSS) sont définies dans
888
        # les packages de thèmes (ex: vigilo-themes-default).
889
        # La vérification de la valeur est faite dans les templates.
890
        session['theme'] = theme
891
        session.save()
892
        return dict()
893

    
894
    @require(access_restriction)
895
    @expose('json')
896
    def get_groups(self, parent_id=None, onlytype="", offset=0, noCache=None):
897
        """
898
        Affiche un étage de l'arbre de
899
        sélection des hôtes et groupes d'hôtes.
900

901
        @param parent_id: identifiant du groupe d'hôte parent
902
        @type  parent_id: C{int} or None
903
        """
904

    
905
        # Si l'identifiant du groupe parent n'est pas
906
        # spécifié, on retourne la liste des groupes
907
        # racines, fournie par la méthode get_root_groups.
908
        if parent_id is None:
909
            return self.get_root_groups()
910

    
911
        # TODO: Utiliser un schéma de validation
912
        parent_id = int(parent_id)
913
        offset = int(offset)
914

    
915
        # On récupère la liste des groupes de supitems dont
916
        # l'identifiant du parent est passé en paramètre.
917
        supitem_groups = DBSession.query(
918
                SupItemGroup.idgroup,
919
                SupItemGroup.name,
920
            ).join(
921
                (GroupHierarchy,
922
                    GroupHierarchy.idchild == SupItemGroup.idgroup),
923
            ).filter(GroupHierarchy.idparent == parent_id
924
            ).filter(GroupHierarchy.hops == 1
925
            ).order_by(SupItemGroup.name)
926

    
927
        # Si l'utilisateur n'appartient pas au groupe 'managers',
928
        # on filtre les résultats en fonction de ses permissions.
929
        if not config.is_manager.is_met(request.environ):
930
            user = get_current_user()
931
            GroupHierarchy_aliased = aliased(GroupHierarchy,
932
                name='GroupHierarchy_aliased')
933
            supitem_groups = supitem_groups.join(
934
                (GroupHierarchy_aliased,
935
                    or_(
936
                        GroupHierarchy_aliased.idchild == SupItemGroup.idgroup,
937
                        GroupHierarchy_aliased.idparent == SupItemGroup.idgroup
938
                    )),
939
                (DataPermission,
940
                    or_(
941
                        DataPermission.idgroup == \
942
                            GroupHierarchy_aliased.idparent,
943
                        DataPermission.idgroup == \
944
                            GroupHierarchy_aliased.idchild,
945
                    )),
946
                (USER_GROUP_TABLE, USER_GROUP_TABLE.c.idgroup == \
947
                    DataPermission.idusergroup),
948
            ).filter(USER_GROUP_TABLE.c.username == user.user_name)
949

    
950
        limit = int(config.get("max_menu_entries", 20))
951
        result = {"groups": [], "items": []}
952
        num_children_left = supitem_groups.distinct().count() - offset
953
        if offset:
954
            result["continued_from"] = offset
955
            result["continued_type"] = "group"
956
        all_grs = supitem_groups.distinct().limit(limit).offset(offset).all()
957
        for group in all_grs:
958
            result["groups"].append({
959
                'id'   : group.idgroup,
960
                'name' : group.name,
961
                'type' : "group",
962
            })
963
        if num_children_left > limit:
964
            result["groups"].append({
965
                'name': _("Next %(limit)s") % {"limit": limit},
966
                'offset': offset + limit,
967
                'parent_id': parent_id,
968
                'type': 'continued',
969
                'for_type': 'group',
970
            })
971

    
972
        return result
973

    
974
    def get_root_groups(self):
975
        """
976
        Retourne tous les groupes racines (c'est à dire n'ayant
977
        aucun parent) d'hôtes auquel l'utilisateur a accès.
978

979
        @return: Un dictionnaire contenant la liste de ces groupes.
980
        @rtype : C{dict} of C{list} of C{dict} of C{mixed}
981
        """
982

    
983
        # On récupère tous les groupes qui ont un parent.
984
        children = DBSession.query(
985
            SupItemGroup,
986
        ).distinct(
987
        ).join(
988
            (GroupHierarchy, GroupHierarchy.idchild == SupItemGroup.idgroup)
989
        ).filter(GroupHierarchy.hops > 0)
990

    
991
        # Ensuite on les exclut de la liste des groupes,
992
        # pour ne garder que ceux qui sont au sommet de
993
        # l'arbre et qui constituent nos "root groups".
994
        root_groups = DBSession.query(
995
            SupItemGroup,
996
        ).except_(children
997
        ).order_by(SupItemGroup.name)
998

    
999
        # On filtre ces groupes racines afin de ne
1000
        # retourner que ceux auquels l'utilisateur a accès
1001
        user = get_current_user()
1002
        if not config.is_manager.is_met(request.environ):
1003
            root_groups = root_groups.join(
1004
                (GroupHierarchy,
1005
                    GroupHierarchy.idparent == SupItemGroup.idgroup),
1006
                (DataPermission,
1007
                    DataPermission.idgroup == GroupHierarchy.idchild),
1008
                (USER_GROUP_TABLE, USER_GROUP_TABLE.c.idgroup == \
1009
                    DataPermission.idusergroup),
1010
            ).filter(USER_GROUP_TABLE.c.username == user.user_name)
1011

    
1012
        groups = []
1013
        for group in root_groups.all():
1014
            groups.append({
1015
                'id'   : group.idgroup,
1016
                'name' : group.name,
1017
                'type' : "group",
1018
            })
1019

    
1020
        return dict(groups=groups, items=[])
1021

    
1022
def get_last_modification_timestamp(event_id_list,
1023
                                    value_if_none=datetime.now()):
1024
    """
1025
    Récupère le timestamp de la dernière modification
1026
    opérée sur l'un des événements dont l'identifiant
1027
    fait partie de la liste passée en paramètre.
1028
    """
1029
    if not event_id_list:
1030
        last_modification_timestamp = None
1031
    else:
1032
        last_modification_timestamp = DBSession.query(
1033
                                func.max(EventHistory.timestamp),
1034
                         ).filter(EventHistory.idevent.in_(event_id_list)
1035
                         ).scalar()
1036

    
1037
    if not last_modification_timestamp:
1038
        if not value_if_none:
1039
            return None
1040
        else:
1041
            last_modification_timestamp = value_if_none
1042
    return datetime.fromtimestamp(mktime(
1043
        last_modification_timestamp.timetuple()))