Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

vigiboard / vigiboard / controllers / silence.py @ 011743be

History | View | Annotate | Download (23.7 KB)

1
# -*- coding: utf-8 -*-
2
# vim:set expandtab tabstop=4 shiftwidth=4:
3
# Copyright (C) 2007-2020 CS GROUP - France
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 tg.i18n import lazy_ugettext as l_, ugettext as _
14
from tg.support import paginate
15
from tg.predicates import Any, All, in_group, \
16
                                    has_permission, not_anonymous
17
from formencode import validators, schema
18
from formencode.compound import All as All_
19
from formencode.foreach import ForEach
20
from sqlalchemy.exc import InvalidRequestError, IntegrityError
21
from sqlalchemy.sql.expression import asc, desc
22

    
23
from vigilo.turbogears.helpers import get_current_user
24
from vigilo.turbogears.controllers import BaseController
25
from vigilo.models.session import DBSession
26
from vigilo.models.tables import SupItem, Host, LowLevelService, \
27
                            HighLevelService, StateName, Silence, UserSupItem
28
from vigilo.models.tables.secondary_tables import SILENCE_STATE_TABLE
29
from vigilo.models.utils import group_concat
30

    
31
from vigiboard.lib import error_handler
32

    
33
import logging
34

    
35
LOGGER = logging.getLogger(__name__)
36

    
37
__all__ = ['SilenceController']
38

    
39
# pylint: disable-msg=R0201
40
class SilenceController(BaseController):
41
    """
42
    Contrôleur gérant la planification des mises en silence.
43
    """
44

    
45
    # Prédicat pour la restriction de l'accès aux interfaces.
46
    # L'utilisateur doit avoir la permission "vigiboard-silence"
47
    # ou appartenir au groupe "managers" pour accéder à VigiBoard.
48
    access_restriction = All(
49
        not_anonymous(msg=l_("You need to be authenticated")),
50
        Any(in_group('managers'),
51
            has_permission('vigiboard-silence'),
52
            msg=l_("Insufficient privileges for this action"))
53
    )
54

    
55
    def process_form_errors(self, *argv, **kwargv):
56
        """
57
        Gestion des erreurs de validation : on affiche les erreurs
58
        puis on redirige vers la dernière page accédée.
59
        """
60
        for k in tmpl_context.form_errors:
61
            flash("'%s': %s" % (k, tmpl_context.form_errors[k]), 'error')
62
        redirect(request.environ.get('HTTP_REFERER', '/'))
63

    
64
    def query_silences(self):
65
        """
66
        Retourne une requête SQLAlchemy interrogeant
67
        la table des mises en silence
68
        """
69

    
70
        # Si l'utilisateur fait partie du groupe 'managers', on récupère la
71
        # liste de tous les supitems de la base de données
72
        if in_group('managers').is_met(request.environ):
73
            lls_query = DBSession.query(
74
                    LowLevelService.idservice.label('idsupitem'),
75
                    LowLevelService.servicename.label("servicename"),
76
                    Host.name.label('hostname')
77
                ).join((Host, Host.idhost == LowLevelService.idhost))
78

    
79
            host_query = DBSession.query(
80
                    Host.idhost.label('idsupitem'),
81
                    "NULL",
82
                    Host.name.label('hostname')
83
                )
84

    
85
            supitems = lls_query.union(host_query).subquery()
86

    
87
        # Sinon on ne récupère que les supitems auxquels l'utilisateurs a accès
88
        else:
89
            user_name = request.identity['repoze.who.userid']
90
            supitems = DBSession.query(
91
                UserSupItem.idsupitem.label('idsupitem'),
92
                UserSupItem.servicename.label("servicename"),
93
                UserSupItem.hostname.label('hostname')
94
            ).filter(
95
                UserSupItem.username == user_name
96
            ).distinct().subquery()
97

    
98
        # On interroge la base pour avoir la liste des règles de mise en silence
99
        # correspondant à ces supitems.
100
        states = DBSession.query(
101
                StateName.statename,
102
                SILENCE_STATE_TABLE.c.idsilence,
103
            ).join((SILENCE_STATE_TABLE,
104
                StateName.idstatename == SILENCE_STATE_TABLE.c.idstate)
105
            ).order_by(StateName.statename
106
            ).subquery()
107
        states = DBSession.query(
108
                states.c.idsilence,
109
                group_concat(states.c.statename, ', ').label('states'),
110
            ).group_by(states.c.idsilence
111
            ).subquery()
112
        silences = DBSession.query(
113
                Silence,
114
                supitems.c.hostname,
115
                supitems.c.servicename,
116
                states.c.states
117
            ).join((supitems, supitems.c.idsupitem == Silence.idsupitem)
118
            ).join((states, states.c.idsilence == Silence.idsilence))
119

    
120
        return silences
121

    
122
    def check_silence_rule_existence(self, idsupitem):
123
        """
124
        S'assure qu'aucune règle de mise en silence n'existe dans la base de
125
        données pour le supitem considéré, et affiche un message d'erreur dans
126
        le cas contraire.
127

128
        @param idsupitem: Identifiant du supitem.
129
        @type  idsupitem: C{int}
130
        """
131
        silence = DBSession.query(Silence
132
            ).filter(Silence.idsupitem == idsupitem
133
            ).first()
134
        if not silence:
135
            return
136
        if isinstance(silence.supitem, LowLevelService):
137
            msg = _("Another rule already exists for service '%s' " \
138
                    "on host '%s'.") % (silence.supitem.servicename,
139
                        silence.supitem.host.name)
140
        else:
141
            msg = _("Another rule already exists for host '%s'.") % (
142
                silence.supitem.name)
143
        error_handler.handle_error_message(msg)
144

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

    
158
    @validate(
159
        validators=IndexSchema(),
160
        error_handler = process_form_errors)
161
    @expose('silence.html')
162
    @require(access_restriction)
163
    def index(self, page=1, sort=None, order=None):
164
        """
165
        Affiche la liste des règles de mise en silence enregistrées dans
166
        la BDD, que l'utilisateur pourra ensuite éditer ou supprimer.
167

168
        @param sort: (optionnel) Critère de tri de la liste des
169
                     règles de mise en silence enregistrées.
170
        @type  sort: C{str}
171
        @param order: (optionnel) Ordre de tri.
172
        @type  order: C{str}
173
        """
174

    
175
#        # On récupère la langue de l'utilisateur
176
#        lang = get_lang()
177
#        if not lang:
178
#            lang = ['fr']
179
#        lang = lang[0]
180

    
181
        # On récupère tous les enregistrements de la table
182
        # silence, qu'ils concernent des hôtes, des services
183
        # de bas niveau, ou bien des services de haut niveau.
184
        silences = self.query_silences()
185

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

    
206
        # On calcule la pagination
207
        page = paginate.Page(silences, page=page,
208
            items_per_page=int(config['vigiboard_items_per_page']))
209

    
210
#        # On initialise les widgets des calendriers
211
#        # utilisés dans le formulaire de mise en silence.
212
#        start_calendar = CalendarDateTimePicker('start',
213
#                                            button_text = l_("Choose a date"),
214
#                                            date_format = '%Y-%m-%d %H:%M',
215
#                                            calendar_lang = lang)
216
#        end_calendar = CalendarDateTimePicker('end',
217
#                                            button_text = l_("Choose a date"),
218
#                                            date_format = '%Y-%m-%d %H:%M',
219
#                                            calendar_lang = lang)
220

    
221
        # Traduction du nom des colonnes
222
        columns = [
223
            ('hostname', l_('Host')),
224
            ('servicename', l_('Service')),
225
            ('states', l_('States')),
226
            ('lastmodification', l_('Last modification')),
227
            ('author', l_('Author')),
228
            ('comment', l_('Comment'))
229
        ]
230

    
231
        return dict(
232
            page=page,
233
            sort=sort,
234
            order=order,
235
#            start_calendar=start_calendar,
236
#            end_calendar=end_calendar,
237
            columns=columns
238
        )
239

    
240
    @expose('silence_form.html')
241
    @require(access_restriction)
242
    def add(self):
243
        """
244
        Affiche un formulaire d'ajout d'une règle de mise en silence.
245
        """
246
        return dict(
247
            id=None,
248
            hostname=None,
249
            servicename=None,
250
            states=None,
251
            comment=None,
252
#            start_calendar=start_calendar,
253
#            end_calendar=end_calendar,
254
        )
255

    
256
    class UpdateSchema(schema.Schema):
257
        """Schéma de validation de la méthode update."""
258
        id = validators.Int(min=1, not_empty=True)
259

    
260
    @validate(
261
        validators=UpdateSchema(),
262
        error_handler = process_form_errors)
263
    @expose('silence_form.html')
264
    @require(access_restriction)
265
    def update(self, id):
266
        """
267
        Affiche un formulaire de mise à jour d'une règle de mise en silence.
268

269
        @param id: Identifiant de la règle.
270
        @type  id: C{int}
271
        """
272

    
273
        # On s'assure que la règle existe bien dans la base
274
        try:
275
            silence = DBSession.query(Silence
276
                ).filter(Silence.idsilence == id).one()
277
        except InvalidRequestError as e:
278
            msg = _('An exception has been raised while ' \
279
                    'querying the database: %s') % str(e)
280
            error_handler.handle_error_message(msg)
281
        if not silence:
282
            msg = _("Silence rule #%s does not exist.") % id
283
            error_handler.handle_error_message(msg)
284

    
285
        # On s'assure que l'utilisateur dispose bien des permissions sur le
286
        # supitem considéré
287
        user = get_current_user()
288
        if not silence.supitem.is_allowed_for(user):
289
            msg = _("Silence rule #%s does not exist.") % id
290
            error_handler.handle_error_message(msg)
291

    
292
        if hasattr(silence.supitem, 'servicename'):
293
            hostname = silence.supitem.host.name
294
            servicename = silence.supitem.servicename
295
        else:
296
            hostname = silence.supitem.name
297
            servicename = None
298

    
299
        return dict(
300
            id=id,
301
            hostname=hostname,
302
            servicename=servicename,
303
            states=[s.statename for s in silence.states],
304
            comment=silence.comment,
305
#            start_calendar=start_calendar,
306
#            end_calendar=end_calendar,
307
        )
308

    
309
    class CreateOrModifySchema(schema.Schema):
310
        """Schéma de validation de la méthode create_or_modify."""
311
        states = All_(
312
            validators.Set(use_set=True),
313
            validators.OneOf(["WARNING", "CRITICAL", "DOWN", "UNKNOWN"],
314
                testValueList=True, hideList=True, not_empty=True)
315
        )
316
        host = validators.String(not_empty=True)
317
        service = validators.String()
318
        comment = validators.String()
319
        idsilence = validators.Int(min=1, if_missing=None,
320
            if_invalid=None, not_empty=False)
321

    
322
    @validate(
323
        validators=CreateOrModifySchema(),
324
        error_handler = process_form_errors)
325
    @expose()
326
    @require(access_restriction)
327
    def create_or_modify(
328
            self,
329
            states,
330
            host,
331
            service=None,
332
#            start=time.time(),
333
#            end=time.time(),
334
            comment=None,
335
            idsilence=None):
336
        """
337
        Ajoute une règle de mise en silence d'un hôte/service,
338
        ou en modifie une déjà existante.
339

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

    
358
        # TODO: Faire ce traitement dans le schéma de validation
359
        if not states:
360
            msg = _('No state specified for the silence rule.')
361
            error_handler.handle_error_message(msg)
362
        states = list(states)
363

    
364
        # On récupère le nom et l'IP de l'utilisateur.
365
        user = get_current_user()
366
        user_name = user.user_name
367
        user_ip = request.remote_addr
368

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

    
389
        # On distingue mise à jour et création :
390

    
391
        # 1. Dans le cas d'une mise à jour :
392
        if idsilence:
393

    
394
            # - On s'assure que la règle existe bien dans la base
395
            try:
396
                silence = DBSession.query(Silence
397
                    ).filter(Silence.idsilence == idsilence).one()
398
            except InvalidRequestError as e:
399
                msg = _('An exception has been raised while ' \
400
                        'querying the database: %s') % str(e)
401
                error_handler.handle_error_message(msg)
402
            if not silence:
403
                msg = _("Silence rule #%s does not exist.") % idsilence
404
                error_handler.handle_error_message(msg)
405

    
406
            # - Si le supitem a été modifié, on vérifie qu'aucune
407
            #   autre règle n'existe pour le nouveau supitem
408
            if silence.idsupitem != idsupitem:
409
                self.check_silence_rule_existence(idsupitem)
410

    
411
            # On supprime les états existants
412
            silence.states = []
413

    
414
        # 2. Dans le cas d'une création :
415
        else:
416

    
417
            # - On s'assure qu'aucune autre règle n'existe pour le supitem
418
            self.check_silence_rule_existence(idsupitem)
419

    
420
            # - Et on crée l'objet représentant la règle
421
            silence = Silence()
422

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

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

    
516

    
517
    class DeleteSchema(schema.Schema):
518
        """Schéma de validation de la méthode delete."""
519
        id = All_(
520
            validators.Set(use_set=True),
521
            ForEach(validators.Int(min=1)),
522
        )
523

    
524
    @validate(
525
        validators=DeleteSchema(),
526
        error_handler = process_form_errors)
527
    @expose()
528
    @require(access_restriction)
529
    def delete(self, id):
530
        """
531
        Suppression d'une règle ou d'une liste de règles de mise en silence.
532

533
        @param id: Liste des identifiants des règles à supprimer.
534
        @type  id: C{list} of C{int}
535
        """
536

    
537
        # TODO: Faire ce traitement dans le schéma de validation
538
        if not id:
539
            msg = _('No silence rule id specified.')
540
            error_handler.handle_error_message(msg)
541
        id = list(id)
542

    
543
        # On recherche les règles dans la BDD.
544
        try:
545
            silences = DBSession.query(Silence
546
                ).filter(Silence.idsilence.in_(id)).all()
547
        except InvalidRequestError as e:
548
            msg = _('An exception has been raised while ' \
549
                    'querying the database: %s') % str(e)
550
            error_handler.handle_error_message(msg)
551

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

    
565
        # On s'assure que l'utilisateur dispose bien des permissions nécessaires
566
        # pour supprimer chacune des règles
567
        user = get_current_user()
568
        for s in silences:
569
            if not s.supitem.is_allowed_for(user):
570
                msg = _("Silence rule #%s does not exist.") % s.idsilence
571
                error_handler.handle_error_message(msg)
572

    
573
        # On supprime les règles dans la BDD.
574
        try:
575
            for silence in silences:
576
                DBSession.delete(silence)
577
            DBSession.flush()
578
        except InvalidRequestError as e:
579
            msg = _('An exception has been raised while ' \
580
                    'deleting the silence rules: %s') % str(e)
581
            error_handler.handle_error_message(msg)
582

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

    
610
        # On affiche un message de succès
611
        if len(id) > 1:
612
            flash(_('The following silence rules have been successfully ' \
613
                'deleted: %s.') % ", ".join(str(i) for i in id))
614
        else:
615
            flash(_('Silence rule #%s has been successfully ' \
616
                'deleted.') % id[0])
617

    
618
        # On redirige le navigateur vers la page d'index
619
        redirect('./', )
620