Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

vigiboard / vigiboard / controllers / root.py @ f1886725

History | View | Annotate | Download (39.7 KB)

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

    
21
"""VigiBoard Controller"""
22

    
23
from datetime import datetime
24
from time import mktime
25

    
26
from pkg_resources import resource_filename, working_set
27

    
28
from tg.exceptions import HTTPNotFound
29
from tg import expose, validate, require, flash, url, \
30
    tmpl_context, request, response, config, session, redirect
31
from webhelpers import paginate
32
from tw.forms import validators
33
from pylons.i18n import ugettext as _, lazy_ugettext as l_, get_lang
34
from sqlalchemy import asc
35
from sqlalchemy.sql import func
36
from sqlalchemy.orm import aliased
37
from sqlalchemy.sql.expression import or_
38
from repoze.what.predicates import Any, All, in_group, \
39
                                    has_permission, not_anonymous, \
40
                                    NotAuthorizedError
41
from formencode import schema
42

    
43
from vigilo.models.session import DBSession
44
from vigilo.models.tables import Event, EventHistory, CorrEvent, Host, \
45
                                    SupItem, SupItemGroup, LowLevelService, \
46
                                    StateName, State, DataPermission
47
from vigilo.models.tables.grouphierarchy import GroupHierarchy
48
from vigilo.models.tables.secondary_tables import EVENTSAGGREGATE_TABLE, \
49
        USER_GROUP_TABLE, SUPITEM_GROUP_TABLE
50

    
51
from vigilo.turbogears.controllers.auth import AuthController
52
from vigilo.turbogears.controllers.selfmonitoring import SelfMonitoringController
53
from vigilo.turbogears.controllers.custom import CustomController
54
from vigilo.turbogears.controllers.error import ErrorController
55
from vigilo.turbogears.controllers.autocomplete import AutoCompleteController
56
from vigilo.turbogears.controllers.proxy import ProxyController
57
from vigilo.turbogears.controllers.api.root import ApiRootController
58
from vigilo.turbogears.helpers import get_current_user
59

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

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

    
69
LOGGER = logging.getLogger(__name__)
70

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

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

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

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

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

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

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

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

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

    
136
        # Nécessaire pour que les critères de recherche soient conservés.
137
        allow_extra_fields = True
138

    
139
        # 2ème validation, cette fois avec les champs
140
        # du formulaire de recherche.
141
        chained_validators = [create_search_form.validator]
142

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

156
        @param page: Numéro de la page souhaitée, commence à 1
157
        @type page: C{int}
158
        @param search: Dictionnaire contenant les critères de recherche.
159
        @type search: C{dict}
160

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

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

    
170
        user = get_current_user()
171
        aggregates = VigiboardRequest(user, search=search)
172

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

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

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

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

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

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

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

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

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

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

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

    
247
        return dict(
248
            hostname = None,
249
            servicename = None,
250
            plugins_data = plugins_data,
251
            page = page,
252
            event_edit_status_options = edit_event_status_options,
253
            search_form = create_search_form,
254
            search = search,
255
            fixed_search = fixed_search,
256
        )
257

    
258

    
259
    @expose()
260
    def i18n(self):
261
        import gettext
262
        import pylons
263
        import os.path
264

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

    
273
        lang = get_lang()
274

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

    
281
        themes_filename = gettext.find(
282
            'vigilo-themes',
283
            resource_filename('vigilo.themes.i18n', ''),
284
            languages=lang)
285
        themes_js = themes_filename[:-3] + '.js'
286

    
287
        # Récupère et envoie le contenu du fichier de traduction *.js.
288
        fhandle = open(js, 'r')
289
        translations = fhandle.read()
290
        fhandle.close()
291

    
292
        fhandle = open(themes_js, 'r')
293
        translations += fhandle.read()
294
        fhandle.close()
295
        return translations
296

    
297

    
298
    class MaskedEventsSchema(schema.Schema):
299
        """Schéma de validation de la méthode masked_events."""
300
        idcorrevent = validators.Int(not_empty=True)
301
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
302

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

313
        @param idcorrevent: identifiant de l'événement corrélé souhaité.
314
        @type idcorrevent: C{int}
315
        """
316

    
317
        # Auto-supervision
318
        self.get_failures()
319

    
320
        user = get_current_user()
321

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

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

    
353
        if isinstance(cause_supitem, LowLevelService):
354
            hostname = cause_supitem.host.name
355
            servicename = cause_supitem.servicename
356
        elif isinstance(cause_supitem, Host):
357
            hostname = cause_supitem.name
358

    
359
        # Pagination des résultats
360
        events.generate_request()
361
        items_per_page = int(config['vigiboard_items_per_page'])
362
        page = paginate.Page(events.req, page=page,
363
            items_per_page=items_per_page)
364

    
365
        # Vérification que l'événement existe
366
        if not page.item_count:
367
            flash(_('No masked event or access denied'), 'error')
368
            redirect('/')
369

    
370
        return dict(
371
            idcorrevent = idcorrevent,
372
            hostname = hostname,
373
            servicename = servicename,
374
            plugins_data = {},
375
            page = page,
376
            search_form = create_search_form,
377
            search = {},
378
            fixed_search = {},
379
        )
380

    
381

    
382
    class EventSchema(schema.Schema):
383
        """Schéma de validation de la méthode event."""
384
        idevent = validators.Int(not_empty=True)
385
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
386

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

397
        @param idevent: identifiant de l'événement brut souhaité.
398
        @type idevent: C{int}
399
        @param page: numéro de la page à afficher.
400
        @type page: C{int}
401

402
        Cette méthode permet de satisfaire l'exigence
403
        VIGILO_EXIG_VIGILO_BAC_0080.
404
        """
405

    
406
        # Auto-supervision
407
        self.get_failures()
408

    
409
        user = get_current_user()
410
        events = VigiboardRequest(user, False)
411
        events.add_table(
412
            Event,
413
            events.items.c.hostname.label('hostname'),
414
            events.items.c.servicename.label('servicename'),
415
        )
416
        events.add_join((EVENTSAGGREGATE_TABLE, \
417
            EVENTSAGGREGATE_TABLE.c.idevent == Event.idevent))
418
        events.add_join((CorrEvent, CorrEvent.idcorrevent == \
419
            EVENTSAGGREGATE_TABLE.c.idcorrevent))
420
        events.add_join((events.items,
421
            Event.idsupitem == events.items.c.idsupitem))
422
        events.add_filter(Event.idevent == idevent)
423

    
424
        if events.num_rows() != 1:
425
            flash(_('No such event or access denied'), 'error')
426
            redirect('/')
427

    
428
        events.format_events(0, 1)
429
        events.generate_tmpl_context()
430
        history = events.format_history()
431

    
432
        # Pagination des résultats
433
        items_per_page = int(config['vigiboard_items_per_page'])
434
        page = paginate.Page(history, page=page, items_per_page=items_per_page)
435
        event = events.req[0]
436

    
437
        return dict(
438
            idevent = idevent,
439
            hostname = event.hostname,
440
            servicename = event.servicename,
441
            plugins_data = {},
442
            page = page,
443
            search_form = create_search_form,
444
            search = {},
445
            fixed_search = {},
446
        )
447

    
448

    
449
    class ItemSchema(schema.Schema):
450
        """Schéma de validation de la méthode item."""
451
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
452
        host = validators.String(not_empty=True)
453
        service = validators.String(if_missing=None)
454

    
455
    @validate(
456
        validators=ItemSchema(),
457
        error_handler = process_form_errors)
458
    @expose('events_table.html')
459
    @require(access_restriction)
460
    def item(self, page, host, service):
461
        """
462
        Affichage de l'historique de l'ensemble des événements corrélés
463
        jamais ouverts sur l'hôte / service demandé.
464
        Pour accéder à cette page, l'utilisateur doit être authentifié.
465

466
        @param page: Numéro de la page à afficher.
467
        @param host: Nom de l'hôte souhaité.
468
        @param service: Nom du service souhaité
469

470
        Cette méthode permet de satisfaire l'exigence
471
        VIGILO_EXIG_VIGILO_BAC_0080.
472
        """
473

    
474
        # Auto-supervision
475
        self.get_failures()
476

    
477
        idsupitem = SupItem.get_supitem(host, service)
478
        if not idsupitem:
479
            flash(_('No such host/service'), 'error')
480
            redirect('/')
481

    
482
        user = get_current_user()
483
        aggregates = VigiboardRequest(user, False)
484
        aggregates.add_table(
485
            CorrEvent,
486
            aggregates.items.c.hostname,
487
            aggregates.items.c.servicename,
488
        )
489
        aggregates.add_join((Event, CorrEvent.idcause == Event.idevent))
490
        aggregates.add_join((aggregates.items,
491
            Event.idsupitem == aggregates.items.c.idsupitem))
492
        aggregates.add_filter(aggregates.items.c.idsupitem == idsupitem)
493

    
494
        # Pagination des résultats
495
        aggregates.generate_request()
496
        items_per_page = int(config['vigiboard_items_per_page'])
497
        page = paginate.Page(aggregates.req, page=page,
498
            items_per_page=items_per_page)
499

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

    
505
        # Ajout des formulaires et préparation
506
        # des données pour ces formulaires.
507
        ids_events = [event[0].idcause for event in page.items]
508
        tmpl_context.last_modification = \
509
            mktime(get_last_modification_timestamp(ids_events).timetuple())
510

    
511
        tmpl_context.edit_event_form = EditEventForm("edit_event_form",
512
            submit_text=_('Apply'), action=url('/update'))
513

    
514
        plugins_data = {}
515
        for plugin in dict(config['columns_plugins']):
516
            plugins_data[plugin] = {}
517

    
518
        return dict(
519
            hostname = host,
520
            servicename = service,
521
            plugins_data = plugins_data,
522
            page = page,
523
            event_edit_status_options = edit_event_status_options,
524
            search_form = create_search_form,
525
            search = {},
526
            fixed_search = {},
527
        )
528

    
529

    
530
    class UpdateSchema(schema.Schema):
531
        """Schéma de validation de la méthode update."""
532
        id = validators.Regex(r'^[0-9]+(,[0-9]+)*,?$')
533
        last_modification = validators.Number(not_empty=True)
534
        trouble_ticket = validators.String(if_missing='')
535
        ack = validators.OneOf(
536
            [unicode(s[0]) for s in edit_event_status_options],
537
            not_empty=True)
538

    
539
    @validate(
540
        validators=UpdateSchema(),
541
        error_handler = process_form_errors)
542
    @require(
543
        All(
544
            not_anonymous(msg=l_("You need to be authenticated")),
545
            Any(in_group('managers'),
546
                has_permission('vigiboard-update'),
547
                msg=l_("You don't have write access to VigiBoard"))
548
        ))
549
    @expose()
550
    def update(self, id, last_modification, trouble_ticket, ack):
551
        """
552
        Mise à jour d'un événement suivant les arguments passés.
553
        Cela peut être un changement de ticket ou un changement de statut.
554

555
        @param id: Le ou les identifiants des événements à traiter
556
        @param last_modification: La date de la dernière modification
557
            dont l'utilisateur est au courant.
558
        @param trouble_ticket: Nouveau numéro du ticket associé.
559
        @param ack: Nouvel état d'acquittement des événements sélectionnés.
560

561
        Cette méthode permet de satisfaire les exigences suivantes :
562
            - VIGILO_EXIG_VIGILO_BAC_0020,
563
            - VIGILO_EXIG_VIGILO_BAC_0060,
564
            - VIGILO_EXIG_VIGILO_BAC_0110.
565
        """
566

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

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

    
577
        user = get_current_user()
578
        events = VigiboardRequest(user)
579
        events.add_table(
580
            CorrEvent,
581
            Event,
582
            events.items.c.hostname,
583
            events.items.c.servicename,
584
        )
585
        events.add_join((Event, CorrEvent.idcause == Event.idevent))
586
        events.add_join((events.items,
587
            Event.idsupitem == events.items.c.idsupitem))
588
        events.add_filter(CorrEvent.idcorrevent.in_(ids))
589

    
590
        events.generate_request()
591
        idevents = [event[0].idcause for event in events.req]
592

    
593
        # Si des changements sont survenus depuis que la
594
        # page est affichée, on en informe l'utilisateur.
595
        last_modification = datetime.fromtimestamp(last_modification)
596
        cur_last_modification = get_last_modification_timestamp(idevents, None)
597
        if cur_last_modification and last_modification < cur_last_modification:
598
            flash(_('Changes have occurred since the page was last displayed, '
599
                    'your changes HAVE NOT been saved.'), 'warning')
600
            raise redirect(request.environ.get('HTTP_REFERER', '/'))
601

    
602
        # Vérification que au moins un des identifiants existe et est éditable
603
        if not events.num_rows():
604
            flash(_('No access to this event'), 'error')
605
            redirect('/')
606

    
607
        if ack == u'Forced':
608
            condition = Any(
609
                in_group('managers'),
610
                has_permission('vigiboard-admin'),
611
                msg=l_("You don't have administrative access "
612
                        "to VigiBoard"))
613
            try:
614
                condition.check_authorization(request.environ)
615
            except NotAuthorizedError, e:
616
                reason = unicode(e)
617
                flash(reason, 'error')
618
                raise redirect(request.environ.get('HTTP_REFERER', '/'))
619

    
620
        # Si un module de gestion de ticket est utilisé,
621
        # il a la possibilité de changer à la volée le libellé du ticket.
622
        if self._tickets:
623
            trouble_ticket = self._tickets.createTicket(events.req, trouble_ticket)
624

    
625
        # Définit 2 mappings dont les ensembles sont disjoincts
626
        # pour basculer entre la représentation en base de données
627
        # et la représentation "humaine" du bac à événements.
628
        ack_mapping = {
629
            # Permet d'associer la valeur dans le widget ToscaWidgets
630
            # (cf. vigiboard.widgets.edit_event.edit_event_status_options)
631
            # avec la valeur dans la base de données.
632
            u'None': CorrEvent.ACK_NONE,
633
            u'Acknowledged': CorrEvent.ACK_KNOWN,
634
            u'AAClosed': CorrEvent.ACK_CLOSED,
635

    
636
            # Permet d'afficher un libellé plus sympathique pour l'utilisateur
637
            # représentant l'état d'acquittement stocké en base de données.
638
            CorrEvent.ACK_NONE: l_('None'),
639
            CorrEvent.ACK_KNOWN: l_('Acknowledged'),
640
            CorrEvent.ACK_CLOSED: l_('Acknowledged and closed'),
641
        }
642

    
643
        # Modification des événements et création d'un historique
644
        # chaque fois que cela est nécessaire.
645
        for data in events.req:
646
            event = data[0]
647
            if trouble_ticket and trouble_ticket != event.trouble_ticket:
648
                history = EventHistory(
649
                        type_action=u"Ticket change",
650
                        idevent=event.idcause,
651
                        value=unicode(trouble_ticket),
652
                        text="Changed trouble ticket from '%(from)s' "
653
                             "to '%(to)s'" % {
654
                            'from': event.trouble_ticket,
655
                            'to': trouble_ticket,
656
                        },
657
                        username=user.user_name,
658
                        timestamp=datetime.now(),
659
                    )
660
                DBSession.add(history)
661
                LOGGER.info(_('User "%(user)s" (%(address)s) changed the '
662
                            'trouble ticket from "%(previous)s" to "%(new)s" '
663
                            'on event #%(idevent)d') % {
664
                                'user': request.identity['repoze.who.userid'],
665
                                'address': request.remote_addr,
666
                                'previous': event.trouble_ticket,
667
                                'new': trouble_ticket,
668
                                'idevent': event.idcause,
669
                            })
670
                event.trouble_ticket = trouble_ticket
671

    
672
            # Changement du statut d'acquittement.
673
            if ack != u'NoChange':
674
                changed_ack = ack
675
                # Pour forcer l'acquittement d'un événement,
676
                # il faut en plus avoir la permission
677
                # "vigiboard-admin".
678
                if ack == u'Forced':
679
                    changed_ack = u'AAClosed'
680
                    cause = event.cause
681
                    # On met systématiquement l'événement à l'état "OK",
682
                    # même s'il s'agit d'un hôte.
683
                    # Techniquement, c'est incorrect, mais on fait ça
684
                    # pour masquer l'événement de toutes façons...
685
                    cause.current_state = \
686
                        StateName.statename_to_value(u'OK')
687

    
688
                    # Mise à jour de l'état dans State, pour que
689
                    # VigiMap soit également mis à jour.
690
                    DBSession.query(State).filter(
691
                            State.idsupitem == cause.idsupitem,
692
                        ).update({
693
                            'state': StateName.statename_to_value(u'OK'),
694
                        })
695

    
696
                    history = EventHistory(
697
                            type_action=u"Forced change state",
698
                            idevent=event.idcause,
699
                            value=u'OK',
700
                            text="Forced state to 'OK'",
701
                            username=user.user_name,
702
                            timestamp=datetime.now(),
703
                            state=StateName.statename_to_value(u'OK'),
704
                        )
705
                    DBSession.add(history)
706
                    LOGGER.info(_('User "%(user)s" (%(address)s) forcefully '
707
                                'closed event #%(idevent)d') % {
708
                                    'user': request. \
709
                                            identity['repoze.who.userid'],
710
                                    'address': request.remote_addr,
711
                                    'idevent': event.idcause,
712
                                })
713

    
714
                # Convertit la valeur du widget ToscaWidgets
715
                # vers le code interne puis vers un libellé
716
                # "humain".
717
                ack_label = ack_mapping[ack_mapping[changed_ack]]
718

    
719
                # Si le changement a été forcé,
720
                # on veut le mettre en évidence.
721
                if ack == u'Forced':
722
                    history_label = u'Forced'
723
                else:
724
                    history_label = ack_label
725

    
726
                history = EventHistory(
727
                        type_action=u"Acknowledgement change state",
728
                        idevent=event.idcause,
729
                        value=unicode(history_label),
730
                        text=u"Changed acknowledgement status "
731
                            u"from '%s' to '%s'" % (
732
                            ack_mapping[event.ack],
733
                            ack_label,
734
                        ),
735
                        username=user.user_name,
736
                        timestamp=datetime.now(),
737
                    )
738
                DBSession.add(history)
739
                LOGGER.info(_('User "%(user)s" (%(address)s) changed the state '
740
                            'from "%(previous)s" to "%(new)s" on event '
741
                            '#%(idevent)d') % {
742
                                'user': request.identity['repoze.who.userid'],
743
                                'address': request.remote_addr,
744
                                'previous': _(ack_mapping[event.ack]),
745
                                'new': _(ack_label),
746
                                'idevent': event.idcause,
747
                            })
748
                event.ack = ack_mapping[changed_ack]
749

    
750
        DBSession.flush()
751
        flash(_('Updated successfully'))
752
        redirect(request.environ.get('HTTP_REFERER', '/'))
753

    
754

    
755
    class GetPluginValueSchema(schema.Schema):
756
        """Schéma de validation de la méthode get_plugin_value."""
757
        idcorrevent = validators.Int(not_empty=True)
758
        plugin_name = validators.String(not_empty=True)
759
        # Permet de passer des paramètres supplémentaires au plugin.
760
        allow_extra_fields = True
761

    
762
    @validate(
763
        validators=GetPluginValueSchema(),
764
        error_handler = handle_validation_errors_json)
765
    @expose('json')
766
    @require(access_restriction)
767
    def plugin_json(self, idcorrevent, plugin_name, *arg, **krgv):
768
        """
769
        Permet de récupérer la valeur d'un plugin associée à un CorrEvent
770
        donné via JSON.
771
        """
772

    
773
        # Vérification de l'existence du plugin
774
        plugins = dict(config['columns_plugins'])
775
        if plugin_name not in plugins:
776
            raise HTTPNotFound(_("No such plugin '%s'") % plugin_name)
777

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

    
781
        # Filtrage des évènements en fonction des permissions de
782
        # l'utilisateur (s'il n'appartient pas au groupe 'managers')
783
        is_manager = in_group('managers').is_met(request.environ)
784
        if not is_manager:
785
            user = get_current_user()
786

    
787
            events = events.join(
788
                (Event, Event.idevent == CorrEvent.idcause),
789
            ).outerjoin(
790
                (LowLevelService, LowLevelService.idservice == Event.idsupitem),
791
            ).join(
792
                (SUPITEM_GROUP_TABLE,
793
                    or_(
794
                        SUPITEM_GROUP_TABLE.c.idsupitem == \
795
                            LowLevelService.idhost,
796
                        SUPITEM_GROUP_TABLE.c.idsupitem == \
797
                            Event.idsupitem,
798
                    )
799
                ),
800
            ).join(
801
                (GroupHierarchy,
802
                    GroupHierarchy.idchild == SUPITEM_GROUP_TABLE.c.idgroup),
803
            ).join(
804
                (DataPermission,
805
                    DataPermission.idgroup == GroupHierarchy.idparent),
806
            ).join(
807
                (USER_GROUP_TABLE,
808
                    USER_GROUP_TABLE.c.idgroup == DataPermission.idusergroup),
809
            ).filter(USER_GROUP_TABLE.c.username == user.user_name)
810

    
811
        # Filtrage des évènements en fonction
812
        # de l'identifiant passé en paramètre
813
        events = events.filter(CorrEvent.idcorrevent == idcorrevent).count()
814

    
815
        # Pas d'événement ou permission refusée. On ne distingue pas
816
        # les 2 cas afin d'éviter la divulgation d'informations.
817
        if events == 0:
818
            raise HTTPNotFound(_('No such incident or insufficient '
819
                                'permissions'))
820

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

    
826
    @validate(validators={
827
        "fontsize": validators.Regex(
828
            r'[0-9]+(pt|px|em|%)',
829
            regexOps = ('I',)
830
        )}, error_handler = handle_validation_errors_json)
831
    @expose('json')
832
    def set_fontsize(self, fontsize):
833
        """Enregistre la taille de la police dans les préférences."""
834
        session['fontsize'] = fontsize
835
        session.save()
836
        return dict()
837

    
838
    @validate(validators={"refresh": validators.Int()},
839
            error_handler = handle_validation_errors_json)
840
    @expose('json')
841
    def set_refresh(self, refresh):
842
        """Enregistre le temps de rafraichissement dans les préférences."""
843
        session['refresh'] = bool(refresh)
844
        session.save()
845
        return dict()
846

    
847
    @expose('json')
848
    def set_theme(self, theme):
849
        """Enregistre le thème à utiliser dans les préférences."""
850
        # On sauvegarde l'ID du thème sans vérifications
851
        # car les thèmes (styles CSS) sont définies dans
852
        # les packages de thèmes (ex: vigilo-themes-default).
853
        # La vérification de la valeur est faite dans les templates.
854
        session['theme'] = theme
855
        session.save()
856
        return dict()
857

    
858
    @require(access_restriction)
859
    @expose('json')
860
    def get_groups(self, parent_id=None, onlytype="", offset=0, noCache=None):
861
        """
862
        Affiche un étage de l'arbre de
863
        sélection des hôtes et groupes d'hôtes.
864

865
        @param parent_id: identifiant du groupe d'hôte parent
866
        @type  parent_id: C{int} or None
867
        """
868

    
869
        # Si l'identifiant du groupe parent n'est pas
870
        # spécifié, on retourne la liste des groupes
871
        # racines, fournie par la méthode get_root_groups.
872
        if parent_id is None:
873
            return self.get_root_groups()
874

    
875
        # TODO: Utiliser un schéma de validation
876
        parent_id = int(parent_id)
877
        offset = int(offset)
878

    
879
        # On récupère la liste des groupes de supitems dont
880
        # l'identifiant du parent est passé en paramètre.
881
        supitem_groups = DBSession.query(
882
                SupItemGroup.idgroup,
883
                SupItemGroup.name,
884
            ).join(
885
                (GroupHierarchy,
886
                    GroupHierarchy.idchild == SupItemGroup.idgroup),
887
            ).filter(GroupHierarchy.idparent == parent_id
888
            ).filter(GroupHierarchy.hops == 1
889
            ).order_by(SupItemGroup.name)
890

    
891
        # Si l'utilisateur n'appartient pas au groupe 'managers',
892
        # on filtre les résultats en fonction de ses permissions.
893
        is_manager = in_group('managers').is_met(request.environ)
894
        if not is_manager:
895
            user = get_current_user()
896
            GroupHierarchy_aliased = aliased(GroupHierarchy,
897
                name='GroupHierarchy_aliased')
898
            supitem_groups = supitem_groups.join(
899
                (GroupHierarchy_aliased,
900
                    or_(
901
                        GroupHierarchy_aliased.idchild == SupItemGroup.idgroup,
902
                        GroupHierarchy_aliased.idparent == SupItemGroup.idgroup
903
                    )),
904
                (DataPermission,
905
                    or_(
906
                        DataPermission.idgroup == \
907
                            GroupHierarchy_aliased.idparent,
908
                        DataPermission.idgroup == \
909
                            GroupHierarchy_aliased.idchild,
910
                    )),
911
                (USER_GROUP_TABLE, USER_GROUP_TABLE.c.idgroup == \
912
                    DataPermission.idusergroup),
913
            ).filter(USER_GROUP_TABLE.c.username == user.user_name)
914

    
915
        limit = int(config.get("max_menu_entries", 20))
916
        result = {"groups": [], "items": []}
917
        num_children_left = supitem_groups.distinct().count() - offset
918
        if offset:
919
            result["continued_from"] = offset
920
            result["continued_type"] = "group"
921
        all_grs = supitem_groups.distinct().limit(limit).offset(offset).all()
922
        for group in all_grs:
923
            result["groups"].append({
924
                'id'   : group.idgroup,
925
                'name' : group.name,
926
                'type' : "group",
927
            })
928
        if num_children_left > limit:
929
            result["groups"].append({
930
                'name': _("Next %(limit)s") % {"limit": limit},
931
                'offset': offset + limit,
932
                'parent_id': parent_id,
933
                'type': 'continued',
934
                'for_type': 'group',
935
            })
936

    
937
        return result
938

    
939
    def get_root_groups(self):
940
        """
941
        Retourne tous les groupes racines (c'est à dire n'ayant
942
        aucun parent) d'hôtes auquel l'utilisateur a accès.
943

944
        @return: Un dictionnaire contenant la liste de ces groupes.
945
        @rtype : C{dict} of C{list} of C{dict} of C{mixed}
946
        """
947

    
948
        # On récupère tous les groupes qui ont un parent.
949
        children = DBSession.query(
950
            SupItemGroup,
951
        ).distinct(
952
        ).join(
953
            (GroupHierarchy, GroupHierarchy.idchild == SupItemGroup.idgroup)
954
        ).filter(GroupHierarchy.hops > 0)
955

    
956
        # Ensuite on les exclut de la liste des groupes,
957
        # pour ne garder que ceux qui sont au sommet de
958
        # l'arbre et qui constituent nos "root groups".
959
        root_groups = DBSession.query(
960
            SupItemGroup,
961
        ).except_(children
962
        ).order_by(SupItemGroup.name)
963

    
964
        # On filtre ces groupes racines afin de ne
965
        # retourner que ceux auquels l'utilisateur a accès
966
        user = get_current_user()
967
        is_manager = in_group('managers').is_met(request.environ)
968
        if not is_manager:
969

    
970
            root_groups = root_groups.join(
971
                (GroupHierarchy,
972
                    GroupHierarchy.idparent == SupItemGroup.idgroup),
973
                (DataPermission,
974
                    DataPermission.idgroup == GroupHierarchy.idchild),
975
                (USER_GROUP_TABLE, USER_GROUP_TABLE.c.idgroup == \
976
                    DataPermission.idusergroup),
977
            ).filter(USER_GROUP_TABLE.c.username == user.user_name)
978

    
979
        groups = []
980
        for group in root_groups.all():
981
            groups.append({
982
                'id'   : group.idgroup,
983
                'name' : group.name,
984
                'type' : "group",
985
            })
986

    
987
        return dict(groups=groups, items=[])
988

    
989
def get_last_modification_timestamp(event_id_list,
990
                                    value_if_none=datetime.now()):
991
    """
992
    Récupère le timestamp de la dernière modification
993
    opérée sur l'un des événements dont l'identifiant
994
    fait partie de la liste passée en paramètre.
995
    """
996
    last_modification_timestamp = DBSession.query(
997
                                func.max(EventHistory.timestamp),
998
                         ).filter(EventHistory.idevent.in_(event_id_list)
999
                         ).scalar()
1000
    if not last_modification_timestamp:
1001
        if not value_if_none:
1002
            return None
1003
        else:
1004
            last_modification_timestamp = value_if_none
1005
    return datetime.fromtimestamp(mktime(
1006
        last_modification_timestamp.timetuple()))