Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

vigiboard / vigiboard / controllers / vigiboardrequest.py @ 011743be

History | View | Annotate | Download (15.7 KB)

1
# -*- coding: utf-8 -*-
2
# vim:set expandtab tabstop=4 shiftwidth=4:
3
# Copyright (C) 2007-2020 CS GROUP - France
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 tg.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
                Host.address.label("address"),
98
            ).join(
99
                (Host, Host.idhost == LowLevelService.idhost),
100
            ).distinct()
101

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

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

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

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

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

    
149
            # Permet d'avoir le même format que pour l'autre requête.
150
            self.items = items.subquery()
151

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

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

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

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

    
201

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

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

    
230
        for plugin in config['columns_plugins']:
231
            self.add_plugin(plugin)
232

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

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

    
244
        # query et join ont besoin de referrence
245
        self.req = self.req.query(*self.table)
246
        self.req = self.req.join(*self.join)
247
        self.req = self.req.options(*self.option)
248

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

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

265
        @return: Nombre de ligne
266
        """
267

    
268
        self.generate_request()
269
        return self.req.count()
270

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

276
        @param argv: Liste des tables à ajouter
277
        """
278

    
279
        # On vérifie qu'il n'y a pas de doublons dans la liste finale
280
        # des tables.
281

    
282
        for i in argv :
283
            for j in self.table:
284
                if str(i) == str(j):
285
                    break
286
            self.table.append(i)
287

    
288
    def add_join(self, *argv):
289
        """
290
        Ajoute une ou plusieurs jointures à
291
        la requête.
292

293
        @param argv: Liste des jointures à ajouter
294
        """
295

    
296
        # On vérifie qu'il n'y a pas de doublons dans la liste finale
297
        # des jointures.
298

    
299
        for i in argv:
300
            for j in self.join:
301
                if str(i) == str(j):
302
                    break
303
            self.join.append(i)
304

    
305
    def add_option(self, *argv):
306
        """
307
        Ajoute une ou plusieurs options à la requête.
308

309
        @param argv: Liste des options à ajouter
310
        """
311

    
312
        # On vérifie qu'il n'y a pas de doublons
313
        # dans la liste finale des options.
314

    
315
        for i in argv:
316
            for j in self.option:
317
                if str(i) == str(j):
318
                    break
319
            self.option.append(i)
320

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

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

333
        @param argv: Liste des jointures externes à ajouter
334
        """
335

    
336
        # On vérifie qu'il n'y a pas de doublons dans la liste finale
337
        # des jointures externes.
338

    
339
        for i in argv:
340
            for j in self.outerjoin:
341
                if str(i) == str(j):
342
                    break
343
            self.outerjoin.append(i)
344

    
345
    def add_filter(self, *argv):
346
        """
347
        Ajoute un ou plusieurs filtres à la requête.
348

349
        @param argv: Liste des filtres à ajouter
350
        """
351

    
352
        # On vérifie qu'il n'y a pas de doublons dans la liste finale
353
        # des filtres.
354

    
355
        for i in argv:
356
            for j in self.filter:
357
                if str(i) == str(j):
358
                    break
359
            self.filter.append(i)
360

    
361
    def add_group_by(self, *argv):
362
        """
363
        Ajoute un ou plusieurs groupements à la requête.
364

365
        @param argv: Liste des groupements à ajouter
366
        """
367

    
368
        # On vérifie qu'il n'y a pas de doublons dans la liste finale
369
        # des groupements.
370

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

    
382
    def add_order_by(self, *argv):
383
        """
384
        Ajoute un ou plusieurs orders à la requête.
385

386
        @param argv: Liste des ordres à ajouter
387
        """
388

    
389
        # On vérifie qu'il n'y a pas de doublons dans la liste finale
390
        # des ordres.
391

    
392
        for i in argv:
393
            for j in self.orderby:
394
                if str(i) == str(j):
395
                    break
396
            self.orderby.append(i)
397

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

405
        @param first_row: Indice de début de la liste des événements
406
        @param last_row: Indice de fin de la liste des événements
407
        """
408

    
409
        # Si la requête n'est pas générée, on le fait
410
        self.generate_request()
411

    
412
        # Liste des éléments pour la tête du tableau
413
        self.events = []
414

    
415
        for data in self.req[first_row : last_row]:
416
            self.events.append(data)
417

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

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

    
434
    def generate_tmpl_context(self):
435
        """
436
        Génère et peuple la variable tmpl_context avec les dialogues et
437
        formulaires nécessaire au fonctionnement de Vigiboard
438
        """
439

    
440
        from vigiboard.controllers.root import get_last_modification_timestamp
441

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

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

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