Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

vigiboard / vigiboard / controllers / vigiboardrequest.py @ 8d647d93

History | View | Annotate | Download (16.3 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
"""Gestion de la requête, des plugins et de l'affichage du Vigiboard"""
22

    
23
import logging
24
from time import mktime
25

    
26
from tg import config, tmpl_context, request, url
27
from pylons.i18n import ugettext as _
28
from paste.deploy.converters import aslist
29

    
30
from sqlalchemy import not_, and_, asc, desc
31
from sqlalchemy.sql.expression import null as expr_null, union_all
32
from sqlalchemy.orm import contains_eager
33

    
34
from vigilo.models.session import DBSession
35
from vigilo.models.tables import Event, CorrEvent, EventHistory, \
36
    Host, LowLevelService, StateName, UserSupItem
37
from vigiboard.widgets.edit_event import EditEventForm
38
from vigiboard.controllers.plugins import VigiboardRequestPlugin, INNER, ITEMS
39

    
40
LOGGER = logging.getLogger(__name__)
41

    
42
class VigiboardRequest():
43
    """
44
    Classe gérant la génération de la requête finale,
45
    le préformatage des événements et celui des historiques
46
    """
47

    
48
    def __init__(self, user, mask_closed_events=True, search=None, sort=None, order=None):
49
        """
50
        Initialisation de l'objet qui effectue les requêtes de VigiBoard
51
        sur la base de données.
52
        Cet objet est responsable de la vérification des droits de
53
        l'utilisateur sur les données manipulées.
54

55
        @param user: Nom de l'utilisateur cherchant à afficher les événements.
56
        @type  user: C{str}
57
        @param mask_closed_events: Booléen indiquant si l'on souhaite masquer les
58
            événements fermés ou non.
59
        @type  mask_closed_events: C{boolean}
60
        @param search: Dictionnaire contenant les critères de recherche.
61
        @type  search: C{dict}
62
        @param sort: Colonne de tri; vide en l'absence de tri.
63
        @type  sort: C{unicode}
64
        @param order: Ordre du tri ("asc" ou "desc"); vide en l'absence de tri.
65
        @type  order: C{unicode}
66

67
        """
68

    
69
        # Permet s'appliquer des filtres de recherche aux sous-requêtes.
70
        self.subqueries = []
71
        self.generaterq = False
72

    
73
        # Éléments à retourner (SELECT ...)
74
        self.table = []
75

    
76
        # Tables sur lesquelles porte la récupération (JOIN)
77
        self.join = []
78

    
79
        # Options à ajouter la requête
80
        self.option = []
81

    
82
        # Tables sur lesquelles porte la récupération (OUTER JOIN)
83
        self.outerjoin = []
84

    
85
        # Critères de filtrage (WHERE)
86
        self.filter = []
87

    
88
        # Regroupements (GROUP BY)
89
        # PostgreSQL est pointilleux sur les colonnes qui apparaissent
90
        # dans la clause GROUP BY. Si une colonne apparaît dans ORDER BY,
91
        # elle doit systématiquement apparaître AUSSI dans GROUP BY.
92
        self.groupby = [
93
            StateName.order,
94
            Event.timestamp,
95
            CorrEvent.ack,
96
            CorrEvent.priority,
97
            StateName.statename,
98
        ]
99

    
100
        self.req = DBSession
101
        self.plugin = []
102
        self.events = []
103

    
104
        # Si l'utilisateur est privilégié, il a accès
105
        # à tous les hôtes/services sans restriction.
106
        if config.is_manager.is_met(request.environ):
107
            # Sélection de tous les services de la BDD.
108
            lls_query = DBSession.query(
109
                LowLevelService.idservice.label("idsupitem"),
110
                LowLevelService.servicename.label("servicename"),
111
                Host.name.label("hostname"),
112
            ).join(
113
                (Host, Host.idhost == LowLevelService.idhost),
114
            ).distinct()
115

    
116
            # Sélection de tous les hôtes de la BDD.
117
            host_query = DBSession.query(
118
                Host.idhost.label("idsupitem"),
119
                expr_null().label("servicename"),
120
                Host.name.label("hostname"),
121
            ).distinct()
122

    
123
            # Application des filtres des plugins si nécessaire.
124
            if search is not None:
125
                # On tire ici partie du fait que les listes sont passées
126
                # par référence dans les fonctions.
127
                subqueries = [lls_query, host_query]
128
                for _plugin, instance in config.get('columns_plugins', []):
129
                    instance.handle_search_fields(
130
                        self, search, INNER, subqueries)
131
                lls_query = subqueries[0]
132
                host_query = subqueries[1]
133

    
134
            # Union des deux sélections précédentes
135
            self.items = union_all(
136
                lls_query,
137
                host_query,
138
                correlate=False
139
            ).alias()
140

    
141
        # Sinon, on ne récupère que les hôtes/services auquel il a accès.
142
        else:
143
            items = DBSession.query(
144
                UserSupItem.idsupitem,
145
                UserSupItem.servicename,
146
                UserSupItem.hostname,
147
            ).filter(
148
                UserSupItem.username == user.user_name
149
            ).distinct()
150

    
151
            # Application des filtres des plugins si nécessaire.
152
            if search is not None:
153
                # On tire ici partie du fait que les listes sont passées
154
                # par référence dans les fonctions.
155
                subqueries = [items]
156
                for _plugin, instance in config.get('columns_plugins', []):
157
                    instance.handle_search_fields(
158
                        self, search, INNER, subqueries)
159
                items = subqueries[0]
160

    
161
            # Permet d'avoir le même format que pour l'autre requête.
162
            self.items = items.subquery()
163

    
164
        # Tris (ORDER BY)
165
        # Permet de répondre aux exigences suivantes :
166
        # - VIGILO_EXIG_VIGILO_BAC_0050
167
        # - VIGILO_EXIG_VIGILO_BAC_0060
168
        self.orderby = []
169
        plugins = config.get('columns_plugins', [])
170
        if sort:
171
            for _plugin, instance in plugins:
172
                criterion = instance.get_sort_criterion(self, sort)
173
                if criterion is not None:
174
                    if order == 'asc':
175
                        self.orderby.append(asc(criterion))
176
                    else:
177
                        self.orderby.append(desc(criterion))
178

    
179
        default_sort = aslist(config.get('default_sort', ''))
180
        for sort_column in default_sort:
181
            criterion = None
182
            sort_field, _dummy, sort_order = sort_column.partition(':')
183
            for _plugin, instance in plugins:
184
                criterion = instance.get_sort_criterion(self, sort_field)
185
                if criterion is not None:
186
                    if sort_order == 'desc':
187
                        self.orderby.append(desc(criterion))
188
                    elif sort_order in ('asc', None):
189
                        self.orderby.append(asc(criterion))
190
                    else:
191
                        self.orderby.append(asc(criterion))
192
                        LOGGER.warn('Invalid sort order: "%s", sorting in '
193
                                    'ascending order instead', sort_order)
194
                    break
195
            if criterion is None:
196
                LOGGER.info('No such plugin: "%s"', sort_field)
197

    
198
        if search is not None:
199
            # 2nde passe pour les filtres : self.items est désormais défini.
200
            for _plugin, instance in plugins:
201
                instance.handle_search_fields(self, search, ITEMS, subqueries)
202

    
203
        if mask_closed_events:
204
            self.filter.append(
205
                # On masque les événements avec l'état OK
206
                # et traités (ack == CorrEvent.ACK_CLOSED).
207
                not_(and_(
208
                    StateName.statename.in_([u'OK', u'UP']),
209
                    CorrEvent.ack == CorrEvent.ACK_CLOSED
210
                ))
211
            )
212

    
213

    
214
    def add_plugin(self, *argv):
215
        """
216
        Ajout d'un plugin, on lui prélève ses ajouts dans la requête
217
        """
218
        for i in argv:
219
            if isinstance(i, VigiboardRequestPlugin):
220
                if i.table:
221
                    self.add_table(*i.table)
222
                if i.join:
223
                    self.add_join(*i.join)
224
                if i.outerjoin:
225
                    self.add_outer_join(*i.outerjoin)
226
                if i.filter:
227
                    self.add_filter(*i.filter)
228
                if i.groupby:
229
                    self.add_group_by(*i.groupby)
230
                if i.orderby:
231
                    self.add_order_by(*i.orderby)
232
                self.plugin.append(i)
233

    
234
    def generate_request(self):
235
        """
236
        Génération de la requête avec l'ensemble des données stockées
237
        et la place dans la variable rq de la classe
238
        """
239
        if self.generaterq:
240
            return
241

    
242
        for plugin in config['columns_plugins']:
243
            self.add_plugin(plugin)
244

    
245
        # Toutes les requêtes ont besoin de récupérer l'état courant
246
        # de l'événement.
247
        self.join.append((StateName, StateName.idstatename == \
248
                                        Event.current_state))
249

    
250
        # PostgreSQL est pointilleux sur les colonnes qui apparaissent
251
        # dans la clause GROUP BY. Si une colonne apparaît dans SELECT,
252
        # elle doit systématiquement apparaître AUSSI dans GROUP BY.
253
        # Ici, on ajoute automatiquement les colonnes du SELECT au GROUP BY.
254
        self.add_group_by(*self.table)
255

    
256
        # query et join ont besoin de referrence
257
        self.req = self.req.query(*self.table)
258
        self.req = self.req.join(*self.join)
259
        self.req = self.req.options(*self.option)
260

    
261
        # le reste, non
262
        for i in self.outerjoin:
263
            self.req = self.req.outerjoin(i)
264
        for i in self.filter:
265
            self.req = self.req.filter(i)
266
        for i in self.groupby:
267
            self.req = self.req.group_by(i)
268
        for i in self.orderby:
269
            self.req = self.req.order_by(i)
270
        self.generaterq = True
271

    
272
    def num_rows(self):
273
        """
274
        Retourne le nombre de lignes de la requête.
275
        Si celle-ci n'est pas encore générée, on le fait.
276

277
        @return: Nombre de ligne
278
        """
279

    
280
        self.generate_request()
281
        return self.req.count()
282

    
283
    def add_table(self, *argv):
284
        """
285
        Ajoute une ou plusieurs tables/élément d'une table à
286
        la requête.
287

288
        @param argv: Liste des tables à ajouter
289
        """
290

    
291
        # On vérifie qu'il n'y a pas de doublons dans la liste finale
292
        # des tables.
293

    
294
        for i in argv :
295
            for j in self.table:
296
                if str(i) == str(j):
297
                    break
298
            self.table.append(i)
299

    
300
    def add_join(self, *argv):
301
        """
302
        Ajoute une ou plusieurs jointures à
303
        la requête.
304

305
        @param argv: Liste des jointures à ajouter
306
        """
307

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

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

    
317
    def add_option(self, *argv):
318
        """
319
        Ajoute une ou plusieurs options à la requête.
320

321
        @param argv: Liste des options à ajouter
322
        """
323

    
324
        # On vérifie qu'il n'y a pas de doublons
325
        # dans la liste finale des options.
326

    
327
        for i in argv:
328
            for j in self.option:
329
                if str(i) == str(j):
330
                    break
331
            self.option.append(i)
332

    
333
    def add_contains_eager(self, relation):
334
        """
335
        Ajoute une option de type contains_eager à la
336
        requête pour la relation passée en paramètre.
337
        """
338
        self.add_option(contains_eager(relation))
339

    
340
    def add_outer_join(self, *argv):
341
        """
342
        Ajoute une ou plusieurs jointures externes à
343
        la requête.
344

345
        @param argv: Liste des jointures externes à ajouter
346
        """
347

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

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

    
357
    def add_filter(self, *argv):
358
        """
359
        Ajoute un ou plusieurs filtres à la requête.
360

361
        @param argv: Liste des filtres à ajouter
362
        """
363

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

    
367
        for i in argv:
368
            for j in self.filter:
369
                if str(i) == str(j):
370
                    break
371
            self.filter.append(i)
372

    
373
    def add_group_by(self, *argv):
374
        """
375
        Ajoute un ou plusieurs groupements à la requête.
376

377
        @param argv: Liste des groupements à ajouter
378
        """
379

    
380
        # On vérifie qu'il n'y a pas de doublons dans la liste finale
381
        # des groupements.
382

    
383
        for i in argv:
384
            for j in self.groupby:
385
                try:
386
                    if str(i) == str(j):
387
                        break
388
                # SQLAlchemy lève cette exception pour certains attributes,
389
                # par exemple les attributs définis avec synonym().
390
                except AttributeError:
391
                    pass
392
            self.groupby.append(i)
393

    
394
    def add_order_by(self, *argv):
395
        """
396
        Ajoute un ou plusieurs orders à la requête.
397

398
        @param argv: Liste des ordres à ajouter
399
        """
400

    
401
        # On vérifie qu'il n'y a pas de doublons dans la liste finale
402
        # des ordres.
403

    
404
        for i in argv:
405
            for j in self.orderby:
406
                if str(i) == str(j):
407
                    break
408
            self.orderby.append(i)
409

    
410
    def format_events(self, first_row, last_row):
411
        """
412
        Formate la réponse de la requête et y applique les plugins
413
        pour un affichage simple du résultat par Genshi.
414
        On génère une liste de liste, chaqu'une étant la description de
415
        l'affichage pour un événement donné.
416

417
        @param first_row: Indice de début de la liste des événements
418
        @param last_row: Indice de fin de la liste des événements
419
        """
420

    
421
        # Si la requête n'est pas générée, on le fait
422
        self.generate_request()
423

    
424
        # Liste des éléments pour la tête du tableau
425
        self.events = []
426

    
427
        for data in self.req[first_row : last_row]:
428
            self.events.append(data)
429

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

    
438
        ids = [data[0].idevent for data in self.events]
439
        history = DBSession.query(
440
                    EventHistory,
441
                ).filter(EventHistory.idevent.in_(ids)
442
                ).order_by(desc(EventHistory.timestamp)
443
                ).order_by(desc(EventHistory.idhistory))
444
        return history
445

    
446
    def generate_tmpl_context(self):
447
        """
448
        Génère et peuple la variable tmpl_context avec les Dialogs et
449
        formulaires nécessaire au fonctionnement de Vigiboard
450
        """
451

    
452
        from vigiboard.controllers.root import get_last_modification_timestamp
453

    
454
        # Si les objets manipulés sont des Event, on a facilement les idevent.
455
        if not len(self.events):
456
            ids = []
457
        elif isinstance(self.events[0][0], Event):
458
            ids = [data[0].idevent for data in self.events]
459
        # Sinon, il s'agit de CorrEvent(s) dont on récupère l'idcause.
460
        else:
461
            ids = [data[0].idcause for data in self.events]
462

    
463
        # Ajout des formulaires et préparation
464
        # des données pour ces formulaires.
465
        tmpl_context.last_modification = \
466
            mktime(get_last_modification_timestamp(ids).timetuple())
467

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