Project

General

Profile

Revision a2fa6a5b

IDa2fa6a5bee1bd04132fbf9b7e8ec0b6cea3c5ebb
Parent 172682b4
Child a4ffe87d

Added by Francois POIROTTE over 11 years ago

Export CSV alertes de VigiBoard (#954).

Permet l'export des alertes (événements corrélés) affichés à l'écran au
format CSV. L'export tient compte des critères de recherche
éventuellement appliqués.

Refs: #954.
Change-Id: I3c6639dbef9ca53043d45d8a683f5c77619af23a
Reviewed-on: https://vigilo-dev.si.c-s.fr/review/1017
Tested-by: Build system <>
Reviewed-by: Thomas BURGUIERE <>

View differences:

deployment/settings.ini.in
148 148
; supprime l'affichage des liens vers les cartes.
149 149
max_maps = -1
150 150

  
151
; Caractère de séparation des champs dans
152
; l'export CSV.
153
csv_delimiter_char = ;
154

  
155
; Caractère utilisé pour délimiter les champs
156
; dans l'export CSV.
157
csv_quote_char = "
158
; Le guillemet qui termine ce commentaire
159
; sert uniquement à corriger la coloration
160
; syntaxique dans certains éditeurs. "
161

  
162
; Caractère d'échappement pour les caractères
163
; spéciaux (définis par csv_delimiter_char,
164
; csv_quote_char et csv_escape_char).
165
csv_escape_char = \
166

  
167
; Algorithme pour la délimitation des champs
168
; dans l'export CSV.
169
; Les valeurs possibles sont :
170
; "all" : les champs sont systématiquement délimités.
171
; "minimal" : les champs ne sont délimités que lorsque
172
;             leur interprétation est ambigüe.
173
; "nonnumeric" : seuls les champs contenant des valeurs
174
;                autres que numériques sont délimités.
175
; "none" : les champs ne sont jamais délimités.
176
; La valeur par défaut est "all".
177
csv_quoting = all
178

  
151 179
;
152 180
; 4 - Configuration du proxy Nagios.
153 181
;
development.ini
69 69
; (état d'acquittement et priorité ITIL).
70 70
state_first = True
71 71

  
72
; Caractère de séparation des champs dans
73
; l'export CSV.
74
csv_delimiter_char = ;
75

  
76
; Caractère utilisé pour délimiter les champs
77
; dans l'export CSV.
78
csv_quote_char = "
79
; Le guillemet qui termine ce commentaire
80
; sert uniquement à corriger la coloration
81
; syntaxique dans certains éditeurs. "
82

  
83
; Caractère d'échappement pour les caractères
84
; spéciaux (définis par csv_delimiter_char,
85
; csv_quote_char et csv_escape_char).
86
csv_escape_char = \
87

  
88
; Algorithme pour la délimitation des champs
89
; dans l'export CSV.
90
; Les valeurs possibles sont :
91
; "all" : les champs sont systématiquement délimités.
92
; "minimal" : les champs ne sont délimités que lorsque
93
;             leur interprétation est ambigüe.
94
; "nonnumeric" : seuls les champs contenant des valeurs
95
;                autres que numériques sont délimités.
96
; "none" : les champs ne sont jamais délimités.
97
; La valeur par défaut est "all".
98
csv_quoting = all
99

  
72 100
; Emplacement des applications (vigirrd, Nagios, ...)
73 101
; sur les serveurs distants.
74 102
app_path.nagios = /nagios/
vigiboard/config/app_cfg.py
75 75

  
76 76
base_config = VigiboardConfig('VigiBoard')
77 77
base_config.package = vigiboard
78
base_config.mimetype_lookup = {
79
    '.csv': 'text/csv',
80
}
78 81

  
79 82
##################################
80 83
# Settings specific to Vigiboard #
......
134 137
    'status',
135 138
#    'test',
136 139
)
140

  
141
base_config['csv_columns'] = (
142
    'id',
143
    'state',
144
    'initial_state',
145
    'peak_state',
146
    'date',
147
    'duration',
148
    'priority',
149
    'occurrences',
150
    'hostname',
151
    'servicename',
152
    'output',
153
    'ack',
154
    'trouble_ticket_id',
155
    'trouble_ticket_link',
156
)
vigiboard/controllers/plugins/__init__.py
33 33
    Classe que les plugins de VigiBoard doivent étendre.
34 34
    """
35 35

  
36
    def __init__ (self, table = None, join = None, outerjoin = None,
37
            filters = None, groupby = None, orderby = None, name = '',
38
            style = None, object_name = ""):
39
        self.table = table
40
        self.join = join
41
        self.outerjoin = outerjoin
42
        self.filter = filters
43
        self.orderby = orderby
44
        self.name = name
45
        self.groupby = groupby
46
        self.style = style
47
        self.object_name = object_name
48

  
49 36
    def get_bulk_data(self, events_ids):
50 37
        """
51 38
        Cette méthode est appelée par le L{RootController} : elle
......
91 78
        """
92 79
        return 1
93 80

  
81
    def get_data(self, event):
82
        return {}
83

  
94 84
    def get_search_fields(self):
95 85
        return []
96 86

  
vigiboard/controllers/plugins/date.py
22 22
Un plugin pour VigiBoard qui ajoute une colonne avec la date à laquelle
23 23
est survenu un événement et la durée depuis laquelle l'événement est actif.
24 24
"""
25
from datetime import datetime, timedelta
25 26
import tw.forms as twf
26 27
from pylons.i18n import ugettext as _, lazy_ugettext as l_
27 28
from tg.i18n import get_lang
28 29
import tg
30
from babel import Locale
29 31

  
32
from vigilo.turbogears.helpers import get_locales
30 33
from vigilo.models import tables
31 34

  
32 35
from vigiboard.controllers.plugins import VigiboardRequestPlugin, ITEMS
......
87 90
        if search.get('to_date'):
88 91
            query.add_filter(tables.CorrEvent.timestamp_active <=
89 92
                search['to_date'])
93

  
94
    def get_data(self, event):
95
        state = tables.StateName.value_to_statename(
96
                    event[0].cause.current_state)
97
        # La résolution maximale de Nagios est la seconde.
98
        # On supprime les microsecondes qui ne nous apportent
99
        # aucune information et fausse l'affichage dans l'export CSV
100
        # en créant un nouvel objet timedelta dérivé du premier.
101
        duration = datetime.now() - event[0].timestamp_active
102
        duration = timedelta(days=duration.days, seconds=duration.seconds)
103
        return {
104
            'state': state,
105
            'date': event[0].cause.timestamp,
106
            'duration': duration,
107
        }
vigiboard/controllers/plugins/details.py
164 164
                maps = user_maps,
165 165
                idcause = event.idcause,
166 166
            )
167

  
168
    def get_data(self, event):
169
        state = StateName.value_to_statename(event[0].cause.current_state)
170
        peak_state = StateName.value_to_statename(event[0].cause.peak_state)
171
        init_state = StateName.value_to_statename(event[0].cause.initial_state)
172
        return {
173
            'state': state,
174
            'peak_state': peak_state,
175
            'initial_state': init_state,
176
            'id': event[0].idcorrevent,
177
        }
vigiboard/controllers/plugins/hls.py
154 154
            hls[service.idcorrevent].append(service.servicename)
155 155

  
156 156
        return hls
157

  
vigiboard/controllers/plugins/hostname.py
47 47
        if search.get('host'):
48 48
            host = sql_escape_like(search['host'])
49 49
            query.add_filter(query.items.c.hostname.ilike(host))
50

  
51
    def get_data(self, event):
52
        return {
53
            'hostname': event.hostname,
54
        }
vigiboard/controllers/plugins/id.py
28 28

  
29 29
class PluginId(VigiboardRequestPlugin):
30 30
    """Plugin de debug qui affiche l'identifiant de l'événement corrélé."""
31
    pass
31
    def get_data(self, event):
32
        return {
33
            'id': event[0].idcorrevent,
34
        }
vigiboard/controllers/plugins/masked_events.py
54 54
            # Il faut retirer la cause du décompte.
55 55
            res[count.idcorrevent] = count.masked - 1
56 56
        return res
57

  
58
    def get_data(self, event):
59
        return {
60
            'id': event[0].idcorrevent,
61
        }
vigiboard/controllers/plugins/occurrences.py
22 22
Un plugin pour VigiBoard qui ajoute une colonne avec le nombre
23 23
d'occurrences d'un événement corrélé donné.
24 24
"""
25
from vigilo.models.tables import StateName
25 26
from vigiboard.controllers.plugins import VigiboardRequestPlugin
26 27

  
27 28
class PluginOccurrences(VigiboardRequestPlugin):
......
31 32
    corrélateur chaque fois qu'un événement brut survient sur la cause
32 33
    de l'événement corrélé.
33 34
    """
34
    pass
35
    def get_data(self, event):
36
        state = StateName.value_to_statename(event[0].cause.current_state)
37
        return {
38
            'state': state,
39
            'occurrences': event[0].occurrence,
40
        }
vigiboard/controllers/plugins/output.py
47 47
        if search.get('output'):
48 48
            output = sql_escape_like(search['output'])
49 49
            query.add_filter(Event.message.ilike(output))
50

  
51
    def get_data(self, event):
52
        return {
53
            'output': event[0].cause.message,
54
        }
vigiboard/controllers/plugins/priority.py
25 25
import tw.forms as twf
26 26
from pylons.i18n import lazy_ugettext as l_
27 27

  
28
from vigilo.models.tables import CorrEvent
28
from vigilo.models.tables import CorrEvent, StateName
29 29
from vigiboard.controllers.plugins import VigiboardRequestPlugin, ITEMS
30 30

  
31 31
from tw.forms.fields import ContainerMixin, FormField
......
130 130
            query.add_filter(CorrEvent.priority < value)
131 131
        elif op == 'lte':
132 132
            query.add_filter(CorrEvent.priority <= value)
133

  
134
    def get_data(self, event):
135
        state = StateName.value_to_statename(event[0].cause.current_state)
136
        return {
137
            'state': state,
138
            'priority': event[0].priority,
139
        }
vigiboard/controllers/plugins/servicename.py
50 50
        if search.get('service'):
51 51
            service = sql_escape_like(search['service'])
52 52
            query.add_filter(query.items.c.servicename.ilike(service))
53

  
54
    def get_data(self, event):
55
        return {
56
            'servicename': event.servicename,
57
        }
vigiboard/controllers/plugins/status.py
26 26
    -   la dernière colonne permet de (dé)sélectionner l'événement pour
27 27
        effectuer un traitement par lot.
28 28
"""
29
import urllib
30
import tg
29 31
import tw.forms as twf
30 32
from pylons.i18n import lazy_ugettext as l_
31 33

  
32
from vigilo.models.tables import CorrEvent
34
from vigilo.models.tables import CorrEvent, StateName
33 35
from vigilo.models.functions import sql_escape_like
34 36
from vigiboard.controllers.plugins import VigiboardRequestPlugin, ITEMS
35 37

  
......
88 90
            except (ValueError, TypeError):
89 91
                # On ignore silencieusement le critère de recherche erroné.
90 92
                pass
93

  
94
    def get_data(self, event):
95
        cause = event[0].cause
96
        ack = event[0].ack
97
        state = StateName.value_to_statename(cause.current_state)
98

  
99
        trouble_ticket_id = None
100
        trouble_ticket_link = None
101
        if event[0].trouble_ticket:
102
            trouble_ticket_id = event[0].trouble_ticket
103
            trouble_ticket_link = tg.config['vigiboard_links.tt'] % {
104
                'id': event[0].idcorrevent,
105
                'host': event[1] and urllib.quote(event[1], '') or event[1],
106
                'service': event[2] and urllib.quote(event[2], '') or event[2],
107
                'tt': trouble_ticket_id and \
108
                        urllib.quote(trouble_ticket_id, '') or \
109
                        trouble_ticket_id,
110
            }
111

  
112
        return {
113
            'trouble_ticket_link': trouble_ticket_link,
114
            'trouble_ticket_id': trouble_ticket_id,
115
            'state': state,
116
            'id': event[0].idcorrevent,
117
            'ack': ack,
118
        }
vigiboard/controllers/root.py
27 27

  
28 28
from tg.exceptions import HTTPNotFound
29 29
from tg import expose, validate, require, flash, url, \
30
    tmpl_context, request, config, session, redirect
30
    tmpl_context, request, response, config, session, redirect
31 31
from webhelpers import paginate
32 32
from tw.forms import validators
33 33
from pylons.i18n import ugettext as _, lazy_ugettext as l_, get_lang
......
59 59
from vigiboard.controllers.vigiboardrequest import VigiboardRequest
60 60
from vigiboard.controllers.feeds import FeedsController
61 61

  
62
from vigiboard.lib import export_csv
62 63
from vigiboard.widgets.edit_event import edit_event_status_options, \
63 64
                                            EditEventForm
64 65
from vigiboard.widgets.search_form import create_search_form
......
127 128
        validators=IndexSchema(),
128 129
        error_handler = process_form_errors)
129 130
    @expose('events_table.html')
131
    @expose('csv', content_type='text/csv')
130 132
    @require(access_restriction)
131 133
    def index(self, page, **search):
132 134
        """
......
201 203
            plugin_data = plugins[plugin].get_bulk_data(ids_correvents)
202 204
            if plugin_data:
203 205
                plugins_data[plugin] = plugin_data
206
            else:
207
                plugins_data[plugin] = {}
204 208

  
205 209
        # Ajout des formulaires et préparation
206 210
        # des données pour ces formulaires.
......
210 214
        tmpl_context.edit_event_form = EditEventForm("edit_event_form",
211 215
            submit_text=_('Apply'), action=url('/update'))
212 216

  
217
        if request.response_type == 'text/csv':
218
            # Sans les 2 en-têtes suivants qui désactivent la mise en cache,
219
            # Internet Explorer refuse de télécharger le fichier CSV (cf. #961).
220
            response.headers['Pragma'] = 'public'           # Nécessaire pour IE.
221
            response.headers['Cache-Control'] = 'max-age=0' # Nécessaire pour IE.
222

  
223
            response.headers['Content-Disposition'] = \
224
                            'attachment;filename="alerts.csv"'
225
            return export_csv.export(page, plugins_data)
226

  
213 227
        return dict(
214 228
            hostname = None,
215 229
            servicename = None,
......
465 479
        tmpl_context.edit_event_form = EditEventForm("edit_event_form",
466 480
            submit_text=_('Apply'), action=url('/update'))
467 481

  
482
        plugins_data = {}
483
        for plugin in dict(config['columns_plugins']):
484
            plugins_data[plugin] = {}
485

  
468 486
        return dict(
469 487
            hostname = host,
470 488
            servicename = service,
471
            plugins_data = {},
489
            plugins_data = plugins_data,
472 490
            page = page,
473 491
            event_edit_status_options = edit_event_status_options,
474 492
            search_form = create_search_form,
......
721 739
        # l'utilisateur (s'il n'appartient pas au groupe 'managers')
722 740
        is_manager = in_group('managers').is_met(request.environ)
723 741
        if not is_manager:
724

  
725 742
            user = get_current_user()
726 743

  
727 744
            events = events.join(
vigiboard/lib/export_csv.py
1
# vim: set fileencoding=utf-8 sw=4 ts=4 et :
2
################################################################################
3
#
4
# Copyright (C) 2007-2012 CS-SI
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License version 2 as
8
# published by the Free Software Foundation.
9
#
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with this program; if not, write to the Free Software
17
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18
################################################################################
19

  
20
"""Fonction d'export des alertes au format CSV."""
21

  
22
import csv
23
from cStringIO import StringIO
24

  
25
from tg import config
26

  
27
def export(page, plugins_data):
28
    buf = StringIO()
29
    quoting = config.get('csv_quoting', 'ALL').upper()
30
    if quoting not in ('ALL', 'MINIMAL', 'NONNUMERIC', 'NONE'):
31
        quoting = 'ALL'
32
    csv_writer = csv.DictWriter(buf,
33
        config['csv_columns'],
34
        extrasaction='ignore',
35
        delimiter=config.get("csv_delimiter_char", ';'),
36
        escapechar=config.get("csv_escape_char", '\\'),
37
        quotechar=config.get("csv_quote_char", '"'),
38
        quoting=getattr(csv, 'QUOTE_%s' % quoting))
39
    csv_writer.writerow(dict(zip(config['csv_columns'], config['csv_columns'])))
40

  
41
    for item in page.items:
42
        values = {}
43
        for plugin_name, plugin_instance in config['columns_plugins']:
44
            if plugins_data[plugin_name]:
45
                values[plugin_name] = repr(plugins_data[plugin_name])
46
            else:
47
                for data_key, data_value in \
48
                    plugin_instance.get_data(item).iteritems():
49
                    # Pour les valeurs en unicode, on convertit en UTF-8.
50
                    if isinstance(data_value, unicode):
51
                        values[data_key] = data_value.encode('utf-8')
52
                    # Pour le reste, on suppose qu'on peut en obtenir une
53
                    # représentation adéquate dont l'encodage ne posera pas
54
                    # de problème.
55
                    else:
56
                        values[data_key] = data_value
57
        csv_writer.writerow(values)
58
    return buf.getvalue()
vigiboard/tests/functional/test_correvents_table.py
30 30
        """
31 31
        # L'utilisateur n'est pas authentifié.
32 32
        response = self.app.get('/', status=401)
33
        response = self.app.get('/index.csv', status=401)
33 34

  
34 35
        # L'utilisateur est authentifié avec des permissions réduites.
35 36
        environ = {'REMOTE_USER': 'limited_access'}
......
45 46
        print "There are %d columns in the result set" % len(cols)
46 47
        assert_true(len(cols) > 1)
47 48

  
49
        # Mêmes vérifications pour le CSV.
50
        response = self.app.get('/index.csv', extra_environ=environ)
51
        # 1 ligne d'en-tête + 2 lignes de données
52
        lines = response.body.strip().splitlines()
53
        assert_equal(3, len(lines))
54
        assert_true(len(lines[0].split(';')) > 1)
55

  
56

  
48 57
        # L'utilisateur est authentifié avec des permissions plus étendues.
49 58
        environ = {'REMOTE_USER': 'access'}
50 59
        response = self.app.get('/', extra_environ=environ)
......
59 68
        print "There are %d columns in the result set" % len(cols)
60 69
        assert_true(len(cols) > 1)
61 70

  
71
        # Mêmes vérifications pour le CSV.
72
        response = self.app.get('/index.csv', extra_environ=environ)
73
        # 1 ligne d'en-tête + 5 lignes de données
74
        lines = response.body.strip().splitlines()
75
        assert_equal(6, len(lines))
76
        assert_true(len(lines[0].split(';')) > 1)
77

  
78

  
62 79
        # L'utilisateur fait partie du groupe 'managers'
63 80
        environ = {'REMOTE_USER': 'manager'}
64 81
        response = self.app.get('/', extra_environ=environ)
......
73 90
        print "There are %d columns in the result set" % len(cols)
74 91
        assert_true(len(cols) > 1)
75 92

  
93
        # Mêmes vérifications pour le CSV.
94
        response = self.app.get('/index.csv', extra_environ=environ)
95
        # 1 ligne d'en-tête + 5 lignes de données
96
        lines = response.body.strip().splitlines()
97
        assert_equal(6, len(lines))
98
        assert_true(len(lines[0].split(';')) > 1)
99

  
76 100
    def test_correvents_table_for_LLS(self):
77 101
        """
78 102
        Tableau des événements corrélés pour un service de bas niveau.

Also available in: Unified diff