Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

vigiboard / vigiboard / controllers / root.py @ b8700112

History | View | Annotate | Download (41 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(content_type=CUSTOM_CONTENT_TYPE)
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

    
300
        response.headers['Content-Type'] = 'text/javascript; charset=utf-8'
301
        return translations
302

    
303

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

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

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

    
325
        # Auto-supervision
326
        self.get_failures()
327

    
328
        user = get_current_user()
329

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

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

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

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

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

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

    
389

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

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

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

410
        Cette méthode permet de satisfaire l'exigence
411
        VIGILO_EXIG_VIGILO_BAC_0080.
412
        """
413

    
414
        # Auto-supervision
415
        self.get_failures()
416

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

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

    
436
        events.format_events(0, 1)
437
        events.generate_tmpl_context()
438
        history = events.format_history()
439

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

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

    
456

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

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

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

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

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

493
        Cette méthode permet de satisfaire l'exigence
494
        VIGILO_EXIG_VIGILO_BAC_0080.
495
        """
496

    
497
        # Auto-supervision
498
        self.get_failures()
499

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

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

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

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

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

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

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

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

    
554

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
779

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
969
        return result
970

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

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

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

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

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

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

    
1017
        return dict(groups=groups, items=[])
1018

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

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