Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

vigiboard / vigiboard / controllers / root.py @ 5a845c93

History | View | Annotate | Download (41 KB)

1
# -*- coding: utf-8 -*-
2
# vim:set expandtab tabstop=4 shiftwidth=4:
3
################################################################################
4
#
5
# Copyright (C) 2007-2013 CS-SI
6
#
7
# This program is free software; you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License version 2 as
9
# published by the Free Software Foundation.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
19
################################################################################
20

    
21
"""VigiBoard Controller"""
22

    
23
from datetime import datetime
24
from time import mktime
25

    
26
from pkg_resources import resource_filename, working_set
27

    
28
from tg.exceptions import HTTPNotFound
29
from tg import expose, validate, require, flash, url, \
30
    tmpl_context, request, response, config, session, redirect
31
from webhelpers import paginate
32
from tw.forms import validators
33
from pylons.i18n import ugettext as _, lazy_ugettext as l_, get_lang
34
from sqlalchemy import asc
35
from sqlalchemy.sql import func
36
from sqlalchemy.orm import aliased
37
from sqlalchemy.sql.expression import or_
38
from repoze.what.predicates import Any, All, 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.selfmonitoring import SelfMonitoringController
53
from vigilo.turbogears.controllers.custom import CustomController
54
from vigilo.turbogears.controllers.error import ErrorController
55
from vigilo.turbogears.controllers.autocomplete import AutoCompleteController
56
from vigilo.turbogears.controllers.proxy import ProxyController
57
from vigilo.turbogears.controllers.api.root import ApiRootController
58
from vigilo.turbogears.helpers import get_current_user
59

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

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

    
69
LOGGER = logging.getLogger(__name__)
70

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
273

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

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

    
288
        lang = get_lang()
289

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

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

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

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

    
312

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

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

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

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

    
337
        user = get_current_user()
338

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

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

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

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

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

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

    
398

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

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

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

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

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

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

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

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

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

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

    
465

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
563

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
788

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
971
        return result
972

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

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

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

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

    
998
        # On filtre ces groupes racines afin de ne
999
        # retourner que ceux auquels l'utilisateur a accès
1000
        user = get_current_user()
1001
        is_manager = in_group('managers').is_met(request.environ)
1002
        if not is_manager:
1003

    
1004
            root_groups = root_groups.join(
1005
                (GroupHierarchy,
1006
                    GroupHierarchy.idparent == SupItemGroup.idgroup),
1007
                (DataPermission,
1008
                    DataPermission.idgroup == GroupHierarchy.idchild),
1009
                (USER_GROUP_TABLE, USER_GROUP_TABLE.c.idgroup == \
1010
                    DataPermission.idusergroup),
1011
            ).filter(USER_GROUP_TABLE.c.username == user.user_name)
1012

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

    
1021
        return dict(groups=groups, items=[])
1022

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

    
1038
    if not last_modification_timestamp:
1039
        if not value_if_none:
1040
            return None
1041
        else:
1042
            last_modification_timestamp = value_if_none
1043
    return datetime.fromtimestamp(mktime(
1044
        last_modification_timestamp.timetuple()))