Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

vigiboard / vigiboard / controllers / root.py @ 73119f8a

History | View | Annotate | Download (41 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

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

    
68
LOGGER = logging.getLogger(__name__)
69

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
179
        # Auto-supervision
180
        self.get_failures()
181

    
182
        user = get_current_user()
183
        aggregates = VigiboardRequest(user, search=search, sort=sort, order=order)
184

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

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

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

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

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

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

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

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

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

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

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

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

    
274

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

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

    
289
        lang = get_lang()
290

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

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

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

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

    
313

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

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

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

    
335
        # Auto-supervision
336
        self.get_failures()
337

    
338
        user = get_current_user()
339

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

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

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

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

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

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

    
399

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

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

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

420
        Cette méthode permet de satisfaire l'exigence
421
        VIGILO_EXIG_VIGILO_BAC_0080.
422
        """
423

    
424
        # Auto-supervision
425
        self.get_failures()
426

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

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

    
446
        events.format_events(0, 1)
447
        events.generate_tmpl_context()
448
        history = events.format_history()
449

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

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

    
466

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

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

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

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

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

503
        Cette méthode permet de satisfaire l'exigence
504
        VIGILO_EXIG_VIGILO_BAC_0080.
505
        """
506

    
507
        # Auto-supervision
508
        self.get_failures()
509

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

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

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

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

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

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

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

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

    
564

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
789

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
970
        return result
971

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

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

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

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

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

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

    
1018
        return dict(groups=groups, items=[])
1019

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

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