Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

vigiboard / vigiboard / controllers / root.py @ a2744508

History | View | Annotate | Download (38 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, 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.error import ErrorController
53
from vigilo.turbogears.controllers.autocomplete import AutoCompleteController
54
from vigilo.turbogears.controllers.proxy import ProxyController
55
from vigilo.turbogears.controllers.api.root import ApiRootController
56
from vigilo.turbogears.helpers import get_current_user
57

    
58
from vigiboard.controllers.vigiboardrequest import VigiboardRequest
59
from vigiboard.controllers.feeds import FeedsController
60

    
61
from vigiboard.widgets.edit_event import edit_event_status_options, \
62
                                            EditEventForm
63
from vigiboard.widgets.search_form import create_search_form
64
import logging
65

    
66
LOGGER = logging.getLogger(__name__)
67

    
68
__all__ = ('RootController', 'get_last_modification_timestamp',
69
           'date_to_timestamp')
70

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

    
76
class RootController(AuthController):
77
    """
78
    Le controller général de vigiboard
79
    """
80
    error = ErrorController()
81
    autocomplete = AutoCompleteController()
82
    nagios = ProxyController('nagios', '/nagios/',
83
        not_anonymous(l_('You need to be authenticated')))
84
    api = ApiRootController("/api")
85
    feeds = FeedsController()
86

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

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

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

    
111
    class DefaultSchema(schema.Schema):
112
        """Schéma de validation de la méthode default."""
113
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
114

    
115
        # Nécessaire pour que les critères de recherche soient conservés.
116
        allow_extra_fields = True
117

    
118
        # 2ème validation, cette fois avec les champs
119
        # du formulaire de recherche.
120
        chained_validators = [create_search_form.validator]
121

    
122
    @validate(
123
        validators=DefaultSchema(),
124
        error_handler = process_form_errors)
125
    @expose('events_table.html')
126
    @require(access_restriction)
127
    def default(self, page, **search):
128
        """
129
        Page d'accueil de Vigiboard. Elle affiche, suivant la page demandée
130
        (page 1 par defaut), la liste des événements, rangés par ordre de prise
131
        en compte, puis de sévérité.
132
        Pour accéder à cette page, l'utilisateur doit être authentifié.
133

134
        @param page: Numéro de la page souhaitée, commence à 1
135
        @type page: C{int}
136
        @param search: Dictionnaire contenant les critères de recherche.
137
        @type search: C{dict}
138

139
        Cette méthode permet de satisfaire les exigences suivantes :
140
            - VIGILO_EXIG_VIGILO_BAC_0040,
141
            - VIGILO_EXIG_VIGILO_BAC_0070,
142
            - VIGILO_EXIG_VIGILO_BAC_0100,
143
        """
144

    
145
        user = get_current_user()
146
        aggregates = VigiboardRequest(user, search=search)
147

    
148
        aggregates.add_table(
149
            CorrEvent,
150
            aggregates.items.c.hostname,
151
            aggregates.items.c.servicename
152
        )
153
        aggregates.add_join((Event, CorrEvent.idcause == Event.idevent))
154
        aggregates.add_contains_eager(CorrEvent.cause)
155
        aggregates.add_group_by(Event)
156
        aggregates.add_join((aggregates.items,
157
            Event.idsupitem == aggregates.items.c.idsupitem))
158
        aggregates.add_order_by(asc(aggregates.items.c.hostname))
159

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

    
169
        # On ne garde que les champs effectivement renseignés.
170
        for column in search.copy():
171
            if not search[column]:
172
                del search[column]
173

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

    
185
        # Pagination des résultats
186
        aggregates.generate_request()
187
        items_per_page = int(config['vigiboard_items_per_page'])
188
        page = paginate.Page(aggregates.req, page=page,
189
            items_per_page=items_per_page)
190

    
191
        # Récupération des données des plugins
192
        plugins_data = {}
193
        plugins = dict(config['columns_plugins'])
194

    
195
        ids_events = [event[0].idcause for event in page.items]
196
        ids_correvents = [event[0].idcorrevent for event in page.items]
197
        for plugin in plugins:
198
            plugin_data = plugins[plugin].get_bulk_data(ids_correvents)
199
            if plugin_data:
200
                plugins_data[plugin] = plugin_data
201

    
202
        # Ajout des formulaires et préparation
203
        # des données pour ces formulaires.
204
        tmpl_context.last_modification = \
205
            mktime(get_last_modification_timestamp(ids_events).timetuple())
206

    
207
        tmpl_context.edit_event_form = EditEventForm("edit_event_form",
208
            submit_text=_('Apply'), action=url('/update'))
209

    
210
        return dict(
211
            hostname = None,
212
            servicename = None,
213
            plugins_data = plugins_data,
214
            page = page,
215
            event_edit_status_options = edit_event_status_options,
216
            search_form = create_search_form,
217
            search = search,
218
            fixed_search = fixed_search,
219
        )
220

    
221

    
222
    @expose()
223
    def i18n(self):
224
        import gettext
225
        import pylons
226
        import os.path
227

    
228
        # Repris de pylons.i18n.translation:_get_translator.
229
        conf = pylons.config.current_conf()
230
        try:
231
            rootdir = conf['pylons.paths']['root']
232
        except KeyError:
233
            rootdir = conf['pylons.paths'].get('root_path')
234
        localedir = os.path.join(rootdir, 'i18n')
235

    
236
        lang = get_lang()
237

    
238
        # Localise le fichier *.mo actuellement chargé
239
        # et génère le chemin jusqu'au *.js correspondant.
240
        filename = gettext.find(conf['pylons.package'], localedir,
241
            languages=lang)
242
        js = filename[:-3] + '.js'
243

    
244
        themes_filename = gettext.find(
245
            'vigilo-themes',
246
            resource_filename('vigilo.themes.i18n', ''),
247
            languages=lang)
248
        themes_js = themes_filename[:-3] + '.js'
249

    
250
        # Récupère et envoie le contenu du fichier de traduction *.js.
251
        fhandle = open(js, 'r')
252
        translations = fhandle.read()
253
        fhandle.close()
254

    
255
        fhandle = open(themes_js, 'r')
256
        translations += fhandle.read()
257
        fhandle.close()
258
        return translations
259

    
260

    
261
    class MaskedEventsSchema(schema.Schema):
262
        """Schéma de validation de la méthode masked_events."""
263
        idcorrevent = validators.Int(not_empty=True)
264
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
265

    
266
    @validate(
267
        validators=MaskedEventsSchema(),
268
        error_handler = process_form_errors)
269
    @expose('raw_events_table.html')
270
    @require(access_restriction)
271
    def masked_events(self, idcorrevent, page):
272
        """
273
        Affichage de la liste des événements bruts masqués d'un événement
274
        corrélé (événements agrégés dans l'événement corrélé).
275

276
        @param idcorrevent: identifiant de l'événement corrélé souhaité.
277
        @type idcorrevent: C{int}
278
        """
279
        user = get_current_user()
280

    
281
        # Récupère la liste des événements masqués de l'événement
282
        # corrélé donné par idcorrevent.
283
        events = VigiboardRequest(user, False)
284
        events.add_table(
285
            Event,
286
            events.items.c.hostname,
287
            events.items.c.servicename,
288
        )
289
        events.add_join((EVENTSAGGREGATE_TABLE, \
290
            EVENTSAGGREGATE_TABLE.c.idevent == Event.idevent))
291
        events.add_join((CorrEvent, CorrEvent.idcorrevent == \
292
            EVENTSAGGREGATE_TABLE.c.idcorrevent))
293
        events.add_join((events.items,
294
            Event.idsupitem == events.items.c.idsupitem))
295
        events.add_filter(Event.idevent != CorrEvent.idcause)
296
        events.add_filter(CorrEvent.idcorrevent == idcorrevent)
297

    
298
        # Récupère l'instance de SupItem associé à la cause de
299
        # l'événement corrélé. Cette instance est utilisé pour
300
        # obtenir le nom d'hôte/service auquel la cause est
301
        # rattachée (afin de fournir un contexte à l'utilisateur).
302
        hostname = None
303
        servicename = None
304
        cause_supitem = DBSession.query(
305
                SupItem,
306
            ).join(
307
                (Event, Event.idsupitem == SupItem.idsupitem),
308
                (CorrEvent, Event.idevent == CorrEvent.idcause),
309
            ).filter(CorrEvent.idcorrevent == idcorrevent
310
            ).one()
311

    
312
        if isinstance(cause_supitem, LowLevelService):
313
            hostname = cause_supitem.host.name
314
            servicename = cause_supitem.servicename
315
        elif isinstance(cause_supitem, Host):
316
            hostname = cause_supitem.name
317

    
318
        # Pagination des résultats
319
        events.generate_request()
320
        items_per_page = int(config['vigiboard_items_per_page'])
321
        page = paginate.Page(events.req, page=page,
322
            items_per_page=items_per_page)
323

    
324
        # Vérification que l'événement existe
325
        if not page.item_count:
326
            flash(_('No masked event or access denied'), 'error')
327
            redirect('/')
328

    
329
        return dict(
330
            idcorrevent = idcorrevent,
331
            hostname = hostname,
332
            servicename = servicename,
333
            plugins_data = {},
334
            page = page,
335
            search_form = create_search_form,
336
            search = {},
337
            fixed_search = {},
338
        )
339

    
340

    
341
    class EventSchema(schema.Schema):
342
        """Schéma de validation de la méthode event."""
343
        idevent = validators.Int(not_empty=True)
344
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
345

    
346
    @validate(
347
        validators=EventSchema(),
348
        error_handler = process_form_errors)
349
    @expose('history_table.html')
350
    @require(access_restriction)
351
    def event(self, idevent, page):
352
        """
353
        Affichage de l'historique d'un événement brut.
354
        Pour accéder à cette page, l'utilisateur doit être authentifié.
355

356
        @param idevent: identifiant de l'événement brut souhaité.
357
        @type idevent: C{int}
358
        @param page: numéro de la page à afficher.
359
        @type page: C{int}
360

361
        Cette méthode permet de satisfaire l'exigence
362
        VIGILO_EXIG_VIGILO_BAC_0080.
363
        """
364
        user = get_current_user()
365
        events = VigiboardRequest(user, False)
366
        events.add_table(
367
            Event,
368
            events.items.c.hostname.label('hostname'),
369
            events.items.c.servicename.label('servicename'),
370
        )
371
        events.add_join((EVENTSAGGREGATE_TABLE, \
372
            EVENTSAGGREGATE_TABLE.c.idevent == Event.idevent))
373
        events.add_join((CorrEvent, CorrEvent.idcorrevent == \
374
            EVENTSAGGREGATE_TABLE.c.idcorrevent))
375
        events.add_join((events.items,
376
            Event.idsupitem == events.items.c.idsupitem))
377
        events.add_filter(Event.idevent == idevent)
378

    
379
        if events.num_rows() != 1:
380
            flash(_('No such event or access denied'), 'error')
381
            redirect('/')
382

    
383
        events.format_events(0, 1)
384
        events.generate_tmpl_context()
385
        history = events.format_history()
386

    
387
        # Pagination des résultats
388
        items_per_page = int(config['vigiboard_items_per_page'])
389
        page = paginate.Page(history, page=page, items_per_page=items_per_page)
390
        event = events.req[0]
391

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

    
403

    
404
    class ItemSchema(schema.Schema):
405
        """Schéma de validation de la méthode item."""
406
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
407
        host = validators.String(not_empty=True)
408
        service = validators.String(if_missing=None)
409

    
410
    @validate(
411
        validators=ItemSchema(),
412
        error_handler = process_form_errors)
413
    @expose('events_table.html')
414
    @require(access_restriction)
415
    def item(self, page, host, service):
416
        """
417
        Affichage de l'historique de l'ensemble des événements corrélés
418
        jamais ouverts sur l'hôte / service demandé.
419
        Pour accéder à cette page, l'utilisateur doit être authentifié.
420

421
        @param page: Numéro de la page à afficher.
422
        @param host: Nom de l'hôte souhaité.
423
        @param service: Nom du service souhaité
424

425
        Cette méthode permet de satisfaire l'exigence
426
        VIGILO_EXIG_VIGILO_BAC_0080.
427
        """
428
        idsupitem = SupItem.get_supitem(host, service)
429
        if not idsupitem:
430
            flash(_('No such host/service'), 'error')
431
            redirect('/')
432

    
433
        user = get_current_user()
434
        aggregates = VigiboardRequest(user, False)
435
        aggregates.add_table(
436
            CorrEvent,
437
            aggregates.items.c.hostname,
438
            aggregates.items.c.servicename,
439
        )
440
        aggregates.add_join((Event, CorrEvent.idcause == Event.idevent))
441
        aggregates.add_join((aggregates.items,
442
            Event.idsupitem == aggregates.items.c.idsupitem))
443
        aggregates.add_filter(aggregates.items.c.idsupitem == idsupitem)
444

    
445
        # Pagination des résultats
446
        aggregates.generate_request()
447
        items_per_page = int(config['vigiboard_items_per_page'])
448
        page = paginate.Page(aggregates.req, page=page,
449
            items_per_page=items_per_page)
450

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

    
456
        # Ajout des formulaires et préparation
457
        # des données pour ces formulaires.
458
        ids_events = [event[0].idcause for event in page.items]
459
        tmpl_context.last_modification = \
460
            mktime(get_last_modification_timestamp(ids_events).timetuple())
461

    
462
        tmpl_context.edit_event_form = EditEventForm("edit_event_form",
463
            submit_text=_('Apply'), action=url('/update'))
464

    
465
        return dict(
466
            hostname = host,
467
            servicename = service,
468
            plugins_data = {},
469
            page = page,
470
            event_edit_status_options = edit_event_status_options,
471
            search_form = create_search_form,
472
            search = {},
473
            fixed_search = {},
474
        )
475

    
476

    
477
    class UpdateSchema(schema.Schema):
478
        """Schéma de validation de la méthode update."""
479
        id = validators.Regex(r'^[0-9]+(,[0-9]+)*,?$')
480
        last_modification = validators.Number(not_empty=True)
481
        trouble_ticket = validators.String(if_missing='')
482
        ack = validators.OneOf(
483
            [unicode(s[0]) for s in edit_event_status_options],
484
            not_empty=True)
485

    
486
    @validate(
487
        validators=UpdateSchema(),
488
        error_handler = process_form_errors)
489
    @require(
490
        All(
491
            not_anonymous(msg=l_("You need to be authenticated")),
492
            Any(in_group('managers'),
493
                has_permission('vigiboard-update'),
494
                msg=l_("You don't have write access to VigiBoard"))
495
        ))
496
    @expose()
497
    def update(self, id, last_modification, trouble_ticket, ack):
498
        """
499
        Mise à jour d'un événement suivant les arguments passés.
500
        Cela peut être un changement de ticket ou un changement de statut.
501

502
        @param id: Le ou les identifiants des événements à traiter
503
        @param last_modification: La date de la dernière modification
504
            dont l'utilisateur est au courant.
505
        @param trouble_ticket: Nouveau numéro du ticket associé.
506
        @param ack: Nouvel état d'acquittement des événements sélectionnés.
507

508
        Cette méthode permet de satisfaire les exigences suivantes :
509
            - VIGILO_EXIG_VIGILO_BAC_0020,
510
            - VIGILO_EXIG_VIGILO_BAC_0060,
511
            - VIGILO_EXIG_VIGILO_BAC_0110.
512
        """
513

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

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

    
524
        user = get_current_user()
525
        events = VigiboardRequest(user)
526
        events.add_table(CorrEvent)
527
        events.add_join((Event, CorrEvent.idcause == Event.idevent))
528
        events.add_join((events.items,
529
            Event.idsupitem == events.items.c.idsupitem))
530
        events.add_filter(CorrEvent.idcorrevent.in_(ids))
531

    
532
        events.generate_request()
533
        idevents = [cause.idcause for cause in events.req]
534

    
535
        # Si des changements sont survenus depuis que la
536
        # page est affichée, on en informe l'utilisateur.
537
        last_modification = datetime.fromtimestamp(last_modification)
538
        cur_last_modification = get_last_modification_timestamp(idevents, None)
539
        if cur_last_modification and last_modification < cur_last_modification:
540
            flash(_('Changes have occurred since the page was last displayed, '
541
                    'your changes HAVE NOT been saved.'), 'warning')
542
            raise redirect(request.environ.get('HTTP_REFERER', '/'))
543

    
544
        # Vérification que au moins un des identifiants existe et est éditable
545
        if not events.num_rows():
546
            flash(_('No access to this event'), 'error')
547
            redirect('/')
548

    
549
        if ack == u'Forced':
550
            condition = Any(
551
                in_group('managers'),
552
                has_permission('vigiboard-admin'),
553
                msg=l_("You don't have administrative access "
554
                        "to VigiBoard"))
555
            try:
556
                condition.check_authorization(request.environ)
557
            except NotAuthorizedError, e:
558
                reason = unicode(e)
559
                flash(reason, 'error')
560
                raise redirect(request.environ.get('HTTP_REFERER', '/'))
561

    
562
        # Définit 2 mappings dont les ensembles sont disjoincts
563
        # pour basculer entre la représentation en base de données
564
        # et la représentation "humaine" du bac à événements.
565
        ack_mapping = {
566
            # Permet d'associer la valeur dans le widget ToscaWidgets
567
            # (cf. vigiboard.widgets.edit_event.edit_event_status_options)
568
            # avec la valeur dans la base de données.
569
            u'None': CorrEvent.ACK_NONE,
570
            u'Acknowledged': CorrEvent.ACK_KNOWN,
571
            u'AAClosed': CorrEvent.ACK_CLOSED,
572

    
573
            # Permet d'afficher un libellé plus sympathique pour l'utilisateur
574
            # représentant l'état d'acquittement stocké en base de données.
575
            CorrEvent.ACK_NONE: l_('None'),
576
            CorrEvent.ACK_KNOWN: l_('Acknowledged'),
577
            CorrEvent.ACK_CLOSED: l_('Acknowledged and closed'),
578
        }
579

    
580
        # Toutes les valeurs de ce dictionnaire ne sont pas utilisées
581
        # par cette méthode, néanmoins il permet de marquer les différentes
582
        # valeurs possibles pour le champ "type_action" comme étant à traduire.
583
        valid_types = {
584
            u'Ticket change': l_('Ticket change'),
585
            u'Forced change state': l_('Forced change state'),
586
            u'Acknowledgement change state': l_('Acknowledgement change state'),
587
            u'Ticket change notification': l_('Ticket change notification'),
588
            u'New occurrence': l_('New occurrence'),
589
            u'Nagios update state': l_('Nagios update state'),
590
        }
591

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

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

    
636
                    # Mise à jour de l'état dans State, pour que
637
                    # VigiMap soit également mis à jour.
638
                    DBSession.query(State).filter(
639
                            State.idsupitem == cause.idsupitem,
640
                        ).update({
641
                            'state': StateName.statename_to_value(u'OK'),
642
                        })
643

    
644
                    history = EventHistory(
645
                            type_action=
646
                                unicode(valid_types[u"Forced change state"]),
647
                            idevent=event.idcause,
648
                            value=u'OK',
649
                            text="Forced state to 'OK'",
650
                            username=user.user_name,
651
                            timestamp=datetime.now(),
652
                            state=StateName.statename_to_value(u'OK'),
653
                        )
654
                    DBSession.add(history)
655
                    LOGGER.info(_('User "%(user)s" (%(address)s) forcefully '
656
                                'closed event #%(idevent)d') % {
657
                                    'user': request. \
658
                                            identity['repoze.who.userid'],
659
                                    'address': request.remote_addr,
660
                                    'idevent': event.idcause,
661
                                })
662

    
663
                # Convertit la valeur du widget ToscaWidgets
664
                # vers le code interne puis vers un libellé
665
                # "humain".
666
                ack_label = ack_mapping[ack_mapping[changed_ack]]
667

    
668
                # Si le changement a été forcé,
669
                # on veut le mettre en évidence.
670
                if ack == u'Forced':
671
                    history_label = l_('Forced')
672
                else:
673
                    history_label = ack_label
674

    
675
                history = EventHistory(
676
                        type_action=unicode(
677
                            valid_types[u"Acknowledgement change state"]
678
                        ),
679
                        idevent=event.idcause,
680
                        value=unicode(history_label),
681
                        text=u"Changed acknowledgement status "
682
                            u"from '%s' to '%s'" % (
683
                            ack_mapping[event.ack],
684
                            ack_label,
685
                        ),
686
                        username=user.user_name,
687
                        timestamp=datetime.now(),
688
                    )
689
                DBSession.add(history)
690
                LOGGER.info(_('User "%(user)s" (%(address)s) changed the state '
691
                            'from "%(previous)s" to "%(new)s" on event '
692
                            '#%(idevent)d') % {
693
                                'user': request.identity['repoze.who.userid'],
694
                                'address': request.remote_addr,
695
                                'previous': _(ack_mapping[event.ack]),
696
                                'new': _(ack_label),
697
                                'idevent': event.idcause,
698
                            })
699
                event.ack = ack_mapping[changed_ack]
700

    
701
        DBSession.flush()
702
        flash(_('Updated successfully'))
703
        redirect(request.environ.get('HTTP_REFERER', '/'))
704

    
705

    
706
    class GetPluginValueSchema(schema.Schema):
707
        """Schéma de validation de la méthode get_plugin_value."""
708
        idcorrevent = validators.Int(not_empty=True)
709
        plugin_name = validators.String(not_empty=True)
710
        # Permet de passer des paramètres supplémentaires au plugin.
711
        allow_extra_fields = True
712

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

    
724
        # Vérification de l'existence du plugin
725
        plugins = dict(config['columns_plugins'])
726
        if plugin_name not in plugins:
727
            raise HTTPNotFound(_("No such plugin '%s'") % plugin_name)
728

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

    
732
        # Filtrage des évènements en fonction des permissions de
733
        # l'utilisateur (s'il n'appartient pas au groupe 'managers')
734
        is_manager = in_group('managers').is_met(request.environ)
735
        if not is_manager:
736

    
737
            user = get_current_user()
738

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

    
763
        # Filtrage des évènements en fonction
764
        # de l'identifiant passé en paramètre
765
        events = events.filter(CorrEvent.idcorrevent == idcorrevent).count()
766

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

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

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

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

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

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

817
        @param parent_id: identifiant du groupe d'hôte parent
818
        @type  parent_id: C{int} or None
819
        """
820

    
821
        # Si l'identifiant du groupe parent n'est pas
822
        # spécifié, on retourne la liste des groupes
823
        # racines, fournie par la méthode get_root_groups.
824
        if parent_id is None:
825
            return self.get_root_groups()
826

    
827
        # TODO: Utiliser un schéma de validation
828
        parent_id = int(parent_id)
829
        offset = int(offset)
830

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

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

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

    
889
        return result
890

    
891
    def get_root_groups(self):
892
        """
893
        Retourne tous les groupes racines (c'est à dire n'ayant
894
        aucun parent) d'hôtes auquel l'utilisateur a accès.
895

896
        @return: Un dictionnaire contenant la liste de ces groupes.
897
        @rtype : C{dict} of C{list} of C{dict} of C{mixed}
898
        """
899

    
900
        # On récupère tous les groupes qui ont un parent.
901
        children = DBSession.query(
902
            SupItemGroup,
903
        ).distinct(
904
        ).join(
905
            (GroupHierarchy, GroupHierarchy.idchild == SupItemGroup.idgroup)
906
        ).filter(GroupHierarchy.hops > 0)
907

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

    
916
        # On filtre ces groupes racines afin de ne
917
        # retourner que ceux auquels l'utilisateur a accès
918
        user = get_current_user()
919
        is_manager = in_group('managers').is_met(request.environ)
920
        if not is_manager:
921

    
922
            root_groups = root_groups.join(
923
                (GroupHierarchy,
924
                    GroupHierarchy.idparent == SupItemGroup.idgroup),
925
                (DataPermission,
926
                    DataPermission.idgroup == GroupHierarchy.idchild),
927
                (USER_GROUP_TABLE, USER_GROUP_TABLE.c.idgroup == \
928
                    DataPermission.idusergroup),
929
            ).filter(USER_GROUP_TABLE.c.username == user.user_name)
930

    
931
        groups = []
932
        for group in root_groups.all():
933
            groups.append({
934
                'id'   : group.idgroup,
935
                'name' : group.name,
936
                'type' : "group",
937
            })
938

    
939
        return dict(groups=groups, items=[])
940

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