Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

vigiboard / vigiboard / controllers / root.py @ 9b8d9497

History | View | Annotate | Download (40.9 KB)

1
# -*- coding: utf-8 -*-
2
# vim:set expandtab tabstop=4 shiftwidth=4:
3
# Copyright (C) 2007-2016 CS-SI
4
# License: GNU GPL v2 <http://www.gnu.org/licenses/gpl-2.0.html>
5

    
6
"""VigiBoard Controller"""
7

    
8
from datetime import datetime
9
from time import mktime
10

    
11
from pkg_resources import resource_filename, working_set
12

    
13
from tg.exceptions import HTTPNotFound
14
from tg.controllers import CUSTOM_CONTENT_TYPE
15
from tg import expose, validate, require, flash, url, \
16
    tmpl_context, request, response, config, session, redirect
17
from webhelpers import paginate
18
from tw.forms import validators
19
from pylons.i18n import ugettext as _, lazy_ugettext as l_, get_lang
20
from sqlalchemy import asc
21
from sqlalchemy.sql import func
22
from sqlalchemy.orm import aliased
23
from sqlalchemy.sql.expression import or_
24
from repoze.what.predicates import Any, All, NotAuthorizedError, \
25
                                    has_permission, not_anonymous
26
from formencode import schema
27

    
28
from vigilo.models.session import DBSession
29
from vigilo.models.tables import Event, EventHistory, CorrEvent, Host, \
30
                                    SupItem, SupItemGroup, LowLevelService, \
31
                                    StateName, State, DataPermission
32
from vigilo.models.tables.grouphierarchy import GroupHierarchy
33
from vigilo.models.tables.secondary_tables import EVENTSAGGREGATE_TABLE, \
34
        USER_GROUP_TABLE, SUPITEM_GROUP_TABLE
35

    
36
from vigilo.turbogears.controllers.auth import AuthController
37
from vigilo.turbogears.controllers.selfmonitoring import SelfMonitoringController
38
from vigilo.turbogears.controllers.custom import CustomController
39
from vigilo.turbogears.controllers.error import ErrorController
40
from vigilo.turbogears.controllers.autocomplete import AutoCompleteController
41
from vigilo.turbogears.controllers.proxy import ProxyController
42
from vigilo.turbogears.controllers.api.root import ApiRootController
43
from vigilo.turbogears.helpers import get_current_user
44

    
45
from vigiboard.controllers.vigiboardrequest import VigiboardRequest
46
from vigiboard.controllers.feeds import FeedsController
47
from vigiboard.controllers.silence import SilenceController
48

    
49
from vigiboard.lib import export_csv, dateformat
50
from vigiboard.widgets.edit_event import edit_event_status_options, \
51
                                            EditEventForm
52
from vigiboard.widgets.search_form import create_search_form
53
import logging
54

    
55
LOGGER = logging.getLogger(__name__)
56

    
57
__all__ = ('RootController', 'get_last_modification_timestamp',
58
           'date_to_timestamp')
59

    
60
# pylint: disable-msg=R0201,W0613,W0622
61
# R0201: Method could be a function
62
# W0613: Unused arguments: les arguments sont la query-string
63
# W0622: Redefining built-in 'id': élément de la query-string
64

    
65
class RootController(AuthController, SelfMonitoringController):
66
    """
67
    Le controller général de vigiboard
68
    """
69
    _tickets = None
70

    
71
    error = ErrorController()
72
    autocomplete = AutoCompleteController()
73
    nagios = ProxyController('nagios', '/nagios/',
74
        not_anonymous(l_('You need to be authenticated')))
75
    api = ApiRootController()
76
    feeds = FeedsController()
77
    silence = SilenceController()
78
    custom = CustomController()
79

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

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

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

    
104
    def __init__(self, *args, **kwargs):
105
        """Initialisation du contrôleur."""
106
        super(RootController, self).__init__(*args, **kwargs)
107
        # Si un module de gestion des tickets a été indiqué dans
108
        # le fichier de configuration, on tente de le charger.
109
        if config.get('tickets.plugin'):
110
            plugins = working_set.iter_entry_points('vigiboard.tickets', config['tickets.plugin'])
111
            if plugins:
112
                # La classe indiquée par la première valeur de l'itérateur
113
                # correspond au plugin que l'on veut instancier.
114
                pluginCls = plugins.next().load()
115
                self._tickets = pluginCls()
116

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

    
128
        # Paramètres de tri
129
        sort = validators.String(if_missing=None)
130
        order = validators.OneOf(['asc', 'desc'], if_missing='asc')
131

    
132
        # Nécessaire pour que les critères de recherche soient conservés.
133
        allow_extra_fields = True
134

    
135
        # 2ème validation, cette fois avec les champs
136
        # du formulaire de recherche.
137
        chained_validators = [create_search_form.validator]
138

    
139
    @validate(
140
        validators=IndexSchema(),
141
        error_handler = process_form_errors)
142
    @expose('events_table.html', content_type=CUSTOM_CONTENT_TYPE)
143
    @require(access_restriction)
144
    def index(self, page, sort=None, order=None, **search):
145
        """
146
        Page d'accueil de Vigiboard. Elle affiche, suivant la page demandée
147
        (page 1 par defaut), la liste des événements, rangés par ordre de prise
148
        en compte, puis de sévérité.
149
        Pour accéder à cette page, l'utilisateur doit être authentifié.
150

151
        @param page: Numéro de la page souhaitée, commence à 1
152
        @type page: C{int}
153
        @param sort: Colonne de tri
154
        @type sort: C{str} or C{None}
155
        @param order: Ordre du tri (asc ou desc)
156
        @type order: C{str} or C{None}
157
        @param search: Dictionnaire contenant les critères de recherche.
158
        @type search: C{dict}
159

160
        Cette méthode permet de satisfaire les exigences suivantes :
161
            - VIGILO_EXIG_VIGILO_BAC_0040,
162
            - VIGILO_EXIG_VIGILO_BAC_0070,
163
            - VIGILO_EXIG_VIGILO_BAC_0100,
164
        """
165

    
166
        # Auto-supervision
167
        self.get_failures()
168

    
169
        user = get_current_user()
170
        aggregates = VigiboardRequest(user, search=search, sort=sort, order=order)
171

    
172
        aggregates.add_table(
173
            CorrEvent,
174
            aggregates.items.c.hostname,
175
            aggregates.items.c.servicename
176
        )
177
        aggregates.add_join((Event, CorrEvent.idcause == Event.idevent))
178
        aggregates.add_contains_eager(CorrEvent.cause)
179
        aggregates.add_group_by(Event)
180
        aggregates.add_join((aggregates.items,
181
            Event.idsupitem == aggregates.items.c.idsupitem))
182
        aggregates.add_order_by(asc(aggregates.items.c.hostname))
183

    
184
        # Certains arguments sont réservés dans routes.util.url_for().
185
        # On effectue les substitutions adéquates.
186
        # Par exemple: "host" devient "host_".
187
        reserved = ('host', 'anchor', 'protocol', 'qualified')
188
        for column in search.copy():
189
            if column in reserved:
190
                search[column + '_'] = search[column]
191
                del search[column]
192

    
193
        # On ne garde que les champs effectivement renseignés.
194
        for column in search.copy():
195
            if not search[column]:
196
                del search[column]
197

    
198
        # On sérialise les champs de type dict.
199
        def serialize_dict(dct, key):
200
            if isinstance(dct[key], dict):
201
                for subkey in dct[key]:
202
                    serialize_dict(dct[key], subkey)
203
                    dct['%s.%s' % (key, subkey)] = dct[key][subkey]
204
                del dct[key]
205
            elif isinstance(dct[key], datetime):
206
                dct[key] = dct[key].strftime(dateformat.get_date_format())
207
        fixed_search = search.copy()
208
        for column in fixed_search.copy():
209
            serialize_dict(fixed_search, column)
210

    
211
        # Pagination des résultats
212
        aggregates.generate_request()
213
        items_per_page = int(session.get('items_per_page', config['vigiboard_items_per_page']))
214
        page = paginate.Page(aggregates.req, page=page,
215
            items_per_page=items_per_page)
216

    
217
        # Récupération des données des plugins
218
        plugins_data = {}
219
        plugins = dict(config['columns_plugins'])
220

    
221
        ids_events = [event[0].idcause for event in page.items]
222
        ids_correvents = [event[0].idcorrevent for event in page.items]
223
        for plugin in plugins:
224
            plugin_data = plugins[plugin].get_bulk_data(ids_correvents)
225
            if plugin_data:
226
                plugins_data[plugin] = plugin_data
227
            else:
228
                plugins_data[plugin] = {}
229

    
230
        # Ajout des formulaires et préparation
231
        # des données pour ces formulaires.
232
        tmpl_context.last_modification = \
233
            mktime(get_last_modification_timestamp(ids_events).timetuple())
234

    
235
        tmpl_context.edit_event_form = EditEventForm("edit_event_form",
236
            submit_text=_('Apply'), action=url('/update'))
237

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

    
244
            response.headers["Content-Type"] = "text/csv"
245
            response.headers['Content-Disposition'] = \
246
                            'attachment;filename="alerts.csv"'
247
            return export_csv.export(page, plugins_data)
248

    
249
        return dict(
250
            hostname = None,
251
            servicename = None,
252
            plugins_data = plugins_data,
253
            page = page,
254
            sort = sort,
255
            order = order,
256
            event_edit_status_options = edit_event_status_options,
257
            search_form = create_search_form,
258
            search = search,
259
            fixed_search = fixed_search,
260
        )
261

    
262

    
263
    @expose()
264
    def i18n(self):
265
        import gettext
266
        import pylons
267
        import os.path
268

    
269
        # Repris de pylons.i18n.translation:_get_translator.
270
        conf = pylons.config.current_conf()
271
        try:
272
            rootdir = conf['pylons.paths']['root']
273
        except KeyError:
274
            rootdir = conf['pylons.paths'].get('root_path')
275
        localedir = os.path.join(rootdir, 'i18n')
276

    
277
        lang = get_lang()
278

    
279
        # Localise le fichier *.mo actuellement chargé
280
        # et génère le chemin jusqu'au *.js correspondant.
281
        filename = gettext.find(conf['pylons.package'], localedir,
282
            languages=lang)
283
        js = filename[:-3] + '.js'
284

    
285
        themes_filename = gettext.find(
286
            'vigilo-themes',
287
            resource_filename('vigilo.themes.i18n', ''),
288
            languages=lang)
289
        themes_js = themes_filename[:-3] + '.js'
290

    
291
        # Récupère et envoie le contenu du fichier de traduction *.js.
292
        fhandle = open(js, 'r')
293
        translations = fhandle.read()
294
        fhandle.close()
295

    
296
        fhandle = open(themes_js, 'r')
297
        translations += fhandle.read()
298
        fhandle.close()
299
        return translations
300

    
301

    
302
    class MaskedEventsSchema(schema.Schema):
303
        """Schéma de validation de la méthode masked_events."""
304
        idcorrevent = validators.Int(not_empty=True)
305
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
306

    
307
    @validate(
308
        validators=MaskedEventsSchema(),
309
        error_handler = process_form_errors)
310
    @expose('raw_events_table.html')
311
    @require(access_restriction)
312
    def masked_events(self, idcorrevent, page):
313
        """
314
        Affichage de la liste des événements bruts masqués d'un événement
315
        corrélé (événements agrégés dans l'événement corrélé).
316

317
        @param page: numéro de la page à afficher.
318
        @type  page: C{int}
319
        @param idcorrevent: identifiant de l'événement corrélé souhaité.
320
        @type  idcorrevent: C{int}
321
        """
322

    
323
        # Auto-supervision
324
        self.get_failures()
325

    
326
        user = get_current_user()
327

    
328
        # Récupère la liste des événements masqués de l'événement
329
        # corrélé donné par idcorrevent.
330
        events = VigiboardRequest(user, False)
331
        events.add_table(
332
            Event,
333
            events.items.c.hostname,
334
            events.items.c.servicename,
335
        )
336
        events.add_join((EVENTSAGGREGATE_TABLE, \
337
            EVENTSAGGREGATE_TABLE.c.idevent == Event.idevent))
338
        events.add_join((CorrEvent, CorrEvent.idcorrevent == \
339
            EVENTSAGGREGATE_TABLE.c.idcorrevent))
340
        events.add_join((events.items,
341
            Event.idsupitem == events.items.c.idsupitem))
342
        events.add_filter(Event.idevent != CorrEvent.idcause)
343
        events.add_filter(CorrEvent.idcorrevent == idcorrevent)
344

    
345
        # Récupère l'instance de SupItem associé à la cause de
346
        # l'événement corrélé. Cette instance est utilisé pour
347
        # obtenir le nom d'hôte/service auquel la cause est
348
        # rattachée (afin de fournir un contexte à l'utilisateur).
349
        hostname = None
350
        servicename = None
351
        cause_supitem = DBSession.query(
352
                SupItem,
353
            ).join(
354
                (Event, Event.idsupitem == SupItem.idsupitem),
355
                (CorrEvent, Event.idevent == CorrEvent.idcause),
356
            ).filter(CorrEvent.idcorrevent == idcorrevent
357
            ).one()
358

    
359
        if isinstance(cause_supitem, LowLevelService):
360
            hostname = cause_supitem.host.name
361
            servicename = cause_supitem.servicename
362
        elif isinstance(cause_supitem, Host):
363
            hostname = cause_supitem.name
364

    
365
        # Pagination des résultats
366
        events.generate_request()
367
        items_per_page = int(session.get('items_per_page', config['vigiboard_items_per_page']))
368
        page = paginate.Page(events.req, page=page,
369
            items_per_page=items_per_page)
370

    
371
        # Vérification que l'événement existe
372
        if not page.item_count:
373
            flash(_('No masked event or access denied'), 'error')
374
            redirect('/')
375

    
376
        return dict(
377
            idcorrevent = idcorrevent,
378
            hostname = hostname,
379
            servicename = servicename,
380
            plugins_data = {},
381
            page = page,
382
            search_form = create_search_form,
383
            search = {},
384
            fixed_search = {},
385
        )
386

    
387

    
388
    class EventSchema(schema.Schema):
389
        """Schéma de validation de la méthode event."""
390
        idevent = validators.Int(not_empty=True)
391
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
392

    
393
    @validate(
394
        validators=EventSchema(),
395
        error_handler = process_form_errors)
396
    @expose('history_table.html')
397
    @require(access_restriction)
398
    def event(self, idevent, page):
399
        """
400
        Affichage de l'historique d'un événement brut.
401
        Pour accéder à cette page, l'utilisateur doit être authentifié.
402

403
        @param idevent: identifiant de l'événement brut souhaité.
404
        @type idevent: C{int}
405
        @param page: numéro de la page à afficher.
406
        @type page: C{int}
407

408
        Cette méthode permet de satisfaire l'exigence
409
        VIGILO_EXIG_VIGILO_BAC_0080.
410
        """
411

    
412
        # Auto-supervision
413
        self.get_failures()
414

    
415
        user = get_current_user()
416
        events = VigiboardRequest(user, False)
417
        events.add_table(
418
            Event,
419
            events.items.c.hostname.label('hostname'),
420
            events.items.c.servicename.label('servicename'),
421
        )
422
        events.add_join((EVENTSAGGREGATE_TABLE, \
423
            EVENTSAGGREGATE_TABLE.c.idevent == Event.idevent))
424
        events.add_join((CorrEvent, CorrEvent.idcorrevent == \
425
            EVENTSAGGREGATE_TABLE.c.idcorrevent))
426
        events.add_join((events.items,
427
            Event.idsupitem == events.items.c.idsupitem))
428
        events.add_filter(Event.idevent == idevent)
429

    
430
        if events.num_rows() != 1:
431
            flash(_('No such event or access denied'), 'error')
432
            redirect('/')
433

    
434
        events.format_events(0, 1)
435
        events.generate_tmpl_context()
436
        history = events.format_history()
437

    
438
        # Pagination des résultats
439
        items_per_page = int(session.get('items_per_page', config['vigiboard_items_per_page']))
440
        page = paginate.Page(history, page=page, items_per_page=items_per_page)
441
        event = events.req[0]
442

    
443
        return dict(
444
            idevent = idevent,
445
            hostname = event.hostname,
446
            servicename = event.servicename,
447
            plugins_data = {},
448
            page = page,
449
            search_form = create_search_form,
450
            search = {},
451
            fixed_search = {},
452
        )
453

    
454

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

    
461
        # Paramètres de tri
462
        sort = validators.String(if_missing=None)
463
        order = validators.OneOf(['asc', 'desc'], if_missing='asc')
464

    
465
        # L'hôte / service dont on doit afficher les évènements
466
        host = validators.String(not_empty=True)
467
        service = validators.String(if_missing=None)
468

    
469
    @validate(
470
        validators=ItemSchema(),
471
        error_handler = process_form_errors)
472
    @expose('events_table.html')
473
    @require(access_restriction)
474
    def item(self, page, host, service, sort=None, order=None):
475
        """
476
        Affichage de l'historique de l'ensemble des événements corrélés
477
        jamais ouverts sur l'hôte / service demandé.
478
        Pour accéder à cette page, l'utilisateur doit être authentifié.
479

480
        @param page: Numéro de la page à afficher.
481
        @type: C{int}
482
        @param host: Nom de l'hôte souhaité.
483
        @type: C{str}
484
        @param service: Nom du service souhaité
485
        @type: C{str}
486
        @param sort: Colonne de tri
487
        @type: C{str} or C{None}
488
        @param order: Ordre du tri (asc ou desc)
489
        @type: C{str} or C{None}
490

491
        Cette méthode permet de satisfaire l'exigence
492
        VIGILO_EXIG_VIGILO_BAC_0080.
493
        """
494

    
495
        # Auto-supervision
496
        self.get_failures()
497

    
498
        idsupitem = SupItem.get_supitem(host, service)
499
        if not idsupitem:
500
            flash(_('No such host/service'), 'error')
501
            redirect('/')
502

    
503
        user = get_current_user()
504
        aggregates = VigiboardRequest(user, False, sort=sort, order=order)
505
        aggregates.add_table(
506
            CorrEvent,
507
            aggregates.items.c.hostname,
508
            aggregates.items.c.servicename,
509
        )
510
        aggregates.add_join((Event, CorrEvent.idcause == Event.idevent))
511
        aggregates.add_join((aggregates.items,
512
            Event.idsupitem == aggregates.items.c.idsupitem))
513
        aggregates.add_filter(aggregates.items.c.idsupitem == idsupitem)
514

    
515
        # Pagination des résultats
516
        aggregates.generate_request()
517
        items_per_page = int(session.get('items_per_page', config['vigiboard_items_per_page']))
518
        page = paginate.Page(aggregates.req, page=page,
519
            items_per_page=items_per_page)
520

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

    
526
        # Ajout des formulaires et préparation
527
        # des données pour ces formulaires.
528
        ids_events = [event[0].idcause for event in page.items]
529
        tmpl_context.last_modification = \
530
            mktime(get_last_modification_timestamp(ids_events).timetuple())
531

    
532
        tmpl_context.edit_event_form = EditEventForm("edit_event_form",
533
            submit_text=_('Apply'), action=url('/update'))
534

    
535
        plugins_data = {}
536
        for plugin in dict(config['columns_plugins']):
537
            plugins_data[plugin] = {}
538

    
539
        return dict(
540
            hostname = host,
541
            servicename = service,
542
            plugins_data = plugins_data,
543
            page = page,
544
            sort = sort,
545
            order = order,
546
            event_edit_status_options = edit_event_status_options,
547
            search_form = create_search_form,
548
            search = {},
549
            fixed_search = {},
550
        )
551

    
552

    
553
    class UpdateSchema(schema.Schema):
554
        """Schéma de validation de la méthode update."""
555
        id = validators.Regex(r'^[0-9]+(,[0-9]+)*,?$')
556
        last_modification = validators.Number(not_empty=True)
557
        trouble_ticket = validators.String(if_missing='')
558
        ack = validators.OneOf(
559
            [unicode(s[0]) for s in edit_event_status_options],
560
            not_empty=True)
561

    
562
    @validate(
563
        validators=UpdateSchema(),
564
        error_handler = process_form_errors)
565
    @require(
566
        All(
567
            not_anonymous(msg=l_("You need to be authenticated")),
568
            Any(config.is_manager,
569
                has_permission('vigiboard-update'),
570
                msg=l_("You don't have write access to VigiBoard"))
571
        ))
572
    @expose()
573
    def update(self, id, last_modification, trouble_ticket, ack):
574
        """
575
        Mise à jour d'un événement suivant les arguments passés.
576
        Cela peut être un changement de ticket ou un changement de statut.
577

578
        @param id: Le ou les identifiants des événements à traiter
579
        @param last_modification: La date de la dernière modification
580
            dont l'utilisateur est au courant.
581
        @param trouble_ticket: Nouveau numéro du ticket associé.
582
        @param ack: Nouvel état d'acquittement des événements sélectionnés.
583

584
        Cette méthode permet de satisfaire les exigences suivantes :
585
            - VIGILO_EXIG_VIGILO_BAC_0020,
586
            - VIGILO_EXIG_VIGILO_BAC_0060,
587
            - VIGILO_EXIG_VIGILO_BAC_0110.
588
        """
589

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

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

    
600
        user = get_current_user()
601
        events = VigiboardRequest(user)
602
        events.add_table(
603
            CorrEvent,
604
            Event,
605
            events.items.c.hostname,
606
            events.items.c.servicename,
607
        )
608
        events.add_join((Event, CorrEvent.idcause == Event.idevent))
609
        events.add_join((events.items,
610
            Event.idsupitem == events.items.c.idsupitem))
611
        events.add_filter(CorrEvent.idcorrevent.in_(ids))
612

    
613
        events.generate_request()
614
        idevents = [event[0].idcause for event in events.req]
615

    
616
        # Si des changements sont survenus depuis que la
617
        # page est affichée, on en informe l'utilisateur.
618
        last_modification = datetime.fromtimestamp(last_modification)
619
        cur_last_modification = get_last_modification_timestamp(idevents, None)
620
        if cur_last_modification and last_modification < cur_last_modification:
621
            flash(_('Changes have occurred since the page was last displayed, '
622
                    'your changes HAVE NOT been saved.'), 'warning')
623
            raise redirect(request.environ.get('HTTP_REFERER', '/'))
624

    
625
        # Vérification que au moins un des identifiants existe et est éditable
626
        if not events.num_rows():
627
            flash(_('No access to this event'), 'error')
628
            redirect('/')
629

    
630
        if ack == u'Forced':
631
            condition = Any(
632
                config.is_manager,
633
                has_permission('vigiboard-admin'),
634
                msg=l_("You don't have administrative access "
635
                        "to VigiBoard"))
636
            try:
637
                condition.check_authorization(request.environ)
638
            except NotAuthorizedError, e:
639
                reason = unicode(e)
640
                flash(reason, 'error')
641
                raise redirect(request.environ.get('HTTP_REFERER', '/'))
642

    
643
        # Si un module de gestion de ticket est utilisé,
644
        # il a la possibilité de changer à la volée le libellé du ticket.
645
        if self._tickets:
646
            trouble_ticket = self._tickets.createTicket(events.req, trouble_ticket)
647

    
648
        # Définit 2 mappings dont les ensembles sont disjoincts
649
        # pour basculer entre la représentation en base de données
650
        # et la représentation "humaine" du bac à événements.
651
        ack_mapping = {
652
            # Permet d'associer la valeur dans le widget ToscaWidgets
653
            # (cf. vigiboard.widgets.edit_event.edit_event_status_options)
654
            # avec la valeur dans la base de données.
655
            u'None': CorrEvent.ACK_NONE,
656
            u'Acknowledged': CorrEvent.ACK_KNOWN,
657
            u'AAClosed': CorrEvent.ACK_CLOSED,
658

    
659
            # Permet d'afficher un libellé plus sympathique pour l'utilisateur
660
            # représentant l'état d'acquittement stocké en base de données.
661
            CorrEvent.ACK_NONE: l_('None'),
662
            CorrEvent.ACK_KNOWN: l_('Acknowledged'),
663
            CorrEvent.ACK_CLOSED: l_('Acknowledged and closed'),
664
        }
665

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

    
695
            # Changement du statut d'acquittement.
696
            if ack != u'NoChange':
697
                changed_ack = ack
698
                # Pour forcer l'acquittement d'un événement,
699
                # il faut en plus avoir la permission
700
                # "vigiboard-admin".
701
                if ack == u'Forced':
702
                    changed_ack = u'AAClosed'
703
                    cause = event.cause
704
                    # On met systématiquement l'événement à l'état "OK",
705
                    # même s'il s'agit d'un hôte.
706
                    # Techniquement, c'est incorrect, mais on fait ça
707
                    # pour masquer l'événement de toutes façons...
708
                    cause.current_state = \
709
                        StateName.statename_to_value(u'OK')
710

    
711
                    # Mise à jour de l'état dans State, pour que
712
                    # VigiMap soit également mis à jour.
713
                    DBSession.query(State).filter(
714
                            State.idsupitem == cause.idsupitem,
715
                        ).update({
716
                            'state': StateName.statename_to_value(u'OK'),
717
                        })
718

    
719
                    history = EventHistory(
720
                            type_action=u"Forced change state",
721
                            idevent=event.idcause,
722
                            value=u'OK',
723
                            text="Forced state to 'OK'",
724
                            username=user.user_name,
725
                            timestamp=datetime.now(),
726
                            state=StateName.statename_to_value(u'OK'),
727
                        )
728
                    DBSession.add(history)
729
                    LOGGER.info(_('User "%(user)s" (%(address)s) forcefully '
730
                                'closed event #%(idevent)d') % {
731
                                    'user': request. \
732
                                            identity['repoze.who.userid'],
733
                                    'address': request.remote_addr,
734
                                    'idevent': event.idcause,
735
                                })
736

    
737
                # Convertit la valeur du widget ToscaWidgets
738
                # vers le code interne puis vers un libellé
739
                # "humain".
740
                ack_label = ack_mapping[ack_mapping[changed_ack]]
741

    
742
                # Si le changement a été forcé,
743
                # on veut le mettre en évidence.
744
                if ack == u'Forced':
745
                    history_label = u'Forced'
746
                else:
747
                    history_label = ack_label
748

    
749
                history = EventHistory(
750
                        type_action=u"Acknowledgement change state",
751
                        idevent=event.idcause,
752
                        value=unicode(history_label),
753
                        text=u"Changed acknowledgement status "
754
                            u"from '%s' to '%s'" % (
755
                            ack_mapping[event.ack],
756
                            ack_label,
757
                        ),
758
                        username=user.user_name,
759
                        timestamp=datetime.now(),
760
                    )
761
                DBSession.add(history)
762
                LOGGER.info(_('User "%(user)s" (%(address)s) changed the state '
763
                            'from "%(previous)s" to "%(new)s" on event '
764
                            '#%(idevent)d') % {
765
                                'user': request.identity['repoze.who.userid'],
766
                                'address': request.remote_addr,
767
                                'previous': _(ack_mapping[event.ack]),
768
                                'new': _(ack_label),
769
                                'idevent': event.idcause,
770
                            })
771
                event.ack = ack_mapping[changed_ack]
772

    
773
        DBSession.flush()
774
        flash(_('Updated successfully'))
775
        redirect(request.environ.get('HTTP_REFERER', '/'))
776

    
777

    
778
    class GetPluginValueSchema(schema.Schema):
779
        """Schéma de validation de la méthode get_plugin_value."""
780
        idcorrevent = validators.Int(not_empty=True)
781
        plugin_name = validators.String(not_empty=True)
782
        # Permet de passer des paramètres supplémentaires au plugin.
783
        allow_extra_fields = True
784

    
785
    @validate(
786
        validators=GetPluginValueSchema(),
787
        error_handler = handle_validation_errors_json)
788
    @expose('json')
789
    @require(access_restriction)
790
    def plugin_json(self, idcorrevent, plugin_name, *arg, **krgv):
791
        """
792
        Permet de récupérer la valeur d'un plugin associée à un CorrEvent
793
        donné via JSON.
794
        """
795

    
796
        # Vérification de l'existence du plugin
797
        plugins = dict(config['columns_plugins'])
798
        if plugin_name not in plugins:
799
            raise HTTPNotFound(_("No such plugin '%s'") % plugin_name)
800

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

    
804
        # Filtrage des évènements en fonction des permissions de
805
        # l'utilisateur (s'il n'appartient pas au groupe 'managers')
806
        if not config.is_manager.is_met(request.environ):
807
            user = get_current_user()
808

    
809
            events = events.join(
810
                (Event, Event.idevent == CorrEvent.idcause),
811
            ).outerjoin(
812
                (LowLevelService, LowLevelService.idservice == Event.idsupitem),
813
            ).join(
814
                (SUPITEM_GROUP_TABLE,
815
                    or_(
816
                        SUPITEM_GROUP_TABLE.c.idsupitem == \
817
                            LowLevelService.idhost,
818
                        SUPITEM_GROUP_TABLE.c.idsupitem == \
819
                            Event.idsupitem,
820
                    )
821
                ),
822
            ).join(
823
                (GroupHierarchy,
824
                    GroupHierarchy.idchild == SUPITEM_GROUP_TABLE.c.idgroup),
825
            ).join(
826
                (DataPermission,
827
                    DataPermission.idgroup == GroupHierarchy.idparent),
828
            ).join(
829
                (USER_GROUP_TABLE,
830
                    USER_GROUP_TABLE.c.idgroup == DataPermission.idusergroup),
831
            ).filter(USER_GROUP_TABLE.c.username == user.user_name)
832

    
833
        # Filtrage des évènements en fonction
834
        # de l'identifiant passé en paramètre
835
        events = events.filter(CorrEvent.idcorrevent == idcorrevent).count()
836

    
837
        # Pas d'événement ou permission refusée. On ne distingue pas
838
        # les 2 cas afin d'éviter la divulgation d'informations.
839
        if events == 0:
840
            raise HTTPNotFound(_('No such incident or insufficient '
841
                                'permissions'))
842

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

    
848
    @validate(validators={
849
        "fontsize": validators.Regex(
850
            r'[0-9]+(pt|px|em|%)',
851
            regexOps = ('I',)
852
        )}, error_handler = handle_validation_errors_json)
853
    @expose('json')
854
    def set_fontsize(self, fontsize):
855
        """Enregistre la taille de la police dans les préférences."""
856
        session['fontsize'] = fontsize
857
        session.save()
858
        return dict()
859

    
860
    @validate(validators={"refresh": validators.Int()},
861
            error_handler = handle_validation_errors_json)
862
    @expose('json')
863
    def set_refresh(self, refresh):
864
        """Enregistre le temps de rafraichissement dans les préférences."""
865
        session['refresh'] = bool(refresh)
866
        session.save()
867
        return dict()
868

    
869
    @expose('json')
870
    def set_theme(self, theme):
871
        """Enregistre le thème à utiliser dans les préférences."""
872
        # On sauvegarde l'ID du thème sans vérifications
873
        # car les thèmes (styles CSS) sont définies dans
874
        # les packages de thèmes (ex: vigilo-themes-default).
875
        # La vérification de la valeur est faite dans les templates.
876
        session['theme'] = theme
877
        session.save()
878
        return dict()
879

    
880
    @validate(validators={"items": validators.Int()},
881
            error_handler = handle_validation_errors_json)
882
    @expose('json')
883
    def set_items_per_page(self, items):
884
        """Enregistre le nombre d'alertes par page dans les préférences."""
885
        session['items_per_page'] = items
886
        session.save()
887
        return dict()
888

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

896
        @param parent_id: identifiant du groupe d'hôte parent
897
        @type  parent_id: C{int} or None
898
        """
899

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

    
906
        # TODO: Utiliser un schéma de validation
907
        parent_id = int(parent_id)
908
        offset = int(offset)
909

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

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

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

    
967
        return result
968

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

974
        @return: Un dictionnaire contenant la liste de ces groupes.
975
        @rtype : C{dict} of C{list} of C{dict} of C{mixed}
976
        """
977

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

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

    
994
        # On filtre ces groupes racines afin de ne
995
        # retourner que ceux auquels l'utilisateur a accès
996
        user = get_current_user()
997
        if not config.is_manager.is_met(request.environ):
998
            root_groups = root_groups.join(
999
                (GroupHierarchy,
1000
                    GroupHierarchy.idparent == SupItemGroup.idgroup),
1001
                (DataPermission,
1002
                    DataPermission.idgroup == GroupHierarchy.idchild),
1003
                (USER_GROUP_TABLE, USER_GROUP_TABLE.c.idgroup == \
1004
                    DataPermission.idusergroup),
1005
            ).filter(USER_GROUP_TABLE.c.username == user.user_name)
1006

    
1007
        groups = []
1008
        for group in root_groups.all():
1009
            groups.append({
1010
                'id'   : group.idgroup,
1011
                'name' : group.name,
1012
                'type' : "group",
1013
            })
1014

    
1015
        return dict(groups=groups, items=[])
1016

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

    
1032
    if not last_modification_timestamp:
1033
        if not value_if_none:
1034
            return None
1035
        else:
1036
            last_modification_timestamp = value_if_none
1037
    return datetime.fromtimestamp(mktime(
1038
        last_modification_timestamp.timetuple()))