Revision a2fa6a5b
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 <qa@vigilo-dev.si.c-s.fr>
Reviewed-by: Thomas BURGUIERE <thomas.burguiere@c-s.fr>
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