Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

vigiboard / vigiboard / controllers / root.py @ 338575f6

History | View | Annotate | Download (38 KB)

1
# -*- coding: utf-8 -*-
2
# vim:set expandtab tabstop=4 shiftwidth=4:
3
################################################################################
4
#
5
# Copyright (C) 2007-2011 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
72
class RootController(AuthController):
73
    """
74
    Le controller général de vigiboard
75
    """
76
    error = ErrorController()
77
    autocomplete = AutoCompleteController()
78
    nagios = ProxyController('nagios', '/nagios/',
79
        not_anonymous(l_('You need to be authenticated')))
80
    api = ApiRootController("/api")
81
    feeds = FeedsController()
82

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

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

    
102
    @expose('json')
103
    def handle_validation_errors_json(self, *args, **kwargs):
104
        kwargs['errors'] = tmpl_context.form_errors
105
        return dict(kwargs)
106

    
107
    class DefaultSchema(schema.Schema):
108
        """Schéma de validation de la méthode default."""
109
        # Si on ne passe pas le paramètre "page" ou qu'on passe une valeur
110
        # invalide ou pas de valeur du tout, alors on affiche la 1ère page.
111
        page = validators.Int(min=1, if_missing=1, if_invalid=1, not_empty=True)
112

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

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

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

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

137
        Cette méthode permet de satisfaire les exigences suivantes :
138
            - VIGILO_EXIG_VIGILO_BAC_0040,
139
            - VIGILO_EXIG_VIGILO_BAC_0070,
140
            - VIGILO_EXIG_VIGILO_BAC_0100,
141
        """
142
        user = get_current_user()
143
        aggregates = VigiboardRequest(user, search=search)
144

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

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

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

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

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

    
188
        # Récupération des données des plugins
189
        plugins_data = {}
190
        plugins = dict(config['columns_plugins'])
191

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

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

    
204
        tmpl_context.edit_event_form = EditEventForm("edit_event_form",
205
            submit_text=_('Apply'), action=url('/update'))
206

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

    
218

    
219
    @expose()
220
    def i18n(self):
221
        import gettext
222
        import pylons
223
        import os.path
224

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

    
233
        lang = get_lang()
234

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

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

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

    
252
        fhandle = open(themes_js, 'r')
253
        translations += fhandle.read()
254
        fhandle.close()
255
        return translations
256

    
257

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

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

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

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

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

    
309
        if isinstance(cause_supitem, LowLevelService):
310
            hostname = cause_supitem.host.name
311
            servicename = cause_supitem.servicename
312
        elif isinstance(cause_supitem, Host):
313
            hostname = cause_supitem.name
314

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

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

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

    
337

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

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

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

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

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

    
380
        events.format_events(0, 1)
381
        events.generate_tmpl_context()
382
        history = events.format_history()
383

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

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

    
400

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

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

418
        @param page: Numéro de la page à afficher.
419
        @param host: Nom de l'hôte souhaité.
420
        @param service: Nom du service souhaité
421

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

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

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

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

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

    
459
        tmpl_context.edit_event_form = EditEventForm("edit_event_form",
460
            submit_text=_('Apply'), action=url('/update'))
461

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

    
473

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

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

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

505
        Cette méthode permet de satisfaire les exigences suivantes :
506
            - VIGILO_EXIG_VIGILO_BAC_0020,
507
            - VIGILO_EXIG_VIGILO_BAC_0060,
508
            - VIGILO_EXIG_VIGILO_BAC_0110.
509
        """
510

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

    
517
        # On récupère la liste de tous les identifiants des événements
518
        # à mettre à jour.
519
        ids = map(int, id.strip(',').split(','))
520

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

    
529
        events.generate_request()
530
        idevents = [cause.idcause for cause in events.req]
531

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

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

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

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

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

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

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

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

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

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

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

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

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

    
698
        DBSession.flush()
699
        flash(_('Updated successfully'))
700
        redirect(request.environ.get('HTTP_REFERER', '/'))
701

    
702

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

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

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

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

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

    
734
            user = get_current_user()
735

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

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

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

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

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

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

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

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

814
        @param parent_id: identifiant du groupe d'hôte parent
815
        @type  parent_id: C{int} or None
816
        """
817

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

    
824
        # TODO: Utiliser un schéma de validation
825
        parent_id = int(parent_id)
826
        offset = int(offset)
827

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

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

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

    
886
        return result
887

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

893
        @return: Un dictionnaire contenant la liste de ces groupes.
894
        @rtype : C{dict} of C{list} of C{dict} of C{mixed}
895
        """
896

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

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

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

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

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

    
936
        return dict(groups=groups, items=[])
937

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