Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

vigiboard / vigiboard / controllers / silence.py @ 9332c7c0

History | View | Annotate | Download (24.5 KB)

1
# -*- coding: utf-8 -*-
2
# vim:set expandtab tabstop=4 shiftwidth=4:
3
###############################################################################
4
#
5
# Copyright (C) 2007-2015 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
from vigiboard.lib import error_handler
55

    
56
import logging
57

    
58
LOGGER = logging.getLogger(__name__)
59

    
60
__all__ = ['SilenceController']
61

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

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

    
78
    def process_form_errors(self, *argv, **kwargv):
79
        """
80
        Gestion des erreurs de validation : on affiche les erreurs
81
        puis on redirige vers la dernière page accédée.
82
        """
83
        for k in tmpl_context.form_errors:
84
            flash("'%s': %s" % (k, tmpl_context.form_errors[k]), 'error')
85
        redirect(request.environ.get('HTTP_REFERER', '/'))
86

    
87
    def query_silences(self):
88
        """
89
        Retourne une requête SQLAlchemy interrogeant
90
        la table des mises en silence
91
        """
92

    
93
        # Si l'utilisateur fait partie du groupe 'managers', on récupère la
94
        # liste de tous les supitems de la base de données
95
        if in_group('managers').is_met(request.environ):
96
            lls_query = DBSession.query(
97
                    LowLevelService.idservice.label('idsupitem'),
98
                    LowLevelService.servicename.label("servicename"),
99
                    Host.name.label('hostname')
100
                ).join((Host, Host.idhost == LowLevelService.idhost))
101

    
102
            host_query = DBSession.query(
103
                    Host.idhost.label('idsupitem'),
104
                    "NULL",
105
                    Host.name.label('hostname')
106
                )
107

    
108
            supitems = lls_query.union(host_query).subquery()
109

    
110
        # Sinon on ne récupère que les supitems auxquels l'utilisateurs a accès
111
        else:
112
            user_name = request.identity['repoze.who.userid']
113
            supitems = DBSession.query(
114
                UserSupItem.idsupitem.label('idsupitem'),
115
                UserSupItem.servicename.label("servicename"),
116
                UserSupItem.hostname.label('hostname')
117
            ).filter(
118
                UserSupItem.username == user_name
119
            ).distinct().subquery()
120

    
121
        # On interroge la base pour avoir la liste des règles de mise en silence
122
        # correspondant à ces supitems.
123
        states = DBSession.query(
124
                StateName.statename,
125
                SILENCE_STATE_TABLE.c.idsilence,
126
            ).join((SILENCE_STATE_TABLE,
127
                StateName.idstatename == SILENCE_STATE_TABLE.c.idstate)
128
            ).order_by(StateName.statename
129
            ).subquery()
130
        states = DBSession.query(
131
                states.c.idsilence,
132
                group_concat(states.c.statename, ', ').label('states'),
133
            ).group_by(states.c.idsilence
134
            ).subquery()
135
        silences = DBSession.query(
136
                Silence,
137
                supitems.c.hostname,
138
                supitems.c.servicename,
139
                states.c.states
140
            ).join((supitems, supitems.c.idsupitem == Silence.idsupitem)
141
            ).join((states, states.c.idsilence == Silence.idsilence))
142

    
143
        return silences
144

    
145
    def check_silence_rule_existence(self, idsupitem):
146
        """
147
        S'assure qu'aucune règle de mise en silence n'existe dans la base de
148
        données pour le supitem considéré, et affiche un message d'erreur dans
149
        le cas contraire.
150

151
        @param idsupitem: Identifiant du supitem.
152
        @type  idsupitem: C{int}
153
        """
154
        silence = DBSession.query(Silence
155
            ).filter(Silence.idsupitem == idsupitem
156
            ).first()
157
        if not silence:
158
            return
159
        if isinstance(silence.supitem, LowLevelService):
160
            msg = _("Another rule already exists for service '%s' " \
161
                    "on host '%s'.") % (silence.supitem.servicename,
162
                        silence.supitem.host.name)
163
        else:
164
            msg = _("Another rule already exists for host '%s'.") % (
165
                silence.supitem.name)
166
        error_handler.handle_error_message(msg)
167

    
168
    class IndexSchema(schema.Schema):
169
        """Schéma de validation de la méthode index."""
170
        # Si on ne passe pas le paramètre "page" ou qu'on passe une valeur
171
        # invalide ou pas de valeur du tout, alors on affiche la 1ère page.
172
        page = validators.Int(min=1, if_missing=1,
173
            if_invalid=1, not_empty=True)
174
        sort = validators.OneOf(
175
            ['hostname', 'servicename', 'lastmodification',
176
                'author', 'comment', 'states'],
177
            if_missing='lastmodification', if_invalid='lastmodification')
178
        order = validators.OneOf(['desc', 'asc'],
179
            if_missing='desc', if_invalid='desc')
180

    
181
    @validate(
182
        validators=IndexSchema(),
183
        error_handler = process_form_errors)
184
    @expose('silence.html')
185
    @require(access_restriction)
186
    def index(self, page=1, sort=None, order=None):
187
        """
188
        Affiche la liste des règles de mise en silence enregistrées dans
189
        la BDD, que l'utilisateur pourra ensuite éditer ou supprimer.
190

191
        @param sort: (optionnel) Critère de tri de la liste des
192
                     règles de mise en silence enregistrées.
193
        @type  sort: C{str}
194
        @param order: (optionnel) Ordre de tri.
195
        @type  order: C{str}
196
        """
197

    
198
#        # On récupère la langue de l'utilisateur
199
#        lang = get_lang()
200
#        if not lang:
201
#            lang = ['fr']
202
#        lang = lang[0]
203

    
204
        # On récupère tous les enregistrements de la table
205
        # silence, qu'ils concernent des hôtes, des services
206
        # de bas niveau, ou bien des services de haut niveau.
207
        silences = self.query_silences()
208

    
209
        # On trie ces enregistrements selon le critère choisi
210
        # par l'utilisateur (par défaut, la date d'ajout).
211
        sort_keys = {
212
            'hostname': 'hostname',
213
            'servicename': 'servicename',
214
            'lastmodification': Silence.lastmodification,
215
            'author': Silence.author,
216
            'comment': Silence.comment,
217
#            'start': Silence.start,
218
#            'end': Silence.end,
219
            'states': 'states',
220
        }
221
        if sort in sort_keys.keys():
222
            # Tri dans l'ordre croissant
223
            if order != 'desc':
224
                silences = silences.order_by(asc(sort_keys[sort]))
225
            # Tri dans l'ordre décroissant
226
            else:
227
                silences = silences.order_by(desc(sort_keys[sort]))
228

    
229
        # On calcule la pagination
230
        page = paginate.Page(silences, page=page,
231
            items_per_page=int(config['vigiboard_items_per_page']))
232

    
233
#        # On initialise les widgets des calendriers
234
#        # utilisés dans le formulaire de mise en silence.
235
#        start_calendar = CalendarDateTimePicker('start',
236
#                                            button_text = l_("Choose a date"),
237
#                                            date_format = '%Y-%m-%d %H:%M',
238
#                                            calendar_lang = lang)
239
#        end_calendar = CalendarDateTimePicker('end',
240
#                                            button_text = l_("Choose a date"),
241
#                                            date_format = '%Y-%m-%d %H:%M',
242
#                                            calendar_lang = lang)
243

    
244
        # Traduction du nom des colonnes
245
        columns = [
246
            ('hostname', l_('Host')),
247
            ('servicename', l_('Service')),
248
            ('states', l_('States')),
249
            ('lastmodification', l_('Last modification')),
250
            ('author', l_('Author')),
251
            ('comment', l_('Comment'))
252
        ]
253

    
254
        return dict(
255
            page=page,
256
            sort=sort,
257
            order=order,
258
#            start_calendar=start_calendar,
259
#            end_calendar=end_calendar,
260
            columns=columns
261
        )
262

    
263
    @expose('silence_form.html')
264
    @require(access_restriction)
265
    def add(self):
266
        """
267
        Affiche un formulaire d'ajout d'une règle de mise en silence.
268
        """
269
        return dict(
270
            id=None,
271
            hostname=None,
272
            servicename=None,
273
            states=None,
274
            comment=None,
275
#            start_calendar=start_calendar,
276
#            end_calendar=end_calendar,
277
        )
278

    
279
    class UpdateSchema(schema.Schema):
280
        """Schéma de validation de la méthode update."""
281
        id = validators.Int(min=1, not_empty=True)
282

    
283
    @validate(
284
        validators=UpdateSchema(),
285
        error_handler = process_form_errors)
286
    @expose('silence_form.html')
287
    @require(access_restriction)
288
    def update(self, id):
289
        """
290
        Affiche un formulaire de mise à jour d'une règle de mise en silence.
291

292
        @param id: Identifiant de la règle.
293
        @type  id: C{int}
294
        """
295

    
296
        # On s'assure que la règle existe bien dans la base
297
        try:
298
            silence = DBSession.query(Silence
299
                ).filter(Silence.idsilence == id).one()
300
        except InvalidRequestError, e:
301
            msg = _('An exception has been raised while ' \
302
                    'querying the database: %s') % str(e)
303
            error_handler.handle_error_message(msg)
304
        if not silence:
305
            msg = _("Silence rule #%s does not exist.") % id
306
            error_handler.handle_error_message(msg)
307

    
308
        # On s'assure que l'utilisateur dispose bien des permissions sur le
309
        # supitem considéré
310
        user = get_current_user()
311
        if not silence.supitem.is_allowed_for(user):
312
            msg = _("Silence rule #%s does not exist.") % id
313
            error_handler.handle_error_message(msg)
314

    
315
        if hasattr(silence.supitem, 'servicename'):
316
            hostname = silence.supitem.host.name
317
            servicename = silence.supitem.servicename
318
        else:
319
            hostname = silence.supitem.name
320
            servicename = None
321

    
322
        return dict(
323
            id=id,
324
            hostname=hostname,
325
            servicename=servicename,
326
            states=[s.statename for s in silence.states],
327
            comment=silence.comment,
328
#            start_calendar=start_calendar,
329
#            end_calendar=end_calendar,
330
        )
331

    
332
    class CreateOrModifySchema(schema.Schema):
333
        """Schéma de validation de la méthode create_or_modify."""
334
        states = All_(
335
            validators.Set(use_set=True),
336
            validators.OneOf(["WARNING", "CRITICAL", "DOWN", "UNKNOWN"],
337
                testValueList=True, hideList=True, not_empty=True)
338
        )
339
        host = validators.String(not_empty=True)
340
        service = validators.String()
341
        comment = validators.String()
342
        idsilence = validators.Int(min=1, if_missing=None,
343
            if_invalid=None, not_empty=False)
344

    
345
    @validate(
346
        validators=CreateOrModifySchema(),
347
        error_handler = process_form_errors)
348
    @expose()
349
    @require(access_restriction)
350
    def create_or_modify(
351
            self,
352
            states,
353
            host,
354
            service=None,
355
#            start=time.time(),
356
#            end=time.time(),
357
            comment=None,
358
            idsilence=None):
359
        """
360
        Ajoute une règle de mise en silence d'un hôte/service,
361
        ou en modifie une déjà existante.
362

363
        @param states: (optionnel) Liste des états concernés par la règle.
364
        @type  states: C{list} of C{unicode}
365
        @param host: Nom de l'hôte sur lequel porte la règle.
366
        @type  host: C{unicode}
367
        @param service: (optionnel) Nom du service sur lequel
368
            porte la règle.
369
        @type  service: C{unicode}
370
#        @param start: Début de la mise en silence planifiée.
371
#        @type  start: C{str}
372
#        @param end: Fin de la mise en silence planifiée.
373
#        @type  end: C{str}
374
        @param comment: (optionnel) Commentaire accompagnant la règle.
375
        @type  comment: C{unicode}
376
        @param idsilence: (optionnel) Identifiant de la règle dans le cas d'une
377
            mise à jour.
378
        @type  idsilence: C{int}
379
        """
380

    
381
        # TODO: Faire ce traitement dans le schéma de validation
382
        if not states:
383
            msg = _('No state specified for the silence rule.')
384
            error_handler.handle_error_message(msg)
385
        states = list(states)
386

    
387
        # On récupère le nom et l'IP de l'utilisateur.
388
        user = get_current_user()
389
        user_name = user.user_name
390
        user_ip = request.remote_addr
391

    
392
        # On récupère l'identifiant de l'item (hôte
393
        # ou service) concerné par la mise en silence.
394
        idsupitem = SupItem.get_supitem(host, service)
395
        if idsupitem:
396
            try:
397
                supitem = DBSession.query(SupItem
398
                    ).filter(SupItem.idsupitem == idsupitem).one()
399
            except InvalidRequestError, e:
400
                msg = _('An exception has been raised while ' \
401
                        'querying the database: %s') % str(e)
402
                error_handler.handle_error_message(msg)
403
        if not idsupitem or not supitem.is_allowed_for(user):
404
            if not service:
405
                msg = _("Host '%s' does not exist.") % host
406
                error_handler.handle_error_message(msg)
407
            else:
408
                msg = _("Service '%s' does not exist for host '%s'."
409
                    ) % (service, host)
410
                error_handler.handle_error_message(msg)
411

    
412
        # On distingue mise à jour et création :
413

    
414
        # 1. Dans le cas d'une mise à jour :
415
        if idsilence:
416

    
417
            # - On s'assure que la règle existe bien dans la base
418
            try:
419
                silence = DBSession.query(Silence
420
                    ).filter(Silence.idsilence == idsilence).one()
421
            except InvalidRequestError, e:
422
                msg = _('An exception has been raised while ' \
423
                        'querying the database: %s') % str(e)
424
                error_handler.handle_error_message(msg)
425
            if not silence:
426
                msg = _("Silence rule #%s does not exist.") % idsilence
427
                error_handler.handle_error_message(msg)
428

    
429
            # - Si le supitem a été modifié, on vérifie qu'aucune
430
            #   autre règle n'existe pour le nouveau supitem
431
            if silence.idsupitem != idsupitem:
432
                self.check_silence_rule_existence(idsupitem)
433

    
434
            # On supprime les états existants
435
            silence.states = []
436

    
437
        # 2. Dans le cas d'une création :
438
        else:
439

    
440
            # - On s'assure qu'aucune autre règle n'existe pour le supitem
441
            self.check_silence_rule_existence(idsupitem)
442

    
443
            # - Et on crée l'objet représentant la règle
444
            silence = Silence()
445

    
446
        # Dans les 2 cas, on met à jour l'objet avec
447
        # les informations passées en paramètre
448
        silence.idsupitem = idsupitem
449
        silence.comment = comment
450
        silence.lastmodification = datetime.now().replace(microsecond=0)
451
        silence.author = user_name
452
        try:
453
            DBSession.add(silence)
454
            for state in states:
455
                s = DBSession.query(StateName
456
                    ).filter(StateName.statename == state).one()
457
                silence.states.append(s)
458
            DBSession.flush()
459
        except (IntegrityError, InvalidRequestError), e:
460
            msg = _('An exception has been raised while ' \
461
                    'updating the database: %s') % str(e)
462
            error_handler.handle_error_message(msg)
463

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

    
539

    
540
    class DeleteSchema(schema.Schema):
541
        """Schéma de validation de la méthode delete."""
542
        id = All_(
543
            validators.Set(use_set=True),
544
            ForEach(validators.Int(min=1)),
545
        )
546

    
547
    @validate(
548
        validators=DeleteSchema(),
549
        error_handler = process_form_errors)
550
    @expose()
551
    @require(access_restriction)
552
    def delete(self, id):
553
        """
554
        Suppression d'une règle ou d'une liste de règles de mise en silence.
555

556
        @param id: Liste des identifiants des règles à supprimer.
557
        @type  id: C{list} of C{int}
558
        """
559

    
560
        # TODO: Faire ce traitement dans le schéma de validation
561
        if not id:
562
            msg = _('No silence rule id specified.')
563
            error_handler.handle_error_message(msg)
564
        id = list(id)
565

    
566
        # On recherche les règles dans la BDD.
567
        try:
568
            silences = DBSession.query(Silence
569
                ).filter(Silence.idsilence.in_(id)).all()
570
        except InvalidRequestError, e:
571
            msg = _('An exception has been raised while ' \
572
                    'querying the database: %s') % str(e)
573
            error_handler.handle_error_message(msg)
574

    
575
        # On s'assure que toutes les règles ont bien été trouvées dans la
576
        # base, faute de quoi on lève une erreur et on arrête le traitement
577
        if len(silences) != len(id):
578
            missing_ids = [
579
                i for i in id if i not in [s.idsilence for s in silences]]
580
            if len(missing_ids) > 1:
581
                msg = _('Error: the following silence rules do not exist:' \
582
                    ' %s.') % ", ".join('#' + str(i) for i in missing_ids)
583
            else:
584
                msg = _('Error: silence rule #%s does not exist.'
585
                    ) % ", ".join(str(i) for i in missing_ids)
586
            error_handler.handle_error_message(msg)
587

    
588
        # On s'assure que l'utilisateur dispose bien des permissions nécessaires
589
        # pour supprimer chacune des règles
590
        user = get_current_user()
591
        for s in silences:
592
            if not s.supitem.is_allowed_for(user):
593
                msg = _("Silence rule #%s does not exist.") % s.idsilence
594
                error_handler.handle_error_message(msg)
595

    
596
        # On supprime les règles dans la BDD.
597
        try:
598
            for silence in silences:
599
                DBSession.delete(silence)
600
            DBSession.flush()
601
        except InvalidRequestError, e:
602
            msg = _('An exception has been raised while ' \
603
                    'deleting the silence rules: %s') % str(e)
604
            error_handler.handle_error_message(msg)
605

    
606
        # On notifie l'opération dans les logs
607
        user_name = user.user_name
608
        user_ip = request.remote_addr
609
        for s in silences:
610
            # Règle concernant un service de bas niveau
611
            if hasattr(s.supitem, 'servicename'):
612
                LOGGER.info(_(
613
                    'User %(user)s (IP: %(ip)s) deleted silence rule ' \
614
                    '#%(id)s for service %(service)s on host ' \
615
                    '%(host)s') % {
616
                        'user': user_name,
617
                        'ip': user_ip,
618
                        'id': s.idsilence,
619
                        'host': s.supitem.host.name,
620
                        'service': s.supitem.servicename
621
                })
622
            # Règle concernant un hôte
623
            else:
624
                LOGGER.info(_(
625
                    'User %(user)s (IP: %(ip)s) deleted silence rule ' \
626
                    '#%(id)s for host %(host)s') % {
627
                        'user': user_name,
628
                        'ip': user_ip,
629
                        'id': s.idsilence,
630
                        'host': s.supitem.name,
631
                })
632

    
633
        # On affiche un message de succès
634
        if len(id) > 1:
635
            flash(_('The following silence rules have been successfully ' \
636
                'deleted: %s.') % ", ".join(str(i) for i in id))
637
        else:
638
            flash(_('Silence rule #%s has been successfully ' \
639
                'deleted.') % id[0])
640

    
641
        # On redirige le navigateur vers la page d'index
642
        redirect('./', )
643