Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

vigiboard / vigiboard / controllers / root.py @ 8d647d93

History | View | Annotate | Download (41.6 KB)

1
# -*- coding: utf-8 -*-
2
# vim:set expandtab tabstop=4 shiftwidth=4:
3
################################################################################
4
#
5
# Copyright (C) 2007-2015 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.controllers import CUSTOM_CONTENT_TYPE
30
from tg import expose, validate, require, flash, url, \
31
    tmpl_context, request, response, config, session, redirect
32
from webhelpers import paginate
33
from tw.forms import validators
34
from pylons.i18n import ugettext as _, lazy_ugettext as l_, get_lang
35
from sqlalchemy import asc
36
from sqlalchemy.sql import func
37
from sqlalchemy.orm import aliased
38
from sqlalchemy.sql.expression import or_
39
from repoze.what.predicates import Any, All, NotAuthorizedError, \
40
                                    has_permission, not_anonymous
41
from formencode import schema
42

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

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

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

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

    
70
LOGGER = logging.getLogger(__name__)
71

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

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

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

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

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

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

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

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

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

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

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

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

    
154
    @validate(
155
        validators=IndexSchema(),
156
        error_handler = process_form_errors)
157
    @expose('events_table.html', content_type=CUSTOM_CONTENT_TYPE)
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(session.get('items_per_page', 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-Type"] = "text/csv"
260
            response.headers['Content-Disposition'] = \
261
                            'attachment;filename="alerts.csv"'
262
            return export_csv.export(page, plugins_data)
263

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

    
277

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

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

    
292
        lang = get_lang()
293

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

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

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

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

    
316

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

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

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

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

    
341
        user = get_current_user()
342

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

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

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

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

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

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

    
402

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

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

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

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

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

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

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

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

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

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

    
469

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
567

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
792

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

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

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

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

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

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

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

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

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

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

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

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

    
895
    @validate(validators={"items": validators.Int()},
896
            error_handler = handle_validation_errors_json)
897
    @expose('json')
898
    def set_items_per_page(self, items):
899
        """Enregistre le nombre d'alertes par page dans les préférences."""
900
        session['items_per_page'] = items
901
        session.save()
902
        return dict()
903

    
904
    @require(access_restriction)
905
    @expose('json')
906
    def get_groups(self, parent_id=None, onlytype="", offset=0, noCache=None):
907
        """
908
        Affiche un étage de l'arbre de
909
        sélection des hôtes et groupes d'hôtes.
910

911
        @param parent_id: identifiant du groupe d'hôte parent
912
        @type  parent_id: C{int} or None
913
        """
914

    
915
        # Si l'identifiant du groupe parent n'est pas
916
        # spécifié, on retourne la liste des groupes
917
        # racines, fournie par la méthode get_root_groups.
918
        if parent_id is None:
919
            return self.get_root_groups()
920

    
921
        # TODO: Utiliser un schéma de validation
922
        parent_id = int(parent_id)
923
        offset = int(offset)
924

    
925
        # On récupère la liste des groupes de supitems dont
926
        # l'identifiant du parent est passé en paramètre.
927
        supitem_groups = DBSession.query(
928
                SupItemGroup.idgroup,
929
                SupItemGroup.name,
930
            ).join(
931
                (GroupHierarchy,
932
                    GroupHierarchy.idchild == SupItemGroup.idgroup),
933
            ).filter(GroupHierarchy.idparent == parent_id
934
            ).filter(GroupHierarchy.hops == 1
935
            ).order_by(SupItemGroup.name)
936

    
937
        # Si l'utilisateur n'appartient pas au groupe 'managers',
938
        # on filtre les résultats en fonction de ses permissions.
939
        if not config.is_manager.is_met(request.environ):
940
            user = get_current_user()
941
            GroupHierarchy_aliased = aliased(GroupHierarchy,
942
                name='GroupHierarchy_aliased')
943
            supitem_groups = supitem_groups.join(
944
                (GroupHierarchy_aliased,
945
                    or_(
946
                        GroupHierarchy_aliased.idchild == SupItemGroup.idgroup,
947
                        GroupHierarchy_aliased.idparent == SupItemGroup.idgroup
948
                    )),
949
                (DataPermission,
950
                    or_(
951
                        DataPermission.idgroup == \
952
                            GroupHierarchy_aliased.idparent,
953
                        DataPermission.idgroup == \
954
                            GroupHierarchy_aliased.idchild,
955
                    )),
956
                (USER_GROUP_TABLE, USER_GROUP_TABLE.c.idgroup == \
957
                    DataPermission.idusergroup),
958
            ).filter(USER_GROUP_TABLE.c.username == user.user_name)
959

    
960
        limit = int(config.get("max_menu_entries", 20))
961
        result = {"groups": [], "items": []}
962
        num_children_left = supitem_groups.distinct().count() - offset
963
        if offset:
964
            result["continued_from"] = offset
965
            result["continued_type"] = "group"
966
        all_grs = supitem_groups.distinct().limit(limit).offset(offset).all()
967
        for group in all_grs:
968
            result["groups"].append({
969
                'id'   : group.idgroup,
970
                'name' : group.name,
971
                'type' : "group",
972
            })
973
        if num_children_left > limit:
974
            result["groups"].append({
975
                'name': _("Next %(limit)s") % {"limit": limit},
976
                'offset': offset + limit,
977
                'parent_id': parent_id,
978
                'type': 'continued',
979
                'for_type': 'group',
980
            })
981

    
982
        return result
983

    
984
    def get_root_groups(self):
985
        """
986
        Retourne tous les groupes racines (c'est à dire n'ayant
987
        aucun parent) d'hôtes auquel l'utilisateur a accès.
988

989
        @return: Un dictionnaire contenant la liste de ces groupes.
990
        @rtype : C{dict} of C{list} of C{dict} of C{mixed}
991
        """
992

    
993
        # On récupère tous les groupes qui ont un parent.
994
        children = DBSession.query(
995
            SupItemGroup,
996
        ).distinct(
997
        ).join(
998
            (GroupHierarchy, GroupHierarchy.idchild == SupItemGroup.idgroup)
999
        ).filter(GroupHierarchy.hops > 0)
1000

    
1001
        # Ensuite on les exclut de la liste des groupes,
1002
        # pour ne garder que ceux qui sont au sommet de
1003
        # l'arbre et qui constituent nos "root groups".
1004
        root_groups = DBSession.query(
1005
            SupItemGroup,
1006
        ).except_(children
1007
        ).order_by(SupItemGroup.name)
1008

    
1009
        # On filtre ces groupes racines afin de ne
1010
        # retourner que ceux auquels l'utilisateur a accès
1011
        user = get_current_user()
1012
        if not config.is_manager.is_met(request.environ):
1013
            root_groups = root_groups.join(
1014
                (GroupHierarchy,
1015
                    GroupHierarchy.idparent == SupItemGroup.idgroup),
1016
                (DataPermission,
1017
                    DataPermission.idgroup == GroupHierarchy.idchild),
1018
                (USER_GROUP_TABLE, USER_GROUP_TABLE.c.idgroup == \
1019
                    DataPermission.idusergroup),
1020
            ).filter(USER_GROUP_TABLE.c.username == user.user_name)
1021

    
1022
        groups = []
1023
        for group in root_groups.all():
1024
            groups.append({
1025
                'id'   : group.idgroup,
1026
                'name' : group.name,
1027
                'type' : "group",
1028
            })
1029

    
1030
        return dict(groups=groups, items=[])
1031

    
1032
def get_last_modification_timestamp(event_id_list,
1033
                                    value_if_none=datetime.now()):
1034
    """
1035
    Récupère le timestamp de la dernière modification
1036
    opérée sur l'un des événements dont l'identifiant
1037
    fait partie de la liste passée en paramètre.
1038
    """
1039
    if not event_id_list:
1040
        last_modification_timestamp = None
1041
    else:
1042
        last_modification_timestamp = DBSession.query(
1043
                                func.max(EventHistory.timestamp),
1044
                         ).filter(EventHistory.idevent.in_(event_id_list)
1045
                         ).scalar()
1046

    
1047
    if not last_modification_timestamp:
1048
        if not value_if_none:
1049
            return None
1050
        else:
1051
            last_modification_timestamp = value_if_none
1052
    return datetime.fromtimestamp(mktime(
1053
        last_modification_timestamp.timetuple()))