Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

vigiboard / vigiboard / controllers / vigiboardrequest.py @ 5d20c2c5

History | View | Annotate | Download (17 KB)

1
# -*- coding: utf-8 -*-
2
# vim:set expandtab tabstop=4 shiftwidth=4: 
3
"""Gestion de la requête, des plugins et de l'affichage du Vigiboard"""
4

    
5
from time import mktime
6
from logging import getLogger
7

    
8
from tg import url, config, tmpl_context
9
from tg.i18n import get_lang
10
from pylons.i18n import ugettext as _
11
from sqlalchemy import not_, and_, asc, desc
12
from sqlalchemy.sql.expression import or_, null as expr_null, union
13

    
14
from vigilo.models.configure import DBSession
15
from vigilo.models import Event, CorrEvent, EventHistory, \
16
                        Host, LowLevelService, StateName
17
from vigilo.models.secondary_tables import HOST_GROUP_TABLE, \
18
                                            SERVICE_GROUP_TABLE
19
from vigiboard.widgets.edit_event import EditEventForm
20
from vigiboard.widgets.search_form import SearchForm
21
from vigiboard.controllers.vigiboard_plugin import VigiboardRequestPlugin
22

    
23
LOGGER = getLogger(__name__)
24

    
25
class VigiboardRequest():
26
    """
27
    Classe gérant la génération de la requête finale,
28
    le préformatage des événements et celui des historiques
29
    """
30

    
31
    class_ack = {
32
        'None': '',
33
        'Acknowledged': '_Ack',
34
        'AAClosed': '_Ack',
35
    }
36

    
37
    def __init__(self, user):
38
        """
39
        Initialisation de toutes les variables nécessaires :
40
        - la liste des groupes de l'utilisateur,
41
        - la langue de l'utilisateur
42
        - les différentes étapes de la génération de la requête,
43
        - la liste des plugins à appliquer.
44
        """
45

    
46
        # TODO: Utiliser le champ "language" du modèle pour cet utilisateur ?
47
        # On récupère la langue du navigateur de l'utilisateur
48
        lang = get_lang()
49
        if not lang:
50
            lang = config['lang']
51
        else:
52
            lang = lang[0]
53

    
54
        self.user_groups = user.groups
55
        self.lang = lang
56
        self.generaterq = False
57
        
58
        lls_query = DBSession.query(
59
            LowLevelService.idservice.label("idsupitem"),
60
            LowLevelService.servicename.label("servicename"),
61
            Host.name.label("hostname"),
62
            SERVICE_GROUP_TABLE.c.idgroup.label("idservicegroup"),
63
            HOST_GROUP_TABLE.c.idgroup.label("idhostgroup"),
64
        ).join(
65
           (Host, Host.idhost == LowLevelService.idhost),
66
        ).outerjoin(
67
            (HOST_GROUP_TABLE, 
68
                HOST_GROUP_TABLE.c.idhost == LowLevelService.idhost),
69
            (SERVICE_GROUP_TABLE, 
70
                SERVICE_GROUP_TABLE.c.idservice == LowLevelService.idservice),
71
        ).filter(
72
            or_(
73
                HOST_GROUP_TABLE.c.idgroup.in_(self.user_groups),
74
                SERVICE_GROUP_TABLE.c.idgroup.in_(self.user_groups),
75
            ),
76
        )
77
               
78
        host_query = DBSession.query(
79
            Host.idhost.label("idsupitem"),
80
            expr_null().label("servicename"),
81
            Host.name.label("hostname"),
82
            expr_null().label("idservicegroup"),
83
            HOST_GROUP_TABLE.c.idgroup.label('idhostgroup'),
84
        ).join((HOST_GROUP_TABLE, HOST_GROUP_TABLE.c.idhost == Host.idhost)
85
        ).filter(HOST_GROUP_TABLE.c.idgroup.label('idhostgroup'
86
            ).in_(self.user_groups),
87
        )
88

    
89
        # Object Selectable renvoyant des informations sur un supitem
90
        # concerné par une alerte, avec prise en compte des droits d'accès.
91
        # On est obligés d'utiliser sqlalchemy.sql.expression.union
92
        # pour indiquer à SQLAlchemy de NE PAS regrouper les tables
93
        # dans la requête principale, sans quoi les résultats sont
94
        # incorrects.
95
        self.items = union(lls_query, host_query, correlate=False).alias()
96

    
97
        # Éléments à retourner (SELECT ...)
98
        self.table = []
99

    
100
        # Tables sur lesquelles porte la récupération (JOIN)
101
        self.join = []
102

    
103
        # Tables sur lesquelles porte la récupération (OUTER JOIN)
104
        self.outerjoin = []
105

    
106
        # Critères de filtrage (WHERE)
107
        self.filter = [
108
            # On masque les événements avec l'état OK
109
            # et traités (status == u'AAClosed').
110
            not_(and_(
111
                StateName.statename.in_([u'OK', u'UP']),
112
                CorrEvent.status == u'AAClosed'
113
            )),
114
            CorrEvent.timestamp_active != None,
115
        ]
116

    
117
        # Permet de définir le sens de tri pour la priorité.
118
        if config['vigiboard_priority_order'] == 'asc':
119
            priority_order = asc(CorrEvent.priority)
120
        else:
121
            priority_order = desc(CorrEvent.priority)
122

    
123
        # Tris (ORDER BY)
124
        self.orderby = [
125
            desc(CorrEvent.status),         # None, Acknowledged, AAClosed
126
            priority_order,                 # Priorité ITIL (entier).
127
            desc(StateName.order),          # Etat courant (entier).
128
            desc(Event.timestamp),
129
        ]
130

    
131
        # Regroupements (GROUP BY)
132
        self.groupby = [
133
            StateName.order,
134
            Event.timestamp,
135
            CorrEvent.status,
136
            CorrEvent.priority,
137
        ]
138

    
139
        self.plugin = []
140
        self.events = []
141
        self.idevents = []
142
        self.hist = []
143
        self.req = DBSession
144
        self.context_fct = []
145

    
146
    def add_plugin(self, *argv):
147
        """
148
        Ajout d'un plugin, on lui prélève ses ajouts dans la requête
149
        """
150
        for i in argv:
151
            if isinstance(i, VigiboardRequestPlugin):
152
                if i.table:
153
                    self.add_table(*i.table)
154
                if i.join:
155
                    self.add_join(*i.join)
156
                if i.outerjoin:
157
                    self.add_outer_join(*i.outerjoin)
158
                if i.filter:
159
                    self.add_filter(*i.filter)
160
                if i.groupby:    
161
                    self.add_group_by(*i.groupby)
162
                if i.orderby:
163
                    self.add_order_by(*i.orderby)
164
                self.plugin.append(i)
165

    
166
    def generate_request(self):
167
        """
168
        Génération de la requête avec l'ensemble des données stockées
169
        et la place dans la variable rq de la classe
170
        """
171
        if self.generaterq:
172
            return
173

    
174
        for plug in config.get('vigiboard_plugins', []):
175
            try:
176
                mypac = __import__(
177
                    'vigiboard.controllers.vigiboard_plugin.' +\
178
                            plug[0], globals(), locals(), [plug[1]], -1)
179
                self.add_plugin(getattr(mypac, plug[1])())
180
            except ImportError:
181
                # On loggue l'erreur et on ignore le plugin.
182
                LOGGER.error(_('No such plugin "%s"') % plug[0])
183

    
184
        self.join.append((StateName, StateName.idstatename == \
185
                                        Event.current_state))
186
        self.add_group_by(*self.table)
187

    
188
        # query et join ont besoin de referrence
189
        self.req = self.req.query(*self.table)
190
        self.req = self.req.join(*self.join)
191

    
192
        # le reste, non
193
        for i in self.outerjoin:
194
            self.req = self.req.outerjoin(i)
195
        for i in self.filter:
196
            self.req = self.req.filter(i)
197
        for i in self.groupby:
198
            self.req = self.req.group_by(i)
199
        for i in self.orderby:
200
            self.req = self.req.order_by(i)
201

    
202
        self.generaterq = True
203

    
204
    def num_rows(self):
205
        """
206
        Retourne le nombre de lignes de la requête.
207
        Si celle-ci n'est pas encore générée, on le fait.
208

209
        @return: Nombre de ligne
210
        """
211

    
212
        self.generate_request()
213
        return self.req.count()
214

    
215
    def add_table(self, *argv):
216
        """
217
        Ajoute une ou plusieurs tables/élément d'une table à
218
        la requête.
219

220
        @param argv: Liste des tables à ajouter
221
        """
222
        
223
        # On vérifie qu'il n'y a pas de doublons dans la liste finale
224
        # des tables.
225
        
226
        for i in argv :
227
            for j in self.table:
228
                if str(i) == str(j):
229
                    break
230
            self.table.append(i)
231

    
232
    def add_join(self, *argv):
233
        """
234
        Ajoute une ou plusieurs jointures à
235
        la requête.
236

237
        @param argv: Liste des jointures à ajouter
238
        """
239
        
240
        # On vérifie qu'il n'y a pas de doublons dans la liste finale
241
        # des jointures.
242
        
243
        for i in argv:
244
            for j in self.join:
245
                if str(i) == str(j):
246
                    break
247
            self.join.append(i)
248

    
249
    def add_outer_join(self, *argv):
250
        """
251
        Ajoute une ou plusieurs jointures externes à
252
        la requête.
253

254
        @param argv: Liste des jointures externes à ajouter
255
        """
256
        
257
        # On vérifie qu'il n'y a pas de doublons dans la liste finale
258
        # des jointures externes.
259
        
260
        for i in argv:
261
            for j in self.outerjoin:
262
                if str(i) == str(j):
263
                    break
264
            self.outerjoin.append(i)    
265

    
266
    def add_filter(self, *argv):
267
        """
268
        Ajoute un ou plusieurs filtres à la requête.
269

270
        @param argv: Liste des filtres à ajouter
271
        """
272
        
273
        # On vérifie qu'il n'y a pas de doublons dans la liste finale
274
        # des filtres.
275
        
276
        for i in argv:
277
            for j in self.filter:
278
                if str(i) == str(j):
279
                    break
280
            self.filter.append(i)
281

    
282
    def add_group_by(self, *argv):
283
        """
284
        Ajoute un ou plusieurs groupements à la requête.
285

286
        @param argv: Liste des groupements à ajouter
287
        """
288
        
289
        # On vérifie qu'il n'y a pas de doublons dans la liste finale
290
        # des groupements.
291
        
292
        for i in argv:
293
            for j in self.groupby:
294
                try:
295
                    if str(i) == str(j):
296
                        break
297
                # SQLAlchemy lève cette exception pour certains attributes,
298
                # par exemple les attributs définis avec synonym().
299
                except AttributeError:
300
                    pass
301
            self.groupby.append(i)
302

    
303
    def add_order_by(self, *argv):
304
        """
305
        Ajoute un ou plusieurs orders à la requête.
306

307
        @param argv: Liste des ordres à ajouter
308
        """
309
        
310
        # On vérifie qu'il n'y a pas de doublons dans la liste finale
311
        # des ordres.
312
        
313
        for i in argv:
314
            for j in self.orderby:
315
                if str(i) == str(j):
316
                    break
317
            self.orderby.append(i)
318

    
319
    def format_events_status(self, event):
320
        """
321
        Suivant l'état de l'événement, retourne la classe à appliquer
322
        à l'image indiquant si l'événement est pris en compte ou non,
323
        ainsi qu'un texte indiquant l'état.
324

325
        @param event: l'événement à analyser
326

327
        @return: Dictionnaire représentant la classe à appliquer
328
            et l'état (sous une forme intelligible).
329
        """
330

    
331
        if event.status == 'AAClosed':
332
            return {
333
                'src': url('/images/crossed.png'),
334
                'label': _('Closed'),
335
            }
336
        elif event.status == 'Acknowledged':
337
            return {
338
                'src': url('/images/checked.png'),
339
                'label': _('Acknowledged'),
340
            }
341
        else:
342
            return None
343

    
344
    def format_events(self, first_row, last_row):
345
        """
346
        Formate la réponse de la requête et y applique les plugins
347
        pour un affichage simple du résultat par Genshi.
348
        On génère une liste de liste, chaqu'une étant la description de
349
        l'affichage pour un événement donné.
350

351
        @param first_row: Indice de début de la liste des événements
352
        @param last_row: Indice de fin de la liste des événements
353
        """
354
        
355
        # Si la requête n'est pas générée, on le fait
356
        self.generate_request()
357

    
358
        # Liste des éléments pour la tête du tableau
359

    
360
        lst_title = [
361
                ['',{}],
362
                [_('Date')+ '<span style="font-weight:normal">' + \
363
                        '<br />['+_('Duration') + ']</span>',
364
                        {'style':'text-align:left'}],
365
                [_('Priority'), {'title':_('ITIL Priority')}],
366
                ['#', {'title':_('Occurrence count')}],
367
                [_('Host'), {'style':'text-align:left'}],
368
                [_('Service Type')+'<br />'+_('Service Name'),
369
                    {'style':'text-align:left'}], 
370
                [_('Output'), {'style':'text-align:left'}]
371
                ]
372
        lst_title.extend([[plug.name, plug.style] for plug in self.plugin])
373
        lst_title.extend([['[' + _('TT') + ']', {'title': _('Trouble Ticket')}],
374
                            ['', {}]])
375
        events = [lst_title]
376
        i = 0
377
        class_tr = ['odd', 'even']
378
        ids = []
379
        for req in self.req[first_row : last_row]:
380
            # Si il y a plus d'un élément dans la liste des tables,
381
            # req devient une liste plutôt que d'être directement la
382
            # table souhaitée
383

    
384
            if isinstance(req, CorrEvent):
385
                event = req
386
            else:
387
                event = req[0]
388
                hostname = req.hostname
389
                servicename = req.servicename
390
            ids.append(event.idcause)
391

    
392
            # La liste pour l'événement actuel comporte dans l'ordre :
393
            #   L'événement en lui-même
394
            #   La classe à appliquer sur la ligne (permet d'alterner les
395
            #       couleurs suivant les lignes)
396
            #   La classe pour la case comportant la flèche de détails
397
            #   La classe pour la date, l'occurence et l'édition
398
            #   L'image à afficher pour la flèche de détails
399
            #   Une liste (une case par plugin) de ce que le plugin souhaite
400
            #       afficher en fonction de l'événement
401

    
402
            cause = event.cause
403

    
404
            events.append([
405
                    event,
406
                    hostname,
407
                    servicename,
408
                    {'class': class_tr[i % 2]},
409
                    {'class': StateName.value_to_statename(
410
                        cause.initial_state) +
411
                        self.class_ack[event.status]},
412
                    {'class': StateName.value_to_statename(
413
                        cause.current_state) +
414
                        self.class_ack[event.status]},
415
                    {'src': '/images/%s2.png' %
416
                        StateName.value_to_statename(
417
                        cause.current_state)},
418
                    self.format_events_status(event),
419
                    [[j.__show__(event), j.style] for j in self.plugin]
420
                ])
421
            i += 1
422

    
423
        # On sauvegarde la liste précédemment créée puis on remplit
424
        # le TmplContext
425
        self.events = events
426
        self.idevents = ids
427

    
428
    def format_history(self):
429
        """
430
        Formate les historiques correspondant aux événements sélectionnés
431
        pour un affichage simple du résultat par Genshi.
432
        On génère une liste de liste, chaqu'une étant la description
433
        de l'affichage pour un historique donné.
434
        """
435

    
436
        history = DBSession.query(
437
                    EventHistory,
438
                ).filter(EventHistory.idevent.in_(self.idevents)
439
                ).order_by(desc(EventHistory.timestamp)
440
                ).order_by(desc(EventHistory.idhistory))
441
        if history.count() == 0:
442
            self.hist = {}
443
            for i in self.idevents:
444
                self.hist[i] = []
445
            return
446

    
447
        hists = {}
448
        i = 0
449
        class_tr = ['odd', 'even']
450

    
451
        for hist in history:
452
            if not hist.idevent in hists:
453
                hists[hist.idevent] = []
454

    
455
            # La liste pour l'historique actuel comporte dans l'ordre :
456
            #   Le moment où il a été généré
457
            #   Qui l'a généré
458
            #   Le type d'action qui a été appliqué
459
            #   La valeur de l'action
460
            #   Le détail de l'action
461
            #   La classe à appliquer à la ligne
462
            #       (permet d'alterner les couleurs)
463

    
464
            hists[hist.idevent].append([
465
                hist.timestamp,
466
                hist.username,
467
                hist.type_action,
468
                hist.value,
469
                hist.text,
470
                {'class': class_tr[i % 2]},
471
            ])
472
            i += 1
473
        
474
        self.hist = hists
475

    
476
    def generate_tmpl_context(self):
477
        """
478
        Génère et peuple la variable tmpl_context avec les Dialogs et
479
        formulaires nécessaire au fonctionnement de Vigiboard
480
        """
481

    
482
        from vigiboard.controllers.root import get_last_modification_timestamp
483
        
484
        # Dialogue d'édition
485
        tmpl_context.edit_event_form = EditEventForm('edit_event_form',
486
            last_modification=mktime(get_last_modification_timestamp(
487
                self.idevents).timetuple()),
488
            action=url('/update'), 
489
        )
490

    
491
        # Dialogue de recherche
492
        tmpl_context.search_form = SearchForm('search_form', lang=self.lang,
493
                                        # TRANSLATORS: Format de date et heure.
494
                                        date_format=_('%Y-%m-%d %I:%M:%S %p'))
495
        
496
        # Dialogue de détail d'un événement
497

    
498
        # Exécution des contexts des plugins
499
        for j in self.plugin:
500
            j.context(self.context_fct)
501