Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

vigiboard / vigiboard / controllers / vigiboardrequest.py @ 9b8d9497

History | View | Annotate | Download (15.6 KB)

1
# -*- coding: utf-8 -*-
2
# vim:set expandtab tabstop=4 shiftwidth=4:
3
# Copyright (C) 2007-2016 CS-SI
4
# License: GNU GPL v2 <http://www.gnu.org/licenses/gpl-2.0.html>
5

    
6
"""Gestion de la requête, des plugins et de l'affichage du Vigiboard"""
7

    
8
import logging
9
from time import mktime
10

    
11
from tg import config, tmpl_context, request, url
12
from pylons.i18n import ugettext as _
13
from paste.deploy.converters import aslist
14

    
15
from sqlalchemy import not_, and_, asc, desc
16
from sqlalchemy.sql.expression import null as expr_null, union_all
17
from sqlalchemy.orm import contains_eager
18

    
19
from vigilo.models.session import DBSession
20
from vigilo.models.tables import Event, CorrEvent, EventHistory, \
21
    Host, LowLevelService, StateName, UserSupItem
22
from vigiboard.widgets.edit_event import EditEventForm
23
from vigiboard.controllers.plugins import VigiboardRequestPlugin, INNER, ITEMS
24

    
25
LOGGER = logging.getLogger(__name__)
26

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

    
33
    def __init__(self, user, mask_closed_events=True, search=None, sort=None, order=None):
34
        """
35
        Initialisation de l'objet qui effectue les requêtes de VigiBoard
36
        sur la base de données.
37
        Cet objet est responsable de la vérification des droits de
38
        l'utilisateur sur les données manipulées.
39

40
        @param user: Nom de l'utilisateur cherchant à afficher les événements.
41
        @type  user: C{str}
42
        @param mask_closed_events: Booléen indiquant si l'on souhaite masquer les
43
            événements fermés ou non.
44
        @type  mask_closed_events: C{boolean}
45
        @param search: Dictionnaire contenant les critères de recherche.
46
        @type  search: C{dict}
47
        @param sort: Colonne de tri; vide en l'absence de tri.
48
        @type  sort: C{unicode}
49
        @param order: Ordre du tri ("asc" ou "desc"); vide en l'absence de tri.
50
        @type  order: C{unicode}
51

52
        """
53

    
54
        # Permet s'appliquer des filtres de recherche aux sous-requêtes.
55
        self.subqueries = []
56
        self.generaterq = False
57

    
58
        # Éléments à retourner (SELECT ...)
59
        self.table = []
60

    
61
        # Tables sur lesquelles porte la récupération (JOIN)
62
        self.join = []
63

    
64
        # Options à ajouter la requête
65
        self.option = []
66

    
67
        # Tables sur lesquelles porte la récupération (OUTER JOIN)
68
        self.outerjoin = []
69

    
70
        # Critères de filtrage (WHERE)
71
        self.filter = []
72

    
73
        # Regroupements (GROUP BY)
74
        # PostgreSQL est pointilleux sur les colonnes qui apparaissent
75
        # dans la clause GROUP BY. Si une colonne apparaît dans ORDER BY,
76
        # elle doit systématiquement apparaître AUSSI dans GROUP BY.
77
        self.groupby = [
78
            StateName.order,
79
            Event.timestamp,
80
            CorrEvent.ack,
81
            CorrEvent.priority,
82
            StateName.statename,
83
        ]
84

    
85
        self.req = DBSession
86
        self.plugin = []
87
        self.events = []
88

    
89
        # Si l'utilisateur est privilégié, il a accès
90
        # à tous les hôtes/services sans restriction.
91
        if config.is_manager.is_met(request.environ):
92
            # Sélection de tous les services de la BDD.
93
            lls_query = DBSession.query(
94
                LowLevelService.idservice.label("idsupitem"),
95
                LowLevelService.servicename.label("servicename"),
96
                Host.name.label("hostname"),
97
            ).join(
98
                (Host, Host.idhost == LowLevelService.idhost),
99
            ).distinct()
100

    
101
            # Sélection de tous les hôtes de la BDD.
102
            host_query = DBSession.query(
103
                Host.idhost.label("idsupitem"),
104
                expr_null().label("servicename"),
105
                Host.name.label("hostname"),
106
            ).distinct()
107

    
108
            # Application des filtres des plugins si nécessaire.
109
            if search is not None:
110
                # On tire ici partie du fait que les listes sont passées
111
                # par référence dans les fonctions.
112
                subqueries = [lls_query, host_query]
113
                for _plugin, instance in config.get('columns_plugins', []):
114
                    instance.handle_search_fields(
115
                        self, search, INNER, subqueries)
116
                lls_query = subqueries[0]
117
                host_query = subqueries[1]
118

    
119
            # Union des deux sélections précédentes
120
            self.items = union_all(
121
                lls_query,
122
                host_query,
123
                correlate=False
124
            ).alias()
125

    
126
        # Sinon, on ne récupère que les hôtes/services auquel il a accès.
127
        else:
128
            items = DBSession.query(
129
                UserSupItem.idsupitem,
130
                UserSupItem.servicename,
131
                UserSupItem.hostname,
132
            ).filter(
133
                UserSupItem.username == user.user_name
134
            ).distinct()
135

    
136
            # Application des filtres des plugins si nécessaire.
137
            if search is not None:
138
                # On tire ici partie du fait que les listes sont passées
139
                # par référence dans les fonctions.
140
                subqueries = [items]
141
                for _plugin, instance in config.get('columns_plugins', []):
142
                    instance.handle_search_fields(
143
                        self, search, INNER, subqueries)
144
                items = subqueries[0]
145

    
146
            # Permet d'avoir le même format que pour l'autre requête.
147
            self.items = items.subquery()
148

    
149
        # Tris (ORDER BY)
150
        # Permet de répondre aux exigences suivantes :
151
        # - VIGILO_EXIG_VIGILO_BAC_0050
152
        # - VIGILO_EXIG_VIGILO_BAC_0060
153
        self.orderby = []
154
        plugins = config.get('columns_plugins', [])
155
        if sort:
156
            for _plugin, instance in plugins:
157
                criterion = instance.get_sort_criterion(self, sort)
158
                if criterion is not None:
159
                    if order == 'asc':
160
                        self.orderby.append(asc(criterion))
161
                    else:
162
                        self.orderby.append(desc(criterion))
163

    
164
        default_sort = aslist(config.get('default_sort', ''))
165
        for sort_column in default_sort:
166
            criterion = None
167
            sort_field, _dummy, sort_order = sort_column.partition(':')
168
            for _plugin, instance in plugins:
169
                criterion = instance.get_sort_criterion(self, sort_field)
170
                if criterion is not None:
171
                    if sort_order == 'desc':
172
                        self.orderby.append(desc(criterion))
173
                    elif sort_order in ('asc', None):
174
                        self.orderby.append(asc(criterion))
175
                    else:
176
                        self.orderby.append(asc(criterion))
177
                        LOGGER.warn('Invalid sort order: "%s", sorting in '
178
                                    'ascending order instead', sort_order)
179
                    break
180
            if criterion is None:
181
                LOGGER.info('No such plugin: "%s"', sort_field)
182

    
183
        if search is not None:
184
            # 2nde passe pour les filtres : self.items est désormais défini.
185
            for _plugin, instance in plugins:
186
                instance.handle_search_fields(self, search, ITEMS, subqueries)
187

    
188
        if mask_closed_events:
189
            self.filter.append(
190
                # On masque les événements avec l'état OK
191
                # et traités (ack == CorrEvent.ACK_CLOSED).
192
                not_(and_(
193
                    StateName.statename.in_([u'OK', u'UP']),
194
                    CorrEvent.ack == CorrEvent.ACK_CLOSED
195
                ))
196
            )
197

    
198

    
199
    def add_plugin(self, *argv):
200
        """
201
        Ajout d'un plugin, on lui prélève ses ajouts dans la requête
202
        """
203
        for i in argv:
204
            if isinstance(i, VigiboardRequestPlugin):
205
                if i.table:
206
                    self.add_table(*i.table)
207
                if i.join:
208
                    self.add_join(*i.join)
209
                if i.outerjoin:
210
                    self.add_outer_join(*i.outerjoin)
211
                if i.filter:
212
                    self.add_filter(*i.filter)
213
                if i.groupby:
214
                    self.add_group_by(*i.groupby)
215
                if i.orderby:
216
                    self.add_order_by(*i.orderby)
217
                self.plugin.append(i)
218

    
219
    def generate_request(self):
220
        """
221
        Génération de la requête avec l'ensemble des données stockées
222
        et la place dans la variable rq de la classe
223
        """
224
        if self.generaterq:
225
            return
226

    
227
        for plugin in config['columns_plugins']:
228
            self.add_plugin(plugin)
229

    
230
        # Toutes les requêtes ont besoin de récupérer l'état courant
231
        # de l'événement.
232
        self.join.append((StateName, StateName.idstatename == \
233
                                        Event.current_state))
234

    
235
        # PostgreSQL est pointilleux sur les colonnes qui apparaissent
236
        # dans la clause GROUP BY. Si une colonne apparaît dans SELECT,
237
        # elle doit systématiquement apparaître AUSSI dans GROUP BY.
238
        # Ici, on ajoute automatiquement les colonnes du SELECT au GROUP BY.
239
        self.add_group_by(*self.table)
240

    
241
        # query et join ont besoin de referrence
242
        self.req = self.req.query(*self.table)
243
        self.req = self.req.join(*self.join)
244
        self.req = self.req.options(*self.option)
245

    
246
        # le reste, non
247
        for i in self.outerjoin:
248
            self.req = self.req.outerjoin(i)
249
        for i in self.filter:
250
            self.req = self.req.filter(i)
251
        for i in self.groupby:
252
            self.req = self.req.group_by(i)
253
        for i in self.orderby:
254
            self.req = self.req.order_by(i)
255
        self.generaterq = True
256

    
257
    def num_rows(self):
258
        """
259
        Retourne le nombre de lignes de la requête.
260
        Si celle-ci n'est pas encore générée, on le fait.
261

262
        @return: Nombre de ligne
263
        """
264

    
265
        self.generate_request()
266
        return self.req.count()
267

    
268
    def add_table(self, *argv):
269
        """
270
        Ajoute une ou plusieurs tables/élément d'une table à
271
        la requête.
272

273
        @param argv: Liste des tables à ajouter
274
        """
275

    
276
        # On vérifie qu'il n'y a pas de doublons dans la liste finale
277
        # des tables.
278

    
279
        for i in argv :
280
            for j in self.table:
281
                if str(i) == str(j):
282
                    break
283
            self.table.append(i)
284

    
285
    def add_join(self, *argv):
286
        """
287
        Ajoute une ou plusieurs jointures à
288
        la requête.
289

290
        @param argv: Liste des jointures à ajouter
291
        """
292

    
293
        # On vérifie qu'il n'y a pas de doublons dans la liste finale
294
        # des jointures.
295

    
296
        for i in argv:
297
            for j in self.join:
298
                if str(i) == str(j):
299
                    break
300
            self.join.append(i)
301

    
302
    def add_option(self, *argv):
303
        """
304
        Ajoute une ou plusieurs options à la requête.
305

306
        @param argv: Liste des options à ajouter
307
        """
308

    
309
        # On vérifie qu'il n'y a pas de doublons
310
        # dans la liste finale des options.
311

    
312
        for i in argv:
313
            for j in self.option:
314
                if str(i) == str(j):
315
                    break
316
            self.option.append(i)
317

    
318
    def add_contains_eager(self, relation):
319
        """
320
        Ajoute une option de type contains_eager à la
321
        requête pour la relation passée en paramètre.
322
        """
323
        self.add_option(contains_eager(relation))
324

    
325
    def add_outer_join(self, *argv):
326
        """
327
        Ajoute une ou plusieurs jointures externes à
328
        la requête.
329

330
        @param argv: Liste des jointures externes à ajouter
331
        """
332

    
333
        # On vérifie qu'il n'y a pas de doublons dans la liste finale
334
        # des jointures externes.
335

    
336
        for i in argv:
337
            for j in self.outerjoin:
338
                if str(i) == str(j):
339
                    break
340
            self.outerjoin.append(i)
341

    
342
    def add_filter(self, *argv):
343
        """
344
        Ajoute un ou plusieurs filtres à la requête.
345

346
        @param argv: Liste des filtres à ajouter
347
        """
348

    
349
        # On vérifie qu'il n'y a pas de doublons dans la liste finale
350
        # des filtres.
351

    
352
        for i in argv:
353
            for j in self.filter:
354
                if str(i) == str(j):
355
                    break
356
            self.filter.append(i)
357

    
358
    def add_group_by(self, *argv):
359
        """
360
        Ajoute un ou plusieurs groupements à la requête.
361

362
        @param argv: Liste des groupements à ajouter
363
        """
364

    
365
        # On vérifie qu'il n'y a pas de doublons dans la liste finale
366
        # des groupements.
367

    
368
        for i in argv:
369
            for j in self.groupby:
370
                try:
371
                    if str(i) == str(j):
372
                        break
373
                # SQLAlchemy lève cette exception pour certains attributes,
374
                # par exemple les attributs définis avec synonym().
375
                except AttributeError:
376
                    pass
377
            self.groupby.append(i)
378

    
379
    def add_order_by(self, *argv):
380
        """
381
        Ajoute un ou plusieurs orders à la requête.
382

383
        @param argv: Liste des ordres à ajouter
384
        """
385

    
386
        # On vérifie qu'il n'y a pas de doublons dans la liste finale
387
        # des ordres.
388

    
389
        for i in argv:
390
            for j in self.orderby:
391
                if str(i) == str(j):
392
                    break
393
            self.orderby.append(i)
394

    
395
    def format_events(self, first_row, last_row):
396
        """
397
        Formate la réponse de la requête et y applique les plugins
398
        pour un affichage simple du résultat par Genshi.
399
        On génère une liste de liste, chaqu'une étant la description de
400
        l'affichage pour un événement donné.
401

402
        @param first_row: Indice de début de la liste des événements
403
        @param last_row: Indice de fin de la liste des événements
404
        """
405

    
406
        # Si la requête n'est pas générée, on le fait
407
        self.generate_request()
408

    
409
        # Liste des éléments pour la tête du tableau
410
        self.events = []
411

    
412
        for data in self.req[first_row : last_row]:
413
            self.events.append(data)
414

    
415
    def format_history(self):
416
        """
417
        Formate les historiques correspondant aux événements sélectionnés
418
        pour un affichage simple du résultat par Genshi.
419
        On génère une liste de liste, chaqu'une étant la description
420
        de l'affichage pour un historique donné.
421
        """
422

    
423
        ids = [data[0].idevent for data in self.events]
424
        history = DBSession.query(
425
                    EventHistory,
426
                ).filter(EventHistory.idevent.in_(ids)
427
                ).order_by(desc(EventHistory.timestamp)
428
                ).order_by(desc(EventHistory.idhistory))
429
        return history
430

    
431
    def generate_tmpl_context(self):
432
        """
433
        Génère et peuple la variable tmpl_context avec les Dialogs et
434
        formulaires nécessaire au fonctionnement de Vigiboard
435
        """
436

    
437
        from vigiboard.controllers.root import get_last_modification_timestamp
438

    
439
        # Si les objets manipulés sont des Event, on a facilement les idevent.
440
        if not len(self.events):
441
            ids = []
442
        elif isinstance(self.events[0][0], Event):
443
            ids = [data[0].idevent for data in self.events]
444
        # Sinon, il s'agit de CorrEvent(s) dont on récupère l'idcause.
445
        else:
446
            ids = [data[0].idcause for data in self.events]
447

    
448
        # Ajout des formulaires et préparation
449
        # des données pour ces formulaires.
450
        tmpl_context.last_modification = \
451
            mktime(get_last_modification_timestamp(ids).timetuple())
452

    
453
        tmpl_context.edit_event_form = EditEventForm("edit_event_form",
454
            submit_text=_('Apply'), action=url('/update'))