Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (23.8 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
"""Gère la planification des mises en silence."""
7

    
8
#import time
9
from datetime import datetime
10

    
11
from tg import expose, validate, require, flash, tmpl_context, \
12
    request, config, redirect
13
from pylons.i18n import lazy_ugettext as l_, ugettext as _
14
#from tg.i18n import get_lang
15

    
16
#from tw.forms import CalendarDateTimePicker
17
from webhelpers import paginate
18
from tw.forms import validators
19

    
20
from vigilo.turbogears.helpers import get_current_user
21
from repoze.what.predicates import Any, All, in_group, \
22
                                    has_permission, not_anonymous
23
from formencode import schema
24
from formencode.compound import All as All_
25
from formencode.foreach import ForEach
26

    
27
from sqlalchemy.exc import InvalidRequestError, IntegrityError
28
from sqlalchemy.sql.expression import asc, desc
29

    
30
from vigilo.turbogears.controllers import BaseController
31
from vigilo.models.session import DBSession
32

    
33
from vigilo.models.tables import SupItem, Host, LowLevelService, \
34
                            HighLevelService, StateName, Silence, UserSupItem
35
from vigilo.models.tables.secondary_tables import SILENCE_STATE_TABLE
36

    
37
from vigilo.models.utils import group_concat
38

    
39
from vigiboard.lib import error_handler
40

    
41
import logging
42

    
43
LOGGER = logging.getLogger(__name__)
44

    
45
__all__ = ['SilenceController']
46

    
47
# pylint: disable-msg=R0201
48
class SilenceController(BaseController):
49
    """
50
    Contrôleur gérant la planification des mises en silence.
51
    """
52

    
53
    # Prédicat pour la restriction de l'accès aux interfaces.
54
    # L'utilisateur doit avoir la permission "vigiboard-silence"
55
    # ou appartenir au groupe "managers" pour accéder à VigiBoard.
56
    access_restriction = All(
57
        not_anonymous(msg=l_("You need to be authenticated")),
58
        Any(in_group('managers'),
59
            has_permission('vigiboard-silence'),
60
            msg=l_("Insufficient privileges for this action"))
61
    )
62

    
63
    def process_form_errors(self, *argv, **kwargv):
64
        """
65
        Gestion des erreurs de validation : on affiche les erreurs
66
        puis on redirige vers la dernière page accédée.
67
        """
68
        for k in tmpl_context.form_errors:
69
            flash("'%s': %s" % (k, tmpl_context.form_errors[k]), 'error')
70
        redirect(request.environ.get('HTTP_REFERER', '/'))
71

    
72
    def query_silences(self):
73
        """
74
        Retourne une requête SQLAlchemy interrogeant
75
        la table des mises en silence
76
        """
77

    
78
        # Si l'utilisateur fait partie du groupe 'managers', on récupère la
79
        # liste de tous les supitems de la base de données
80
        if in_group('managers').is_met(request.environ):
81
            lls_query = DBSession.query(
82
                    LowLevelService.idservice.label('idsupitem'),
83
                    LowLevelService.servicename.label("servicename"),
84
                    Host.name.label('hostname')
85
                ).join((Host, Host.idhost == LowLevelService.idhost))
86

    
87
            host_query = DBSession.query(
88
                    Host.idhost.label('idsupitem'),
89
                    "NULL",
90
                    Host.name.label('hostname')
91
                )
92

    
93
            supitems = lls_query.union(host_query).subquery()
94

    
95
        # Sinon on ne récupère que les supitems auxquels l'utilisateurs a accès
96
        else:
97
            user_name = request.identity['repoze.who.userid']
98
            supitems = DBSession.query(
99
                UserSupItem.idsupitem.label('idsupitem'),
100
                UserSupItem.servicename.label("servicename"),
101
                UserSupItem.hostname.label('hostname')
102
            ).filter(
103
                UserSupItem.username == user_name
104
            ).distinct().subquery()
105

    
106
        # On interroge la base pour avoir la liste des règles de mise en silence
107
        # correspondant à ces supitems.
108
        states = DBSession.query(
109
                StateName.statename,
110
                SILENCE_STATE_TABLE.c.idsilence,
111
            ).join((SILENCE_STATE_TABLE,
112
                StateName.idstatename == SILENCE_STATE_TABLE.c.idstate)
113
            ).order_by(StateName.statename
114
            ).subquery()
115
        states = DBSession.query(
116
                states.c.idsilence,
117
                group_concat(states.c.statename, ', ').label('states'),
118
            ).group_by(states.c.idsilence
119
            ).subquery()
120
        silences = DBSession.query(
121
                Silence,
122
                supitems.c.hostname,
123
                supitems.c.servicename,
124
                states.c.states
125
            ).join((supitems, supitems.c.idsupitem == Silence.idsupitem)
126
            ).join((states, states.c.idsilence == Silence.idsilence))
127

    
128
        return silences
129

    
130
    def check_silence_rule_existence(self, idsupitem):
131
        """
132
        S'assure qu'aucune règle de mise en silence n'existe dans la base de
133
        données pour le supitem considéré, et affiche un message d'erreur dans
134
        le cas contraire.
135

136
        @param idsupitem: Identifiant du supitem.
137
        @type  idsupitem: C{int}
138
        """
139
        silence = DBSession.query(Silence
140
            ).filter(Silence.idsupitem == idsupitem
141
            ).first()
142
        if not silence:
143
            return
144
        if isinstance(silence.supitem, LowLevelService):
145
            msg = _("Another rule already exists for service '%s' " \
146
                    "on host '%s'.") % (silence.supitem.servicename,
147
                        silence.supitem.host.name)
148
        else:
149
            msg = _("Another rule already exists for host '%s'.") % (
150
                silence.supitem.name)
151
        error_handler.handle_error_message(msg)
152

    
153
    class IndexSchema(schema.Schema):
154
        """Schéma de validation de la méthode index."""
155
        # Si on ne passe pas le paramètre "page" ou qu'on passe une valeur
156
        # invalide ou pas de valeur du tout, alors on affiche la 1ère page.
157
        page = validators.Int(min=1, if_missing=1,
158
            if_invalid=1, not_empty=True)
159
        sort = validators.OneOf(
160
            ['hostname', 'servicename', 'lastmodification',
161
                'author', 'comment', 'states'],
162
            if_missing='lastmodification', if_invalid='lastmodification')
163
        order = validators.OneOf(['desc', 'asc'],
164
            if_missing='desc', if_invalid='desc')
165

    
166
    @validate(
167
        validators=IndexSchema(),
168
        error_handler = process_form_errors)
169
    @expose('silence.html')
170
    @require(access_restriction)
171
    def index(self, page=1, sort=None, order=None):
172
        """
173
        Affiche la liste des règles de mise en silence enregistrées dans
174
        la BDD, que l'utilisateur pourra ensuite éditer ou supprimer.
175

176
        @param sort: (optionnel) Critère de tri de la liste des
177
                     règles de mise en silence enregistrées.
178
        @type  sort: C{str}
179
        @param order: (optionnel) Ordre de tri.
180
        @type  order: C{str}
181
        """
182

    
183
#        # On récupère la langue de l'utilisateur
184
#        lang = get_lang()
185
#        if not lang:
186
#            lang = ['fr']
187
#        lang = lang[0]
188

    
189
        # On récupère tous les enregistrements de la table
190
        # silence, qu'ils concernent des hôtes, des services
191
        # de bas niveau, ou bien des services de haut niveau.
192
        silences = self.query_silences()
193

    
194
        # On trie ces enregistrements selon le critère choisi
195
        # par l'utilisateur (par défaut, la date d'ajout).
196
        sort_keys = {
197
            'hostname': 'hostname',
198
            'servicename': 'servicename',
199
            'lastmodification': Silence.lastmodification,
200
            'author': Silence.author,
201
            'comment': Silence.comment,
202
#            'start': Silence.start,
203
#            'end': Silence.end,
204
            'states': 'states',
205
        }
206
        if sort in sort_keys.keys():
207
            # Tri dans l'ordre croissant
208
            if order != 'desc':
209
                silences = silences.order_by(asc(sort_keys[sort]))
210
            # Tri dans l'ordre décroissant
211
            else:
212
                silences = silences.order_by(desc(sort_keys[sort]))
213

    
214
        # On calcule la pagination
215
        page = paginate.Page(silences, page=page,
216
            items_per_page=int(config['vigiboard_items_per_page']))
217

    
218
#        # On initialise les widgets des calendriers
219
#        # utilisés dans le formulaire de mise en silence.
220
#        start_calendar = CalendarDateTimePicker('start',
221
#                                            button_text = l_("Choose a date"),
222
#                                            date_format = '%Y-%m-%d %H:%M',
223
#                                            calendar_lang = lang)
224
#        end_calendar = CalendarDateTimePicker('end',
225
#                                            button_text = l_("Choose a date"),
226
#                                            date_format = '%Y-%m-%d %H:%M',
227
#                                            calendar_lang = lang)
228

    
229
        # Traduction du nom des colonnes
230
        columns = [
231
            ('hostname', l_('Host')),
232
            ('servicename', l_('Service')),
233
            ('states', l_('States')),
234
            ('lastmodification', l_('Last modification')),
235
            ('author', l_('Author')),
236
            ('comment', l_('Comment'))
237
        ]
238

    
239
        return dict(
240
            page=page,
241
            sort=sort,
242
            order=order,
243
#            start_calendar=start_calendar,
244
#            end_calendar=end_calendar,
245
            columns=columns
246
        )
247

    
248
    @expose('silence_form.html')
249
    @require(access_restriction)
250
    def add(self):
251
        """
252
        Affiche un formulaire d'ajout d'une règle de mise en silence.
253
        """
254
        return dict(
255
            id=None,
256
            hostname=None,
257
            servicename=None,
258
            states=None,
259
            comment=None,
260
#            start_calendar=start_calendar,
261
#            end_calendar=end_calendar,
262
        )
263

    
264
    class UpdateSchema(schema.Schema):
265
        """Schéma de validation de la méthode update."""
266
        id = validators.Int(min=1, not_empty=True)
267

    
268
    @validate(
269
        validators=UpdateSchema(),
270
        error_handler = process_form_errors)
271
    @expose('silence_form.html')
272
    @require(access_restriction)
273
    def update(self, id):
274
        """
275
        Affiche un formulaire de mise à jour d'une règle de mise en silence.
276

277
        @param id: Identifiant de la règle.
278
        @type  id: C{int}
279
        """
280

    
281
        # On s'assure que la règle existe bien dans la base
282
        try:
283
            silence = DBSession.query(Silence
284
                ).filter(Silence.idsilence == id).one()
285
        except InvalidRequestError, e:
286
            msg = _('An exception has been raised while ' \
287
                    'querying the database: %s') % str(e)
288
            error_handler.handle_error_message(msg)
289
        if not silence:
290
            msg = _("Silence rule #%s does not exist.") % id
291
            error_handler.handle_error_message(msg)
292

    
293
        # On s'assure que l'utilisateur dispose bien des permissions sur le
294
        # supitem considéré
295
        user = get_current_user()
296
        if not silence.supitem.is_allowed_for(user):
297
            msg = _("Silence rule #%s does not exist.") % id
298
            error_handler.handle_error_message(msg)
299

    
300
        if hasattr(silence.supitem, 'servicename'):
301
            hostname = silence.supitem.host.name
302
            servicename = silence.supitem.servicename
303
        else:
304
            hostname = silence.supitem.name
305
            servicename = None
306

    
307
        return dict(
308
            id=id,
309
            hostname=hostname,
310
            servicename=servicename,
311
            states=[s.statename for s in silence.states],
312
            comment=silence.comment,
313
#            start_calendar=start_calendar,
314
#            end_calendar=end_calendar,
315
        )
316

    
317
    class CreateOrModifySchema(schema.Schema):
318
        """Schéma de validation de la méthode create_or_modify."""
319
        states = All_(
320
            validators.Set(use_set=True),
321
            validators.OneOf(["WARNING", "CRITICAL", "DOWN", "UNKNOWN"],
322
                testValueList=True, hideList=True, not_empty=True)
323
        )
324
        host = validators.String(not_empty=True)
325
        service = validators.String()
326
        comment = validators.String()
327
        idsilence = validators.Int(min=1, if_missing=None,
328
            if_invalid=None, not_empty=False)
329

    
330
    @validate(
331
        validators=CreateOrModifySchema(),
332
        error_handler = process_form_errors)
333
    @expose()
334
    @require(access_restriction)
335
    def create_or_modify(
336
            self,
337
            states,
338
            host,
339
            service=None,
340
#            start=time.time(),
341
#            end=time.time(),
342
            comment=None,
343
            idsilence=None):
344
        """
345
        Ajoute une règle de mise en silence d'un hôte/service,
346
        ou en modifie une déjà existante.
347

348
        @param states: (optionnel) Liste des états concernés par la règle.
349
        @type  states: C{list} of C{unicode}
350
        @param host: Nom de l'hôte sur lequel porte la règle.
351
        @type  host: C{unicode}
352
        @param service: (optionnel) Nom du service sur lequel
353
            porte la règle.
354
        @type  service: C{unicode}
355
#        @param start: Début de la mise en silence planifiée.
356
#        @type  start: C{str}
357
#        @param end: Fin de la mise en silence planifiée.
358
#        @type  end: C{str}
359
        @param comment: (optionnel) Commentaire accompagnant la règle.
360
        @type  comment: C{unicode}
361
        @param idsilence: (optionnel) Identifiant de la règle dans le cas d'une
362
            mise à jour.
363
        @type  idsilence: C{int}
364
        """
365

    
366
        # TODO: Faire ce traitement dans le schéma de validation
367
        if not states:
368
            msg = _('No state specified for the silence rule.')
369
            error_handler.handle_error_message(msg)
370
        states = list(states)
371

    
372
        # On récupère le nom et l'IP de l'utilisateur.
373
        user = get_current_user()
374
        user_name = user.user_name
375
        user_ip = request.remote_addr
376

    
377
        # On récupère l'identifiant de l'item (hôte
378
        # ou service) concerné par la mise en silence.
379
        idsupitem = SupItem.get_supitem(host, service)
380
        if idsupitem:
381
            try:
382
                supitem = DBSession.query(SupItem
383
                    ).filter(SupItem.idsupitem == idsupitem).one()
384
            except InvalidRequestError, e:
385
                msg = _('An exception has been raised while ' \
386
                        'querying the database: %s') % str(e)
387
                error_handler.handle_error_message(msg)
388
        if not idsupitem or not supitem.is_allowed_for(user):
389
            if not service:
390
                msg = _("Host '%s' does not exist.") % host
391
                error_handler.handle_error_message(msg)
392
            else:
393
                msg = _("Service '%s' does not exist for host '%s'."
394
                    ) % (service, host)
395
                error_handler.handle_error_message(msg)
396

    
397
        # On distingue mise à jour et création :
398

    
399
        # 1. Dans le cas d'une mise à jour :
400
        if idsilence:
401

    
402
            # - On s'assure que la règle existe bien dans la base
403
            try:
404
                silence = DBSession.query(Silence
405
                    ).filter(Silence.idsilence == idsilence).one()
406
            except InvalidRequestError, e:
407
                msg = _('An exception has been raised while ' \
408
                        'querying the database: %s') % str(e)
409
                error_handler.handle_error_message(msg)
410
            if not silence:
411
                msg = _("Silence rule #%s does not exist.") % idsilence
412
                error_handler.handle_error_message(msg)
413

    
414
            # - Si le supitem a été modifié, on vérifie qu'aucune
415
            #   autre règle n'existe pour le nouveau supitem
416
            if silence.idsupitem != idsupitem:
417
                self.check_silence_rule_existence(idsupitem)
418

    
419
            # On supprime les états existants
420
            silence.states = []
421

    
422
        # 2. Dans le cas d'une création :
423
        else:
424

    
425
            # - On s'assure qu'aucune autre règle n'existe pour le supitem
426
            self.check_silence_rule_existence(idsupitem)
427

    
428
            # - Et on crée l'objet représentant la règle
429
            silence = Silence()
430

    
431
        # Dans les 2 cas, on met à jour l'objet avec
432
        # les informations passées en paramètre
433
        silence.idsupitem = idsupitem
434
        silence.comment = comment
435
        silence.lastmodification = datetime.now().replace(microsecond=0)
436
        silence.author = user_name
437
        try:
438
            DBSession.add(silence)
439
            for state in states:
440
                s = DBSession.query(StateName
441
                    ).filter(StateName.statename == state).one()
442
                silence.states.append(s)
443
            DBSession.flush()
444
        except (IntegrityError, InvalidRequestError), e:
445
            msg = _('An exception has been raised while ' \
446
                    'updating the database: %s') % str(e)
447
            error_handler.handle_error_message(msg)
448

    
449
        # On notifie l'opération dans les logs, on affiche un message de
450
        # succès, et on redirige le navigateur vers la liste des règles de
451
        # mise en silence.
452
        if idsilence:
453
            # Mise à jour d'une règle portant sur un service
454
            if hasattr(silence.supitem, 'servicename'):
455
                LOGGER.info(_(
456
                    'User %(user)s (IP: %(ip)s) updated silence rule ' \
457
                    '#%(id)s for service %(service)s on host %(host)s.'
458
                ) % {
459
                    'user': user_name,
460
                    'ip': user_ip,
461
                    'id': idsilence,
462
                    'host': host,
463
                    'service': service
464
                })
465
                flash(_(
466
                    'Silence rule #%(id)s (host: %(host)s, service: ' \
467
                    '%(service)s) has been successfully updated.') % {
468
                        'id': idsilence,
469
                        'host': host,
470
                        'service': service
471
                })
472
            # Mise à jour d'une règle portant sur un hôte
473
            else:
474
                LOGGER.info(_(
475
                    'User %(user)s (IP: %(ip)s) updated silence rule ' \
476
                    '#%(id)s for host %(host)s.') % {
477
                        'user': user_name,
478
                        'ip': user_ip,
479
                        'id': idsilence,
480
                        'host': host
481
                })
482
                flash(_(
483
                    'Silence rule #%(id)s (host: %(host)s) ' \
484
                    'has been successfully updated.') % {
485
                        'id': idsilence,
486
                        'host': host
487
                })
488
        else:
489
            # Ajout d'une règle portant sur un service
490
            if service:
491
                LOGGER.info(_(
492
                    'User %(user)s (IP: %(ip)s) added a silence rule (#' \
493
                    '%(id)s) for service %(service)s on host %(host)s.'
494
                ) % {
495
                    'user': user_name,
496
                    'ip': user_ip,
497
                    'id': idsilence,
498
                    'host': host,
499
                    'service': service
500
                })
501
                flash(_('A new silence rule (#%(id)s) has been added for '
502
                    'service "%(service)s" on host "%(host)s".') % {
503
                        'id': silence.idsilence,
504
                        'service': service,
505
                        'host': host
506
                    })
507
            # Ajout d'une règle portant sur un hôte
508
            else:
509
                LOGGER.info(_(
510
                    'User %(user)s (IP: %(ip)s) added a silence rule ' \
511
                    '(#%(id)s) for host %(host)s.') % {
512
                        'user': user_name,
513
                        'ip': user_ip,
514
                        'id': idsilence,
515
                        'host': host
516
                })
517
                flash(_('A new silence rule (#%(id)s) has been added for the '
518
                    'host "%(host)s".') % {
519
                        'id': silence.idsilence,
520
                        'host': host
521
                    })
522
        redirect('./')
523

    
524

    
525
    class DeleteSchema(schema.Schema):
526
        """Schéma de validation de la méthode delete."""
527
        id = All_(
528
            validators.Set(use_set=True),
529
            ForEach(validators.Int(min=1)),
530
        )
531

    
532
    @validate(
533
        validators=DeleteSchema(),
534
        error_handler = process_form_errors)
535
    @expose()
536
    @require(access_restriction)
537
    def delete(self, id):
538
        """
539
        Suppression d'une règle ou d'une liste de règles de mise en silence.
540

541
        @param id: Liste des identifiants des règles à supprimer.
542
        @type  id: C{list} of C{int}
543
        """
544

    
545
        # TODO: Faire ce traitement dans le schéma de validation
546
        if not id:
547
            msg = _('No silence rule id specified.')
548
            error_handler.handle_error_message(msg)
549
        id = list(id)
550

    
551
        # On recherche les règles dans la BDD.
552
        try:
553
            silences = DBSession.query(Silence
554
                ).filter(Silence.idsilence.in_(id)).all()
555
        except InvalidRequestError, e:
556
            msg = _('An exception has been raised while ' \
557
                    'querying the database: %s') % str(e)
558
            error_handler.handle_error_message(msg)
559

    
560
        # On s'assure que toutes les règles ont bien été trouvées dans la
561
        # base, faute de quoi on lève une erreur et on arrête le traitement
562
        if len(silences) != len(id):
563
            missing_ids = [
564
                i for i in id if i not in [s.idsilence for s in silences]]
565
            if len(missing_ids) > 1:
566
                msg = _('Error: the following silence rules do not exist:' \
567
                    ' %s.') % ", ".join('#' + str(i) for i in missing_ids)
568
            else:
569
                msg = _('Error: silence rule #%s does not exist.'
570
                    ) % ", ".join(str(i) for i in missing_ids)
571
            error_handler.handle_error_message(msg)
572

    
573
        # On s'assure que l'utilisateur dispose bien des permissions nécessaires
574
        # pour supprimer chacune des règles
575
        user = get_current_user()
576
        for s in silences:
577
            if not s.supitem.is_allowed_for(user):
578
                msg = _("Silence rule #%s does not exist.") % s.idsilence
579
                error_handler.handle_error_message(msg)
580

    
581
        # On supprime les règles dans la BDD.
582
        try:
583
            for silence in silences:
584
                DBSession.delete(silence)
585
            DBSession.flush()
586
        except InvalidRequestError, e:
587
            msg = _('An exception has been raised while ' \
588
                    'deleting the silence rules: %s') % str(e)
589
            error_handler.handle_error_message(msg)
590

    
591
        # On notifie l'opération dans les logs
592
        user_name = user.user_name
593
        user_ip = request.remote_addr
594
        for s in silences:
595
            # Règle concernant un service de bas niveau
596
            if hasattr(s.supitem, 'servicename'):
597
                LOGGER.info(_(
598
                    'User %(user)s (IP: %(ip)s) deleted silence rule ' \
599
                    '#%(id)s for service %(service)s on host ' \
600
                    '%(host)s') % {
601
                        'user': user_name,
602
                        'ip': user_ip,
603
                        'id': s.idsilence,
604
                        'host': s.supitem.host.name,
605
                        'service': s.supitem.servicename
606
                })
607
            # Règle concernant un hôte
608
            else:
609
                LOGGER.info(_(
610
                    'User %(user)s (IP: %(ip)s) deleted silence rule ' \
611
                    '#%(id)s for host %(host)s') % {
612
                        'user': user_name,
613
                        'ip': user_ip,
614
                        'id': s.idsilence,
615
                        'host': s.supitem.name,
616
                })
617

    
618
        # On affiche un message de succès
619
        if len(id) > 1:
620
            flash(_('The following silence rules have been successfully ' \
621
                'deleted: %s.') % ", ".join(str(i) for i in id))
622
        else:
623
            flash(_('Silence rule #%s has been successfully ' \
624
                'deleted.') % id[0])
625

    
626
        # On redirige le navigateur vers la page d'index
627
        redirect('./', )
628