Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

vigiboard / vigiboard / controllers / root.py @ a2fa6a5b

History | View | Annotate | Download (38.3 KB)

1
# -*- coding: utf-8 -*-
2
# vim:set expandtab tabstop=4 shiftwidth=4:
3
################################################################################
4
#
5
# Copyright (C) 2007-2012 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
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, in_group, \
39
                                    has_permission, not_anonymous, \
40
                                    NotAuthorizedError
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.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
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):
79
    """
80
    Le controller général de vigiboard
81
    """
82
    error = ErrorController()
83
    autocomplete = AutoCompleteController()
84
    nagios = ProxyController('nagios', '/nagios/',
85
        not_anonymous(l_('You need to be authenticated')))
86
    api = ApiRootController("/api")
87
    feeds = FeedsController()
88
    custom = CustomController()
89

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

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

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

    
114
    class IndexSchema(schema.Schema):
115
        """Schéma de validation de la méthode index."""
116
        # Si on ne passe pas le paramètre "page" ou qu'on passe une valeur
117
        # invalide ou pas de valeur du tout, alors on affiche la 1ère page.
118
        page = validators.Int(min=1, if_missing=1, if_invalid=1, not_empty=True)
119

    
120
        # Nécessaire pour que les critères de recherche soient conservés.
121
        allow_extra_fields = True
122

    
123
        # 2ème validation, cette fois avec les champs
124
        # du formulaire de recherche.
125
        chained_validators = [create_search_form.validator]
126

    
127
    @validate(
128
        validators=IndexSchema(),
129
        error_handler = process_form_errors)
130
    @expose('events_table.html')
131
    @expose('csv', content_type='text/csv')
132
    @require(access_restriction)
133
    def index(self, page, **search):
134
        """
135
        Page d'accueil de Vigiboard. Elle affiche, suivant la page demandée
136
        (page 1 par defaut), la liste des événements, rangés par ordre de prise
137
        en compte, puis de sévérité.
138
        Pour accéder à cette page, l'utilisateur doit être authentifié.
139

140
        @param page: Numéro de la page souhaitée, commence à 1
141
        @type page: C{int}
142
        @param search: Dictionnaire contenant les critères de recherche.
143
        @type search: C{dict}
144

145
        Cette méthode permet de satisfaire les exigences suivantes :
146
            - VIGILO_EXIG_VIGILO_BAC_0040,
147
            - VIGILO_EXIG_VIGILO_BAC_0070,
148
            - VIGILO_EXIG_VIGILO_BAC_0100,
149
        """
150
        user = get_current_user()
151
        aggregates = VigiboardRequest(user, search=search)
152

    
153
        aggregates.add_table(
154
            CorrEvent,
155
            aggregates.items.c.hostname,
156
            aggregates.items.c.servicename
157
        )
158
        aggregates.add_join((Event, CorrEvent.idcause == Event.idevent))
159
        aggregates.add_contains_eager(CorrEvent.cause)
160
        aggregates.add_group_by(Event)
161
        aggregates.add_join((aggregates.items,
162
            Event.idsupitem == aggregates.items.c.idsupitem))
163
        aggregates.add_order_by(asc(aggregates.items.c.hostname))
164

    
165
        # Certains arguments sont réservés dans routes.util.url_for().
166
        # On effectue les substitutions adéquates.
167
        # Par exemple: "host" devient "host_".
168
        reserved = ('host', 'anchor', 'protocol', 'qualified')
169
        for column in search.copy():
170
            if column in reserved:
171
                search[column + '_'] = search[column]
172
                del search[column]
173

    
174
        # On ne garde que les champs effectivement renseignés.
175
        for column in search.copy():
176
            if not search[column]:
177
                del search[column]
178

    
179
        # On sérialise les champs de type dict.
180
        def serialize_dict(dct, key):
181
            if isinstance(dct[key], dict):
182
                for subkey in dct[key]:
183
                    serialize_dict(dct[key], subkey)
184
                    dct[key+'.'+subkey] = dct[key][subkey]
185
                del dct[key]
186
        fixed_search = search.copy()
187
        for column in fixed_search.copy():
188
            serialize_dict(fixed_search, column)
189

    
190
        # Pagination des résultats
191
        aggregates.generate_request()
192
        items_per_page = int(config['vigiboard_items_per_page'])
193
        page = paginate.Page(aggregates.req, page=page,
194
            items_per_page=items_per_page)
195

    
196
        # Récupération des données des plugins
197
        plugins_data = {}
198
        plugins = dict(config['columns_plugins'])
199

    
200
        ids_events = [event[0].idcause for event in page.items]
201
        ids_correvents = [event[0].idcorrevent for event in page.items]
202
        for plugin in plugins:
203
            plugin_data = plugins[plugin].get_bulk_data(ids_correvents)
204
            if plugin_data:
205
                plugins_data[plugin] = plugin_data
206
            else:
207
                plugins_data[plugin] = {}
208

    
209
        # Ajout des formulaires et préparation
210
        # des données pour ces formulaires.
211
        tmpl_context.last_modification = \
212
            mktime(get_last_modification_timestamp(ids_events).timetuple())
213

    
214
        tmpl_context.edit_event_form = EditEventForm("edit_event_form",
215
            submit_text=_('Apply'), action=url('/update'))
216

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

    
223
            response.headers['Content-Disposition'] = \
224
                            'attachment;filename="alerts.csv"'
225
            return export_csv.export(page, plugins_data)
226

    
227
        return dict(
228
            hostname = None,
229
            servicename = None,
230
            plugins_data = plugins_data,
231
            page = page,
232
            event_edit_status_options = edit_event_status_options,
233
            search_form = create_search_form,
234
            search = search,
235
            fixed_search = fixed_search,
236
        )
237

    
238

    
239
    @expose()
240
    def i18n(self):
241
        import gettext
242
        import pylons
243
        import os.path
244

    
245
        # Repris de pylons.i18n.translation:_get_translator.
246
        conf = pylons.config.current_conf()
247
        try:
248
            rootdir = conf['pylons.paths']['root']
249
        except KeyError:
250
            rootdir = conf['pylons.paths'].get('root_path')
251
        localedir = os.path.join(rootdir, 'i18n')
252

    
253
        lang = get_lang()
254

    
255
        # Localise le fichier *.mo actuellement chargé
256
        # et génère le chemin jusqu'au *.js correspondant.
257
        filename = gettext.find(conf['pylons.package'], localedir,
258
            languages=lang)
259
        js = filename[:-3] + '.js'
260

    
261
        themes_filename = gettext.find(
262
            'vigilo-themes',
263
            resource_filename('vigilo.themes.i18n', ''),
264
            languages=lang)
265
        themes_js = themes_filename[:-3] + '.js'
266

    
267
        # Récupère et envoie le contenu du fichier de traduction *.js.
268
        fhandle = open(js, 'r')
269
        translations = fhandle.read()
270
        fhandle.close()
271

    
272
        fhandle = open(themes_js, 'r')
273
        translations += fhandle.read()
274
        fhandle.close()
275
        return translations
276

    
277

    
278
    class MaskedEventsSchema(schema.Schema):
279
        """Schéma de validation de la méthode masked_events."""
280
        idcorrevent = validators.Int(not_empty=True)
281
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
282

    
283
    @validate(
284
        validators=MaskedEventsSchema(),
285
        error_handler = process_form_errors)
286
    @expose('raw_events_table.html')
287
    @require(access_restriction)
288
    def masked_events(self, idcorrevent, page):
289
        """
290
        Affichage de la liste des événements bruts masqués d'un événement
291
        corrélé (événements agrégés dans l'événement corrélé).
292

293
        @param idcorrevent: identifiant de l'événement corrélé souhaité.
294
        @type idcorrevent: C{int}
295
        """
296
        user = get_current_user()
297

    
298
        # Récupère la liste des événements masqués de l'événement
299
        # corrélé donné par idcorrevent.
300
        events = VigiboardRequest(user, False)
301
        events.add_table(
302
            Event,
303
            events.items.c.hostname,
304
            events.items.c.servicename,
305
        )
306
        events.add_join((EVENTSAGGREGATE_TABLE, \
307
            EVENTSAGGREGATE_TABLE.c.idevent == Event.idevent))
308
        events.add_join((CorrEvent, CorrEvent.idcorrevent == \
309
            EVENTSAGGREGATE_TABLE.c.idcorrevent))
310
        events.add_join((events.items,
311
            Event.idsupitem == events.items.c.idsupitem))
312
        events.add_filter(Event.idevent != CorrEvent.idcause)
313
        events.add_filter(CorrEvent.idcorrevent == idcorrevent)
314

    
315
        # Récupère l'instance de SupItem associé à la cause de
316
        # l'événement corrélé. Cette instance est utilisé pour
317
        # obtenir le nom d'hôte/service auquel la cause est
318
        # rattachée (afin de fournir un contexte à l'utilisateur).
319
        hostname = None
320
        servicename = None
321
        cause_supitem = DBSession.query(
322
                SupItem,
323
            ).join(
324
                (Event, Event.idsupitem == SupItem.idsupitem),
325
                (CorrEvent, Event.idevent == CorrEvent.idcause),
326
            ).filter(CorrEvent.idcorrevent == idcorrevent
327
            ).one()
328

    
329
        if isinstance(cause_supitem, LowLevelService):
330
            hostname = cause_supitem.host.name
331
            servicename = cause_supitem.servicename
332
        elif isinstance(cause_supitem, Host):
333
            hostname = cause_supitem.name
334

    
335
        # Pagination des résultats
336
        events.generate_request()
337
        items_per_page = int(config['vigiboard_items_per_page'])
338
        page = paginate.Page(events.req, page=page,
339
            items_per_page=items_per_page)
340

    
341
        # Vérification que l'événement existe
342
        if not page.item_count:
343
            flash(_('No masked event or access denied'), 'error')
344
            redirect('/')
345

    
346
        return dict(
347
            idcorrevent = idcorrevent,
348
            hostname = hostname,
349
            servicename = servicename,
350
            plugins_data = {},
351
            page = page,
352
            search_form = create_search_form,
353
            search = {},
354
            fixed_search = {},
355
        )
356

    
357

    
358
    class EventSchema(schema.Schema):
359
        """Schéma de validation de la méthode event."""
360
        idevent = validators.Int(not_empty=True)
361
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
362

    
363
    @validate(
364
        validators=EventSchema(),
365
        error_handler = process_form_errors)
366
    @expose('history_table.html')
367
    @require(access_restriction)
368
    def event(self, idevent, page):
369
        """
370
        Affichage de l'historique d'un événement brut.
371
        Pour accéder à cette page, l'utilisateur doit être authentifié.
372

373
        @param idevent: identifiant de l'événement brut souhaité.
374
        @type idevent: C{int}
375
        @param page: numéro de la page à afficher.
376
        @type page: C{int}
377

378
        Cette méthode permet de satisfaire l'exigence
379
        VIGILO_EXIG_VIGILO_BAC_0080.
380
        """
381
        user = get_current_user()
382
        events = VigiboardRequest(user, False)
383
        events.add_table(
384
            Event,
385
            events.items.c.hostname.label('hostname'),
386
            events.items.c.servicename.label('servicename'),
387
        )
388
        events.add_join((EVENTSAGGREGATE_TABLE, \
389
            EVENTSAGGREGATE_TABLE.c.idevent == Event.idevent))
390
        events.add_join((CorrEvent, CorrEvent.idcorrevent == \
391
            EVENTSAGGREGATE_TABLE.c.idcorrevent))
392
        events.add_join((events.items,
393
            Event.idsupitem == events.items.c.idsupitem))
394
        events.add_filter(Event.idevent == idevent)
395

    
396
        if events.num_rows() != 1:
397
            flash(_('No such event or access denied'), 'error')
398
            redirect('/')
399

    
400
        events.format_events(0, 1)
401
        events.generate_tmpl_context()
402
        history = events.format_history()
403

    
404
        # Pagination des résultats
405
        items_per_page = int(config['vigiboard_items_per_page'])
406
        page = paginate.Page(history, page=page, items_per_page=items_per_page)
407
        event = events.req[0]
408

    
409
        return dict(
410
            idevent = idevent,
411
            hostname = event.hostname,
412
            servicename = event.servicename,
413
            plugins_data = {},
414
            page = page,
415
            search_form = create_search_form,
416
            search = {},
417
            fixed_search = {},
418
        )
419

    
420

    
421
    class ItemSchema(schema.Schema):
422
        """Schéma de validation de la méthode item."""
423
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
424
        host = validators.String(not_empty=True)
425
        service = validators.String(if_missing=None)
426

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

438
        @param page: Numéro de la page à afficher.
439
        @param host: Nom de l'hôte souhaité.
440
        @param service: Nom du service souhaité
441

442
        Cette méthode permet de satisfaire l'exigence
443
        VIGILO_EXIG_VIGILO_BAC_0080.
444
        """
445
        idsupitem = SupItem.get_supitem(host, service)
446
        if not idsupitem:
447
            flash(_('No such host/service'), 'error')
448
            redirect('/')
449

    
450
        user = get_current_user()
451
        aggregates = VigiboardRequest(user, False)
452
        aggregates.add_table(
453
            CorrEvent,
454
            aggregates.items.c.hostname,
455
            aggregates.items.c.servicename,
456
        )
457
        aggregates.add_join((Event, CorrEvent.idcause == Event.idevent))
458
        aggregates.add_join((aggregates.items,
459
            Event.idsupitem == aggregates.items.c.idsupitem))
460
        aggregates.add_filter(aggregates.items.c.idsupitem == idsupitem)
461

    
462
        # Pagination des résultats
463
        aggregates.generate_request()
464
        items_per_page = int(config['vigiboard_items_per_page'])
465
        page = paginate.Page(aggregates.req, page=page,
466
            items_per_page=items_per_page)
467

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

    
473
        # Ajout des formulaires et préparation
474
        # des données pour ces formulaires.
475
        ids_events = [event[0].idcause for event in page.items]
476
        tmpl_context.last_modification = \
477
            mktime(get_last_modification_timestamp(ids_events).timetuple())
478

    
479
        tmpl_context.edit_event_form = EditEventForm("edit_event_form",
480
            submit_text=_('Apply'), action=url('/update'))
481

    
482
        plugins_data = {}
483
        for plugin in dict(config['columns_plugins']):
484
            plugins_data[plugin] = {}
485

    
486
        return dict(
487
            hostname = host,
488
            servicename = service,
489
            plugins_data = plugins_data,
490
            page = page,
491
            event_edit_status_options = edit_event_status_options,
492
            search_form = create_search_form,
493
            search = {},
494
            fixed_search = {},
495
        )
496

    
497

    
498
    class UpdateSchema(schema.Schema):
499
        """Schéma de validation de la méthode update."""
500
        id = validators.Regex(r'^[0-9]+(,[0-9]+)*,?$')
501
        last_modification = validators.Number(not_empty=True)
502
        trouble_ticket = validators.String(if_missing='')
503
        ack = validators.OneOf(
504
            [unicode(s[0]) for s in edit_event_status_options],
505
            not_empty=True)
506

    
507
    @validate(
508
        validators=UpdateSchema(),
509
        error_handler = process_form_errors)
510
    @require(
511
        All(
512
            not_anonymous(msg=l_("You need to be authenticated")),
513
            Any(in_group('managers'),
514
                has_permission('vigiboard-update'),
515
                msg=l_("You don't have write access to VigiBoard"))
516
        ))
517
    @expose()
518
    def update(self, id, last_modification, trouble_ticket, ack):
519
        """
520
        Mise à jour d'un événement suivant les arguments passés.
521
        Cela peut être un changement de ticket ou un changement de statut.
522

523
        @param id: Le ou les identifiants des événements à traiter
524
        @param last_modification: La date de la dernière modification
525
            dont l'utilisateur est au courant.
526
        @param trouble_ticket: Nouveau numéro du ticket associé.
527
        @param ack: Nouvel état d'acquittement des événements sélectionnés.
528

529
        Cette méthode permet de satisfaire les exigences suivantes :
530
            - VIGILO_EXIG_VIGILO_BAC_0020,
531
            - VIGILO_EXIG_VIGILO_BAC_0060,
532
            - VIGILO_EXIG_VIGILO_BAC_0110.
533
        """
534

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

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

    
545
        user = get_current_user()
546
        events = VigiboardRequest(user)
547
        events.add_table(CorrEvent)
548
        events.add_join((Event, CorrEvent.idcause == Event.idevent))
549
        events.add_join((events.items,
550
            Event.idsupitem == events.items.c.idsupitem))
551
        events.add_filter(CorrEvent.idcorrevent.in_(ids))
552

    
553
        events.generate_request()
554
        idevents = [cause.idcause for cause in events.req]
555

    
556
        # Si des changements sont survenus depuis que la
557
        # page est affichée, on en informe l'utilisateur.
558
        last_modification = datetime.fromtimestamp(last_modification)
559
        cur_last_modification = get_last_modification_timestamp(idevents, None)
560
        if cur_last_modification and last_modification < cur_last_modification:
561
            flash(_('Changes have occurred since the page was last displayed, '
562
                    'your changes HAVE NOT been saved.'), 'warning')
563
            raise redirect(request.environ.get('HTTP_REFERER', '/'))
564

    
565
        # Vérification que au moins un des identifiants existe et est éditable
566
        if not events.num_rows():
567
            flash(_('No access to this event'), 'error')
568
            redirect('/')
569

    
570
        if ack == u'Forced':
571
            condition = Any(
572
                in_group('managers'),
573
                has_permission('vigiboard-admin'),
574
                msg=l_("You don't have administrative access "
575
                        "to VigiBoard"))
576
            try:
577
                condition.check_authorization(request.environ)
578
            except NotAuthorizedError, e:
579
                reason = unicode(e)
580
                flash(reason, 'error')
581
                raise redirect(request.environ.get('HTTP_REFERER', '/'))
582

    
583
        # Définit 2 mappings dont les ensembles sont disjoincts
584
        # pour basculer entre la représentation en base de données
585
        # et la représentation "humaine" du bac à événements.
586
        ack_mapping = {
587
            # Permet d'associer la valeur dans le widget ToscaWidgets
588
            # (cf. vigiboard.widgets.edit_event.edit_event_status_options)
589
            # avec la valeur dans la base de données.
590
            u'None': CorrEvent.ACK_NONE,
591
            u'Acknowledged': CorrEvent.ACK_KNOWN,
592
            u'AAClosed': CorrEvent.ACK_CLOSED,
593

    
594
            # Permet d'afficher un libellé plus sympathique pour l'utilisateur
595
            # représentant l'état d'acquittement stocké en base de données.
596
            CorrEvent.ACK_NONE: l_('None'),
597
            CorrEvent.ACK_KNOWN: l_('Acknowledged'),
598
            CorrEvent.ACK_CLOSED: l_('Acknowledged and closed'),
599
        }
600

    
601
        # Modification des événements et création d'un historique
602
        # chaque fois que cela est nécessaire.
603
        for event in events.req:
604
            if trouble_ticket and trouble_ticket != event.trouble_ticket:
605
                history = EventHistory(
606
                        type_action=u"Ticket change",
607
                        idevent=event.idcause,
608
                        value=unicode(trouble_ticket),
609
                        text="Changed trouble ticket from '%(from)s' "
610
                             "to '%(to)s'" % {
611
                            'from': event.trouble_ticket,
612
                            'to': trouble_ticket,
613
                        },
614
                        username=user.user_name,
615
                        timestamp=datetime.now(),
616
                    )
617
                DBSession.add(history)
618
                LOGGER.info(_('User "%(user)s" (%(address)s) changed the '
619
                            'trouble ticket from "%(previous)s" to "%(new)s" '
620
                            'on event #%(idevent)d') % {
621
                                'user': request.identity['repoze.who.userid'],
622
                                'address': request.remote_addr,
623
                                'previous': event.trouble_ticket,
624
                                'new': trouble_ticket,
625
                                'idevent': event.idcause,
626
                            })
627
                event.trouble_ticket = trouble_ticket
628

    
629
            # Changement du statut d'acquittement.
630
            if ack != u'NoChange':
631
                changed_ack = ack
632
                # Pour forcer l'acquittement d'un événement,
633
                # il faut en plus avoir la permission
634
                # "vigiboard-admin".
635
                if ack == u'Forced':
636
                    changed_ack = u'AAClosed'
637
                    cause = event.cause
638
                    # On met systématiquement l'événement à l'état "OK",
639
                    # même s'il s'agit d'un hôte.
640
                    # Techniquement, c'est incorrect, mais on fait ça
641
                    # pour masquer l'événement de toutes façons...
642
                    cause.current_state = \
643
                        StateName.statename_to_value(u'OK')
644

    
645
                    # Mise à jour de l'état dans State, pour que
646
                    # VigiMap soit également mis à jour.
647
                    DBSession.query(State).filter(
648
                            State.idsupitem == cause.idsupitem,
649
                        ).update({
650
                            'state': StateName.statename_to_value(u'OK'),
651
                        })
652

    
653
                    history = EventHistory(
654
                            type_action=u"Forced change state",
655
                            idevent=event.idcause,
656
                            value=u'OK',
657
                            text="Forced state to 'OK'",
658
                            username=user.user_name,
659
                            timestamp=datetime.now(),
660
                            state=StateName.statename_to_value(u'OK'),
661
                        )
662
                    DBSession.add(history)
663
                    LOGGER.info(_('User "%(user)s" (%(address)s) forcefully '
664
                                'closed event #%(idevent)d') % {
665
                                    'user': request. \
666
                                            identity['repoze.who.userid'],
667
                                    'address': request.remote_addr,
668
                                    'idevent': event.idcause,
669
                                })
670

    
671
                # Convertit la valeur du widget ToscaWidgets
672
                # vers le code interne puis vers un libellé
673
                # "humain".
674
                ack_label = ack_mapping[ack_mapping[changed_ack]]
675

    
676
                # Si le changement a été forcé,
677
                # on veut le mettre en évidence.
678
                if ack == u'Forced':
679
                    history_label = u'Forced'
680
                else:
681
                    history_label = ack_label
682

    
683
                history = EventHistory(
684
                        type_action=u"Acknowledgement change state",
685
                        idevent=event.idcause,
686
                        value=unicode(history_label),
687
                        text=u"Changed acknowledgement status "
688
                            u"from '%s' to '%s'" % (
689
                            ack_mapping[event.ack],
690
                            ack_label,
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 state '
697
                            'from "%(previous)s" to "%(new)s" on event '
698
                            '#%(idevent)d') % {
699
                                'user': request.identity['repoze.who.userid'],
700
                                'address': request.remote_addr,
701
                                'previous': _(ack_mapping[event.ack]),
702
                                'new': _(ack_label),
703
                                'idevent': event.idcause,
704
                            })
705
                event.ack = ack_mapping[changed_ack]
706

    
707
        DBSession.flush()
708
        flash(_('Updated successfully'))
709
        redirect(request.environ.get('HTTP_REFERER', '/'))
710

    
711

    
712
    class GetPluginValueSchema(schema.Schema):
713
        """Schéma de validation de la méthode get_plugin_value."""
714
        idcorrevent = validators.Int(not_empty=True)
715
        plugin_name = validators.String(not_empty=True)
716
        # Permet de passer des paramètres supplémentaires au plugin.
717
        allow_extra_fields = True
718

    
719
    @validate(
720
        validators=GetPluginValueSchema(),
721
        error_handler = handle_validation_errors_json)
722
    @expose('json')
723
    @require(access_restriction)
724
    def plugin_json(self, idcorrevent, plugin_name, *arg, **krgv):
725
        """
726
        Permet de récupérer la valeur d'un plugin associée à un CorrEvent
727
        donné via JSON.
728
        """
729

    
730
        # Vérification de l'existence du plugin
731
        plugins = dict(config['columns_plugins'])
732
        if plugin_name not in plugins:
733
            raise HTTPNotFound(_("No such plugin '%s'") % plugin_name)
734

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

    
738
        # Filtrage des évènements en fonction des permissions de
739
        # l'utilisateur (s'il n'appartient pas au groupe 'managers')
740
        is_manager = in_group('managers').is_met(request.environ)
741
        if not is_manager:
742
            user = get_current_user()
743

    
744
            events = events.join(
745
                (Event, Event.idevent == CorrEvent.idcause),
746
            ).outerjoin(
747
                (LowLevelService, LowLevelService.idservice == Event.idsupitem),
748
            ).join(
749
                (SUPITEM_GROUP_TABLE,
750
                    or_(
751
                        SUPITEM_GROUP_TABLE.c.idsupitem == \
752
                            LowLevelService.idhost,
753
                        SUPITEM_GROUP_TABLE.c.idsupitem == \
754
                            Event.idsupitem,
755
                    )
756
                ),
757
            ).join(
758
                (GroupHierarchy,
759
                    GroupHierarchy.idchild == SUPITEM_GROUP_TABLE.c.idgroup),
760
            ).join(
761
                (DataPermission,
762
                    DataPermission.idgroup == GroupHierarchy.idparent),
763
            ).join(
764
                (USER_GROUP_TABLE,
765
                    USER_GROUP_TABLE.c.idgroup == DataPermission.idusergroup),
766
            ).filter(USER_GROUP_TABLE.c.username == user.user_name)
767

    
768
        # Filtrage des évènements en fonction
769
        # de l'identifiant passé en paramètre
770
        events = events.filter(CorrEvent.idcorrevent == idcorrevent).count()
771

    
772
        # Pas d'événement ou permission refusée. On ne distingue pas
773
        # les 2 cas afin d'éviter la divulgation d'informations.
774
        if events == 0:
775
            raise HTTPNotFound(_('No such incident or insufficient '
776
                                'permissions'))
777

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

    
783
    @validate(validators={
784
        "fontsize": validators.Regex(
785
            r'[0-9]+(pt|px|em|%)',
786
            regexOps = ('I',)
787
        )}, error_handler = handle_validation_errors_json)
788
    @expose('json')
789
    def set_fontsize(self, fontsize):
790
        """Enregistre la taille de la police dans les préférences."""
791
        session['fontsize'] = fontsize
792
        session.save()
793
        return dict()
794

    
795
    @validate(validators={"refresh": validators.Int()},
796
            error_handler = handle_validation_errors_json)
797
    @expose('json')
798
    def set_refresh(self, refresh):
799
        """Enregistre le temps de rafraichissement dans les préférences."""
800
        session['refresh'] = bool(refresh)
801
        session.save()
802
        return dict()
803

    
804
    @expose('json')
805
    def set_theme(self, theme):
806
        """Enregistre le thème à utiliser dans les préférences."""
807
        # On sauvegarde l'ID du thème sans vérifications
808
        # car les thèmes (styles CSS) sont définies dans
809
        # les packages de thèmes (ex: vigilo-themes-default).
810
        # La vérification de la valeur est faite dans les templates.
811
        session['theme'] = theme
812
        session.save()
813
        return dict()
814

    
815
    @require(access_restriction)
816
    @expose('json')
817
    def get_groups(self, parent_id=None, onlytype="", offset=0, noCache=None):
818
        """
819
        Affiche un étage de l'arbre de
820
        sélection des hôtes et groupes d'hôtes.
821

822
        @param parent_id: identifiant du groupe d'hôte parent
823
        @type  parent_id: C{int} or None
824
        """
825

    
826
        # Si l'identifiant du groupe parent n'est pas
827
        # spécifié, on retourne la liste des groupes
828
        # racines, fournie par la méthode get_root_groups.
829
        if parent_id is None:
830
            return self.get_root_groups()
831

    
832
        # TODO: Utiliser un schéma de validation
833
        parent_id = int(parent_id)
834
        offset = int(offset)
835

    
836
        # On récupère la liste des groupes de supitems dont
837
        # l'identifiant du parent est passé en paramètre.
838
        supitem_groups = DBSession.query(
839
                SupItemGroup.idgroup,
840
                SupItemGroup.name,
841
            ).join(
842
                (GroupHierarchy,
843
                    GroupHierarchy.idchild == SupItemGroup.idgroup),
844
            ).filter(GroupHierarchy.idparent == parent_id
845
            ).filter(GroupHierarchy.hops == 1
846
            ).order_by(SupItemGroup.name)
847

    
848
        # Si l'utilisateur n'appartient pas au groupe 'managers',
849
        # on filtre les résultats en fonction de ses permissions.
850
        is_manager = in_group('managers').is_met(request.environ)
851
        if not is_manager:
852
            user = get_current_user()
853
            GroupHierarchy_aliased = aliased(GroupHierarchy,
854
                name='GroupHierarchy_aliased')
855
            supitem_groups = supitem_groups.join(
856
                (GroupHierarchy_aliased,
857
                    or_(
858
                        GroupHierarchy_aliased.idchild == SupItemGroup.idgroup,
859
                        GroupHierarchy_aliased.idparent == SupItemGroup.idgroup
860
                    )),
861
                (DataPermission,
862
                    or_(
863
                        DataPermission.idgroup == \
864
                            GroupHierarchy_aliased.idparent,
865
                        DataPermission.idgroup == \
866
                            GroupHierarchy_aliased.idchild,
867
                    )),
868
                (USER_GROUP_TABLE, USER_GROUP_TABLE.c.idgroup == \
869
                    DataPermission.idusergroup),
870
            ).filter(USER_GROUP_TABLE.c.username == user.user_name)
871

    
872
        limit = int(config.get("max_menu_entries", 20))
873
        result = {"groups": [], "items": []}
874
        num_children_left = supitem_groups.distinct().count() - offset
875
        if offset:
876
            result["continued_from"] = offset
877
            result["continued_type"] = "group"
878
        all_grs = supitem_groups.distinct().limit(limit).offset(offset).all()
879
        for group in all_grs:
880
            result["groups"].append({
881
                'id'   : group.idgroup,
882
                'name' : group.name,
883
                'type' : "group",
884
            })
885
        if num_children_left > limit:
886
            result["groups"].append({
887
                'name': _("Next %(limit)s") % {"limit": limit},
888
                'offset': offset + limit,
889
                'parent_id': parent_id,
890
                'type': 'continued',
891
                'for_type': 'group',
892
            })
893

    
894
        return result
895

    
896
    def get_root_groups(self):
897
        """
898
        Retourne tous les groupes racines (c'est à dire n'ayant
899
        aucun parent) d'hôtes auquel l'utilisateur a accès.
900

901
        @return: Un dictionnaire contenant la liste de ces groupes.
902
        @rtype : C{dict} of C{list} of C{dict} of C{mixed}
903
        """
904

    
905
        # On récupère tous les groupes qui ont un parent.
906
        children = DBSession.query(
907
            SupItemGroup,
908
        ).distinct(
909
        ).join(
910
            (GroupHierarchy, GroupHierarchy.idchild == SupItemGroup.idgroup)
911
        ).filter(GroupHierarchy.hops > 0)
912

    
913
        # Ensuite on les exclut de la liste des groupes,
914
        # pour ne garder que ceux qui sont au sommet de
915
        # l'arbre et qui constituent nos "root groups".
916
        root_groups = DBSession.query(
917
            SupItemGroup,
918
        ).except_(children
919
        ).order_by(SupItemGroup.name)
920

    
921
        # On filtre ces groupes racines afin de ne
922
        # retourner que ceux auquels l'utilisateur a accès
923
        user = get_current_user()
924
        is_manager = in_group('managers').is_met(request.environ)
925
        if not is_manager:
926

    
927
            root_groups = root_groups.join(
928
                (GroupHierarchy,
929
                    GroupHierarchy.idparent == SupItemGroup.idgroup),
930
                (DataPermission,
931
                    DataPermission.idgroup == GroupHierarchy.idchild),
932
                (USER_GROUP_TABLE, USER_GROUP_TABLE.c.idgroup == \
933
                    DataPermission.idusergroup),
934
            ).filter(USER_GROUP_TABLE.c.username == user.user_name)
935

    
936
        groups = []
937
        for group in root_groups.all():
938
            groups.append({
939
                'id'   : group.idgroup,
940
                'name' : group.name,
941
                'type' : "group",
942
            })
943

    
944
        return dict(groups=groups, items=[])
945

    
946
def get_last_modification_timestamp(event_id_list,
947
                                    value_if_none=datetime.now()):
948
    """
949
    Récupère le timestamp de la dernière modification
950
    opérée sur l'un des événements dont l'identifiant
951
    fait partie de la liste passée en paramètre.
952
    """
953
    last_modification_timestamp = DBSession.query(
954
                                func.max(EventHistory.timestamp),
955
                         ).filter(EventHistory.idevent.in_(event_id_list)
956
                         ).scalar()
957
    if not last_modification_timestamp:
958
        if not value_if_none:
959
            return None
960
        else:
961
            last_modification_timestamp = value_if_none
962
    return datetime.fromtimestamp(mktime(
963
        last_modification_timestamp.timetuple()))