Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (17.4 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
            asc(self.items.c.hostname),
130
        ]
131

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

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

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

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

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

    
185
#        temp_join = self.join
186
#        self.join =  [(self.items, Event.idsupitem == self.items.c.idsupitem),
187
#            (StateName, StateName.idstatename == Event.current_state),]
188
#        self.join.extend(temp_join)
189

    
190
#        self.join.extend(
191
#            [(self.items, Event.idsupitem == self.items.c.idsupitem),
192
#            (StateName, StateName.idstatename == Event.current_state),])
193
        self.join.append((StateName, StateName.idstatename == Event.current_state))
194
        self.add_group_by(*self.table)
195

    
196
        # query et join ont besoin de referrence
197
        self.req = self.req.query(*self.table)
198
        self.req = self.req.join(*self.join)
199

    
200
        # le reste, non
201
        for i in self.outerjoin:
202
            self.req = self.req.outerjoin(i)
203
        for i in self.filter:
204
            self.req = self.req.filter(i)
205
        for i in self.groupby:
206
            self.req = self.req.group_by(i)
207
        for i in self.orderby:
208
            self.req = self.req.order_by(i)
209

    
210
        self.generaterq = True
211

    
212
    def num_rows(self):
213
        """
214
        Retourne le nombre de lignes de la requête.
215
        Si celle-ci n'est pas encore générée, on le fait.
216

217
        @return: Nombre de ligne
218
        """
219

    
220
        self.generate_request()
221
        return self.req.count()
222

    
223
    def add_table(self, *argv):
224
        """
225
        Ajoute une ou plusieurs tables/élément d'une table à
226
        la requête.
227

228
        @param argv: Liste des tables à ajouter
229
        """
230
        
231
        # On vérifie qu'il n'y a pas de doublons dans la liste finale
232
        # des tables.
233
        
234
        for i in argv :
235
            for j in self.table:
236
                if str(i) == str(j):
237
                    break
238
            self.table.append(i)
239

    
240
    def add_join(self, *argv):
241
        """
242
        Ajoute une ou plusieurs jointures à
243
        la requête.
244

245
        @param argv: Liste des jointures à ajouter
246
        """
247
        
248
        # On vérifie qu'il n'y a pas de doublons dans la liste finale
249
        # des jointures.
250
        
251
        for i in argv:
252
            for j in self.join:
253
                if str(i) == str(j):
254
                    break
255
            self.join.append(i)
256

    
257
    def add_outer_join(self, *argv):
258
        """
259
        Ajoute une ou plusieurs jointures externes à
260
        la requête.
261

262
        @param argv: Liste des jointures externes à ajouter
263
        """
264
        
265
        # On vérifie qu'il n'y a pas de doublons dans la liste finale
266
        # des jointures externes.
267
        
268
        for i in argv:
269
            for j in self.outerjoin:
270
                if str(i) == str(j):
271
                    break
272
            self.outerjoin.append(i)    
273

    
274
    def add_filter(self, *argv):
275
        """
276
        Ajoute un ou plusieurs filtres à la requête.
277

278
        @param argv: Liste des filtres à ajouter
279
        """
280
        
281
        # On vérifie qu'il n'y a pas de doublons dans la liste finale
282
        # des filtres.
283
        
284
        for i in argv:
285
            for j in self.filter:
286
                if str(i) == str(j):
287
                    break
288
            self.filter.append(i)
289

    
290
    def add_group_by(self, *argv):
291
        """
292
        Ajoute un ou plusieurs groupements à la requête.
293

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

    
311
    def add_order_by(self, *argv):
312
        """
313
        Ajoute un ou plusieurs orders à la requête.
314

315
        @param argv: Liste des ordres à ajouter
316
        """
317
        
318
        # On vérifie qu'il n'y a pas de doublons dans la liste finale
319
        # des ordres.
320
        
321
        for i in argv:
322
            for j in self.orderby:
323
                if str(i) == str(j):
324
                    break
325
            self.orderby.append(i)
326

    
327
    def format_events_status(self, event):
328
        """
329
        Suivant l'état de l'événement, retourne la classe à appliquer
330
        à l'image indiquant si l'événement est pris en compte ou non,
331
        ainsi qu'un texte indiquant l'état.
332

333
        @param event: l'événement à analyser
334

335
        @return: Dictionnaire représentant la classe à appliquer
336
            et l'état (sous une forme intelligible).
337
        """
338

    
339
        if event.status == 'AAClosed':
340
            return {
341
                'src': url('/images/crossed.png'),
342
                'label': _('Closed'),
343
            }
344
        elif event.status == 'Acknowledged':
345
            return {
346
                'src': url('/images/checked.png'),
347
                'label': _('Acknowledged'),
348
            }
349
        else:
350
            return None
351

    
352
    def format_events(self, first_row, last_row):
353
        """
354
        Formate la réponse de la requête et y applique les plugins
355
        pour un affichage simple du résultat par Genshi.
356
        On génère une liste de liste, chaqu'une étant la description de
357
        l'affichage pour un événement donné.
358

359
        @param first_row: Indice de début de la liste des événements
360
        @param last_row: Indice de fin de la liste des événements
361
        """
362
        
363
        # Si la requête n'est pas générée, on le fait
364
        self.generate_request()
365

    
366
        # Liste des éléments pour la tête du tableau
367

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

    
392
            if isinstance(req, CorrEvent):
393
                event = req
394
            else:
395
                event = req[0]
396
                hostname = req.hostname
397
                servicename = req.servicename
398
            ids.append(event.idcause)
399

    
400
            # La liste pour l'événement actuel comporte dans l'ordre :
401
            #   L'événement en lui-même
402
            #   La classe à appliquer sur la ligne (permet d'alterner les
403
            #       couleurs suivant les lignes)
404
            #   La classe pour la case comportant la flèche de détails
405
            #   La classe pour la date, l'occurence et l'édition
406
            #   L'image à afficher pour la flèche de détails
407
            #   Une liste (une case par plugin) de ce que le plugin souhaite
408
            #       afficher en fonction de l'événement
409

    
410
            cause = event.cause
411

    
412
            events.append([
413
                    event,
414
                    hostname,
415
                    servicename,
416
                    {'class': class_tr[i % 2]},
417
                    {'class': StateName.value_to_statename(
418
                        cause.initial_state) +
419
                        self.class_ack[event.status]},
420
                    {'class': StateName.value_to_statename(
421
                        cause.current_state) +
422
                        self.class_ack[event.status]},
423
                    {'src': '/images/%s2.png' %
424
                        StateName.value_to_statename(
425
                        cause.current_state)},
426
                    self.format_events_status(event),
427
                    [[j.__show__(event), j.style] for j in self.plugin]
428
                ])
429
            i += 1
430

    
431
        # On sauvegarde la liste précédemment créée puis on remplit
432
        # le TmplContext
433
        self.events = events
434
        self.idevents = ids
435

    
436
    def format_history(self):
437
        """
438
        Formate les historiques correspondant aux événements sélectionnés
439
        pour un affichage simple du résultat par Genshi.
440
        On génère une liste de liste, chaqu'une étant la description
441
        de l'affichage pour un historique donné.
442
        """
443

    
444
        history = DBSession.query(
445
                    EventHistory,
446
                ).filter(EventHistory.idevent.in_(self.idevents)
447
                ).order_by(desc(EventHistory.timestamp)
448
                ).order_by(desc(EventHistory.idhistory))
449
        if history.count() == 0:
450
            self.hist = {}
451
            for i in self.idevents:
452
                self.hist[i] = []
453
            return
454

    
455
        hists = {}
456
        i = 0
457
        class_tr = ['odd', 'even']
458

    
459
        for hist in history:
460
            if not hist.idevent in hists:
461
                hists[hist.idevent] = []
462

    
463
            # La liste pour l'historique actuel comporte dans l'ordre :
464
            #   Le moment où il a été généré
465
            #   Qui l'a généré
466
            #   Le type d'action qui a été appliqué
467
            #   La valeur de l'action
468
            #   Le détail de l'action
469
            #   La classe à appliquer à la ligne
470
            #       (permet d'alterner les couleurs)
471

    
472
            hists[hist.idevent].append([
473
                hist.timestamp,
474
                hist.username,
475
                hist.type_action,
476
                hist.value,
477
                hist.text,
478
                {'class': class_tr[i % 2]},
479
            ])
480
            i += 1
481
        
482
        self.hist = hists
483

    
484
    def generate_tmpl_context(self):
485
        """
486
        Génère et peuple la variable tmpl_context avec les Dialogs et
487
        formulaires nécessaire au fonctionnement de Vigiboard
488
        """
489

    
490
        from vigiboard.controllers.root import get_last_modification_timestamp
491
        
492
        # Dialogue d'édition
493
        tmpl_context.edit_event_form = EditEventForm('edit_event_form',
494
            last_modification=mktime(get_last_modification_timestamp(
495
                self.idevents).timetuple()),
496
            action=url('/update'), 
497
        )
498

    
499
        # Dialogue de recherche
500
        tmpl_context.search_form = SearchForm('search_form', lang=self.lang,
501
                                        # TRANSLATORS: Format de date et heure.
502
                                        date_format=_('%Y-%m-%d %I:%M:%S %p'))
503
        
504
        # Dialogue de détail d'un événement
505

    
506
        # Exécution des contexts des plugins
507
        for j in self.plugin:
508
            j.context(self.context_fct)
509