Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

vigiboard / vigiboard / controllers / silence.py @ 703bd599

History | View | Annotate | Download (24.8 KB)

1
# -*- coding: utf-8 -*-
2
# vim:set expandtab tabstop=4 shiftwidth=4:
3
###############################################################################
4
#
5
# Copyright (C) 2007-2014 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
"""Gère la planification des mises en silence."""
22

    
23
#import time
24
from datetime import datetime
25

    
26
from tg import expose, validate, require, flash, tmpl_context, \
27
    request, config, redirect
28
from pylons.i18n import lazy_ugettext as l_, ugettext as _
29
#from tg.i18n import get_lang
30

    
31
#from tw.forms import CalendarDateTimePicker
32
from webhelpers import paginate
33
from tw.forms import validators
34

    
35
from vigilo.turbogears.helpers import get_current_user
36
from repoze.what.predicates import Any, All, in_group, \
37
                                    has_permission, not_anonymous
38
from formencode import schema
39
from formencode.compound import All as All_
40
from formencode.foreach import ForEach
41

    
42
from sqlalchemy.exc import InvalidRequestError, IntegrityError
43
from sqlalchemy.sql.expression import asc, desc
44

    
45
from vigilo.turbogears.controllers import BaseController
46
from vigilo.models.session import DBSession
47

    
48
from vigilo.models.tables import SupItem, Host, LowLevelService, \
49
                            HighLevelService, StateName, Silence, UserSupItem
50
from vigilo.models.tables.secondary_tables import SILENCE_STATE_TABLE
51

    
52
from vigilo.models.utils import group_concat
53

    
54
import logging
55

    
56
LOGGER = logging.getLogger(__name__)
57

    
58
__all__ = ['SilenceController']
59

    
60
# pylint: disable-msg=R0201
61
class SilenceController(BaseController):
62
    """
63
    Contrôleur gérant la planification des mises en silence.
64
    """
65

    
66
    # Prédicat pour la restriction de l'accès aux interfaces.
67
    # L'utilisateur doit avoir la permission "vigiboard-silence"
68
    # ou appartenir au groupe "managers" pour accéder à VigiBoard.
69
    access_restriction = All(
70
        not_anonymous(msg=l_("You need to be authenticated")),
71
        Any(in_group('managers'),
72
            has_permission('vigiboard-silence'),
73
            msg=l_("Insufficient privileges for this action"))
74
    )
75

    
76
    def handle_error_message(self, message, redirection_url='./'):
77
        """
78
        Affiche le message dans l'IHM, l'enregistre dans les logs
79
        et renvoie le navigateur vers l'URL de redirection.
80

81
        @param message: message d'erreur à afficher
82
        @type  message: C{str}
83
        @param redirection_url: (optionnel) URL de redirection
84
        @type  redirection_url: C{str}
85
        """
86
        LOGGER.error(message)
87
        flash(message, 'error')
88
        redirect(redirection_url)
89

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

    
99
    def query_silences(self):
100
        """
101
        Retourne une requête SQLAlchemy interrogeant
102
        la table des mises en silence
103
        """
104

    
105
        # Si l'utilisateur fait partie du groupe 'managers', on récupère la
106
        # liste de tous les supitems de la base de données
107
        if in_group('managers').is_met(request.environ):
108
            lls_query = DBSession.query(
109
                    LowLevelService.idservice.label('idsupitem'),
110
                    LowLevelService.servicename.label("servicename"),
111
                    Host.name.label('hostname')
112
                ).join((Host, Host.idhost == LowLevelService.idhost))
113

    
114
            host_query = DBSession.query(
115
                    Host.idhost.label('idsupitem'),
116
                    "NULL",
117
                    Host.name.label('hostname')
118
                )
119

    
120
            supitems = lls_query.union(host_query).subquery()
121

    
122
        # Sinon on ne récupère que les supitems auxquels l'utilisateurs a accès
123
        else:
124
            user_name = request.identity['repoze.who.userid']
125
            supitems = DBSession.query(
126
                UserSupItem.idsupitem.label('idsupitem'),
127
                UserSupItem.servicename.label("servicename"),
128
                UserSupItem.hostname.label('hostname')
129
            ).filter(
130
                UserSupItem.username == user_name
131
            ).distinct().subquery()
132

    
133
        # On interroge la base pour avoir la liste des règles de mise en silence
134
        # correspondant à ces supitems.
135
        states = DBSession.query(
136
                SILENCE_STATE_TABLE.c.idsilence,
137
                StateName.statename
138
            ).join((StateName,
139
                StateName.idstatename == SILENCE_STATE_TABLE.c.idstate)
140
            ).order_by(StateName.statename
141
            ).subquery()
142
        states = DBSession.query(
143
                states.c.idsilence,
144
                group_concat(states.c.statename, ', ').label('states'),
145
            ).group_by(states.c.idsilence
146
            ).subquery()
147
        silences = DBSession.query(
148
                Silence,
149
                supitems.c.hostname,
150
                supitems.c.servicename,
151
                states.c.states
152
            ).join((supitems, supitems.c.idsupitem == Silence.idsupitem)
153
            ).join((states, states.c.idsilence == Silence.idsilence))
154

    
155
        return silences
156

    
157
    def check_silence_rule_existence(self, idsupitem):
158
        """
159
        S'assure qu'aucune règle de mise en silence n'existe dans la base de
160
        données pour le supitem considéré, et affiche un message d'erreur dans
161
        le cas contraire.
162

163
        @param idsupitem: Identifiant du supitem.
164
        @type  idsupitem: C{int}
165
        """
166
        silence = DBSession.query(Silence
167
            ).filter(Silence.idsupitem == idsupitem
168
            ).first()
169
        if not silence:
170
            return
171
        if isinstance(silence.supitem, LowLevelService):
172
            msg = _("Another rule already exists for service '%s' " \
173
                    "on host '%s'.") % (silence.supitem.servicename,
174
                        silence.supitem.host.name)
175
        else:
176
            msg = _("Another rule already exists for host '%s'.") % (
177
                silence.supitem.name)
178
        self.handle_error_message(msg)
179

    
180
    class IndexSchema(schema.Schema):
181
        """Schéma de validation de la méthode index."""
182
        # Si on ne passe pas le paramètre "page" ou qu'on passe une valeur
183
        # invalide ou pas de valeur du tout, alors on affiche la 1ère page.
184
        page = validators.Int(min=1, if_missing=1,
185
            if_invalid=1, not_empty=True)
186
        sort = validators.OneOf(
187
            ['hostname', 'servicename', 'lastmodification',
188
                'author', 'comment', 'states'],
189
            if_missing='lastmodification', if_invalid='lastmodification')
190
        order = validators.OneOf(['desc', 'asc'],
191
            if_missing='desc', if_invalid='desc')
192

    
193
    @validate(
194
        validators=IndexSchema(),
195
        error_handler = process_form_errors)
196
    @expose('silence.html')
197
    @require(access_restriction)
198
    def index(self, page=1, sort=None, order=None):
199
        """
200
        Affiche la liste des règles de mise en silence enregistrées dans
201
        la BDD, que l'utilisateur pourra ensuite éditer ou supprimer.
202

203
        @param sort: (optionnel) Critère de tri de la liste des
204
                     règles de mise en silence enregistrées.
205
        @type  sort: C{str}
206
        @param order: (optionnel) Ordre de tri.
207
        @type  order: C{str}
208
        """
209

    
210
#        # On récupère la langue de l'utilisateur
211
#        lang = get_lang()
212
#        if not lang:
213
#            lang = ['fr']
214
#        lang = lang[0]
215

    
216
        # On récupère tous les enregistrements de la table
217
        # silence, qu'ils concernent des hôtes, des services
218
        # de bas niveau, ou bien des services de haut niveau.
219
        silences = self.query_silences()
220

    
221
        # On trie ces enregistrements selon le critère choisi
222
        # par l'utilisateur (par défaut, la date d'ajout).
223
        sort_keys = {
224
            'hostname': 'hostname',
225
            'servicename': 'servicename',
226
            'lastmodification': Silence.lastmodification,
227
            'author': Silence.author,
228
            'comment': Silence.comment,
229
#            'start': Silence.start,
230
#            'end': Silence.end,
231
            'states': 'states',
232
        }
233
        if sort in sort_keys.keys():
234
            # Tri dans l'ordre croissant
235
            if order != 'desc':
236
                silences = silences.order_by(asc(sort_keys[sort]))
237
            # Tri dans l'ordre décroissant
238
            else:
239
                silences = silences.order_by(desc(sort_keys[sort]))
240

    
241
        # On calcule la pagination
242
        page = paginate.Page(silences, page=page,
243
            items_per_page=int(config['vigiboard_items_per_page']))
244

    
245
#        # On initialise les widgets des calendriers
246
#        # utilisés dans le formulaire de mise en silence.
247
#        start_calendar = CalendarDateTimePicker('start',
248
#                                            button_text = l_("Choose a date"),
249
#                                            date_format = '%Y-%m-%d %H:%M',
250
#                                            calendar_lang = lang)
251
#        end_calendar = CalendarDateTimePicker('end',
252
#                                            button_text = l_("Choose a date"),
253
#                                            date_format = '%Y-%m-%d %H:%M',
254
#                                            calendar_lang = lang)
255

    
256
        # Traduction du nom des colonnes
257
        columns = [
258
            ('hostname', l_('Host')),
259
            ('servicename', l_('Service')),
260
            ('states', l_('States')),
261
            ('lastmodification', l_('Last modification')),
262
            ('author', l_('Author')),
263
            ('comment', l_('Comment'))
264
        ]
265

    
266
        return dict(
267
            page=page,
268
            sort=sort,
269
            order=order,
270
#            start_calendar=start_calendar,
271
#            end_calendar=end_calendar,
272
            columns=columns
273
        )
274

    
275
    @expose('silence_form.html')
276
    @require(access_restriction)
277
    def add(self):
278
        """
279
        Affiche un formulaire d'ajout d'une règle de mise en silence.
280
        """
281
        return dict(
282
            id=None,
283
            hostname=None,
284
            servicename=None,
285
            states=None,
286
            comment=None,
287
#            start_calendar=start_calendar,
288
#            end_calendar=end_calendar,
289
        )
290

    
291
    class UpdateSchema(schema.Schema):
292
        """Schéma de validation de la méthode update."""
293
        id = validators.Int(min=1, not_empty=True)
294

    
295
    @validate(
296
        validators=UpdateSchema(),
297
        error_handler = process_form_errors)
298
    @expose('silence_form.html')
299
    @require(access_restriction)
300
    def update(self, id):
301
        """
302
        Affiche un formulaire de mise à jour d'une règle de mise en silence.
303

304
        @param id: Identifiant de la règle.
305
        @type  id: C{int}
306
        """
307

    
308
        # On s'assure que la règle existe bien dans la base
309
        try:
310
            silence = DBSession.query(Silence
311
                ).filter(Silence.idsilence == id).one()
312
        except InvalidRequestError, e:
313
            msg = _('An exception has been raised while ' \
314
                    'querying the database: %s') % str(e)
315
            self.handle_error_message(msg)
316
        if not silence:
317
            msg = _("Silence rule #%s does not exist.") % id
318
            self.handle_error_message(msg)
319

    
320
        # On s'assure que l'utilisateur dispose bien des permissions sur le
321
        # supitem considéré
322
        user = get_current_user()
323
        if not silence.supitem.is_allowed_for(user):
324
            msg = _("Silence rule #%s does not exist.") % id
325
            self.handle_error_message(msg)
326

    
327
        if hasattr(silence.supitem, 'servicename'):
328
            hostname = silence.supitem.host.name
329
            servicename = silence.supitem.servicename
330
        else:
331
            hostname = silence.supitem.name
332
            servicename = None
333

    
334
        return dict(
335
            id=id,
336
            hostname=hostname,
337
            servicename=servicename,
338
            states=[s.statename for s in silence.states],
339
            comment=silence.comment,
340
#            start_calendar=start_calendar,
341
#            end_calendar=end_calendar,
342
        )
343

    
344
    class CreateOrModifySchema(schema.Schema):
345
        """Schéma de validation de la méthode create_or_modify."""
346
        states = All_(
347
            validators.Set(use_set=True),
348
            validators.OneOf(["WARNING", "CRITICAL", "DOWN", "UNKNOWN"],
349
                testValueList=True, hideList=True, not_empty=True)
350
        )
351
        host = validators.String(not_empty=True)
352
        service = validators.String()
353
        comment = validators.String()
354
        idsilence = validators.Int(min=1, if_missing=None,
355
            if_invalid=None, not_empty=False)
356

    
357
    @validate(
358
        validators=CreateOrModifySchema(),
359
        error_handler = process_form_errors)
360
    @expose()
361
    @require(access_restriction)
362
    def create_or_modify(
363
            self,
364
            states,
365
            host,
366
            service=None,
367
#            start=time.time(),
368
#            end=time.time(),
369
            comment=None,
370
            idsilence=None):
371
        """
372
        Ajoute une règle de mise en silence d'un hôte/service,
373
        ou en modifie une déjà existante.
374

375
        @param states: (optionnel) Liste des états concernés par la règle.
376
        @type  states: C{list} of C{unicode}
377
        @param host: Nom de l'hôte sur lequel porte la règle.
378
        @type  host: C{unicode}
379
        @param service: (optionnel) Nom du service sur lequel
380
            porte la règle.
381
        @type  service: C{unicode}
382
#        @param start: Début de la mise en silence planifiée.
383
#        @type  start: C{str}
384
#        @param end: Fin de la mise en silence planifiée.
385
#        @type  end: C{str}
386
        @param comment: (optionnel) Commentaire accompagnant la règle.
387
        @type  comment: C{unicode}
388
        @param idsilence: (optionnel) Identifiant de la règle dans le cas d'une
389
            mise à jour.
390
        @type  idsilence: C{int}
391
        """
392

    
393
        # TODO: Faire ce traitement dans le schéma de validation
394
        if not states:
395
            msg = _('No state specified for the silence rule.')
396
            self.handle_error_message(msg)
397
        states = list(states)
398

    
399
        # On récupère le nom et l'IP de l'utilisateur.
400
        user = get_current_user()
401
        user_name = user.user_name
402
        user_ip = request.remote_addr
403

    
404
        # On récupère l'identifiant de l'item (hôte
405
        # ou service) concerné par la mise en silence.
406
        idsupitem = SupItem.get_supitem(host, service)
407
        if idsupitem:
408
            try:
409
                supitem = DBSession.query(SupItem
410
                    ).filter(SupItem.idsupitem == idsupitem).one()
411
            except InvalidRequestError, e:
412
                msg = _('An exception has been raised while ' \
413
                        'querying the database: %s') % str(e)
414
                self.handle_error_message(msg)
415
        if not idsupitem or not supitem.is_allowed_for(user):
416
            if not service:
417
                msg = _("Host '%s' does not exist.") % host
418
                self.handle_error_message(msg)
419
            else:
420
                msg = _("Service '%s' does not exist for host '%s'."
421
                    ) % (service, host)
422
                self.handle_error_message(msg)
423

    
424
        # On distingue mise à jour et création :
425

    
426
        # 1. Dans le cas d'une mise à jour :
427
        if idsilence:
428

    
429
            # - On s'assure que la règle existe bien dans la base
430
            try:
431
                silence = DBSession.query(Silence
432
                    ).filter(Silence.idsilence == idsilence).one()
433
            except InvalidRequestError, e:
434
                msg = _('An exception has been raised while ' \
435
                        'querying the database: %s') % str(e)
436
                self.handle_error_message(msg)
437
            if not silence:
438
                msg = _("Silence rule #%s does not exist.") % idsilence
439
                self.handle_error_message(msg)
440

    
441
            # - Si le supitem a été modifié, on vérifie qu'aucune
442
            #   autre règle n'existe pour le nouveau supitem
443
            if silence.idsupitem != idsupitem:
444
                self.check_silence_rule_existence(idsupitem)
445

    
446
            # On supprime les états existants
447
            silence.states = []
448

    
449
        # 2. Dans le cas d'une création :
450
        else:
451

    
452
            # - On s'assure qu'aucune autre règle n'existe pour le supitem
453
            self.check_silence_rule_existence(idsupitem)
454

    
455
            # - Et on crée l'objet représentant la règle
456
            silence = Silence()
457

    
458
        # Dans les 2 cas, on met à jour l'objet avec
459
        # les informations passées en paramètre
460
        silence.idsupitem = idsupitem
461
        silence.comment = comment
462
        silence.lastmodification = datetime.now().replace(microsecond=0)
463
        silence.author = user_name
464
        try:
465
            DBSession.add(silence)
466
            for state in states:
467
                s = DBSession.query(StateName
468
                    ).filter(StateName.statename == state).one()
469
                silence.states.append(s)
470
            DBSession.flush()
471
        except (IntegrityError, InvalidRequestError), e:
472
            msg = _('An exception has been raised while ' \
473
                    'updating the database: %s') % str(e)
474
            self.handle_error_message(msg)
475

    
476
        # On notifie l'opération dans les logs, on affiche un message de
477
        # succès, et on redirige le navigateur vers la liste des règles de
478
        # mise en silence.
479
        if idsilence:
480
            # Mise à jour d'une règle portant sur un service
481
            if hasattr(silence.supitem, 'servicename'):
482
                LOGGER.info(_(
483
                    'User %(user)s (IP: %(ip)s) updated silence rule ' \
484
                    '#%(id)s for service %(service)s on host %(host)s.'
485
                ) % {
486
                    'user': user_name,
487
                    'ip': user_ip,
488
                    'id': idsilence,
489
                    'host': host,
490
                    'service': service
491
                })
492
                flash(_(
493
                    'Silence rule #%(id)s (host: %(host)s, service: ' \
494
                    '%(service)s) has been successfully updated.') % {
495
                        'id': idsilence,
496
                        'host': host,
497
                        'service': service
498
                })
499
            # Mise à jour d'une règle portant sur un hôte
500
            else:
501
                LOGGER.info(_(
502
                    'User %(user)s (IP: %(ip)s) updated 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(_(
510
                    'Silence rule #%(id)s (host: %(host)s) ' \
511
                    'has been successfully updated.') % {
512
                        'id': idsilence,
513
                        'host': host
514
                })
515
        else:
516
            # Ajout d'une règle portant sur un service
517
            if service:
518
                LOGGER.info(_(
519
                    'User %(user)s (IP: %(ip)s) added a silence rule (#' \
520
                    '%(id)s) for service %(service)s on host %(host)s.'
521
                ) % {
522
                    'user': user_name,
523
                    'ip': user_ip,
524
                    'id': idsilence,
525
                    'host': host,
526
                    'service': service
527
                })
528
                flash(_('A new silence rule (#%(id)s) has been added for '
529
                    'service "%(service)s" on host "%(host)s".') % {
530
                        'id': silence.idsilence,
531
                        'service': service,
532
                        'host': host
533
                    })
534
            # Ajout d'une règle portant sur un hôte
535
            else:
536
                LOGGER.info(_(
537
                    'User %(user)s (IP: %(ip)s) added a silence rule ' \
538
                    '(#%(id)s) for host %(host)s.') % {
539
                        'user': user_name,
540
                        'ip': user_ip,
541
                        'id': idsilence,
542
                        'host': host
543
                })
544
                flash(_('A new silence rule (#%(id)s) has been added for the '
545
                    'host "%(host)s".') % {
546
                        'id': silence.idsilence,
547
                        'host': host
548
                    })
549
        redirect('./')
550

    
551

    
552
    class DeleteSchema(schema.Schema):
553
        """Schéma de validation de la méthode delete."""
554
        id = All_(
555
            validators.Set(use_set=True),
556
            ForEach(validators.Int(min=1)),
557
        )
558

    
559
    @validate(
560
        validators=DeleteSchema(),
561
        error_handler = process_form_errors)
562
    @expose()
563
    @require(access_restriction)
564
    def delete(self, id):
565
        """
566
        Suppression d'une règle ou d'une liste de règles de mise en silence.
567

568
        @param id: Liste des identifiants des règles à supprimer.
569
        @type  id: C{list} of C{int}
570
        """
571

    
572
        # TODO: Faire ce traitement dans le schéma de validation
573
        if not id:
574
            msg = _('No silence rule id specified.')
575
            self.handle_error_message(msg)
576
        id = list(id)
577

    
578
        # On recherche les règles dans la BDD.
579
        try:
580
            silences = DBSession.query(Silence
581
                ).filter(Silence.idsilence.in_(id)).all()
582
        except InvalidRequestError, e:
583
            msg = _('An exception has been raised while ' \
584
                    'querying the database: %s') % str(e)
585
            self.handle_error_message(msg)
586

    
587
        # On s'assure que toutes les règles ont bien été trouvées dans la
588
        # base, faute de quoi on lève une erreur et on arrête le traitement
589
        if len(silences) != len(id):
590
            missing_ids = [
591
                i for i in id if i not in [s.idsilence for s in silences]]
592
            if len(missing_ids) > 1:
593
                msg = _('Error: the following silence rules do not exist:' \
594
                    ' %s.') % ", ".join('#' + str(i) for i in missing_ids)
595
            else:
596
                msg = _('Error: silence rule #%s does not exist.'
597
                    ) % ", ".join(str(i) for i in missing_ids)
598
            self.handle_error_message(msg)
599

    
600
        # On s'assure que l'utilisateur dispose bien des permissions nécessaires
601
        # pour supprimer chacune des règles
602
        user = get_current_user()
603
        for s in silences:
604
            if not s.supitem.is_allowed_for(user):
605
                msg = _("Silence rule #%s does not exist.") % s.idsilence
606
                self.handle_error_message(msg)
607

    
608
        # On supprime les règles dans la BDD.
609
        try:
610
            for silence in silences:
611
                DBSession.delete(silence)
612
            DBSession.flush()
613
        except InvalidRequestError, e:
614
            msg = _('An exception has been raised while ' \
615
                    'deleting the silence rules: %s') % str(e)
616
            self.handle_error_message(msg)
617

    
618
        # On notifie l'opération dans les logs
619
        user_name = user.user_name
620
        user_ip = request.remote_addr
621
        for s in silences:
622
            # Règle concernant un service de bas niveau
623
            if hasattr(s.supitem, 'servicename'):
624
                LOGGER.info(_(
625
                    'User %(user)s (IP: %(ip)s) deleted silence rule ' \
626
                    '#%(id)s for service %(service)s on host ' \
627
                    '%(host)s') % {
628
                        'user': user_name,
629
                        'ip': user_ip,
630
                        'id': s.idsilence,
631
                        'host': s.supitem.host.name,
632
                        'service': s.supitem.servicename
633
                })
634
            # Règle concernant un hôte
635
            else:
636
                LOGGER.info(_(
637
                    'User %(user)s (IP: %(ip)s) deleted silence rule ' \
638
                    '#%(id)s for host %(host)s') % {
639
                        'user': user_name,
640
                        'ip': user_ip,
641
                        'id': s.idsilence,
642
                        'host': s.supitem.name,
643
                })
644

    
645
        # On affiche un message de succès
646
        if len(id) > 1:
647
            flash(_('The following silence rules have been successfully ' \
648
                'deleted: %s.') % ", ".join(str(i) for i in id))
649
        else:
650
            flash(_('Silence rule #%s has been successfully ' \
651
                'deleted.') % id[0])
652

    
653
        # On redirige le navigateur vers la page d'index
654
        redirect('./', )
655