Skip to content
Snippets Groups Projects
Commit b25e4648 authored by Pelle Koster's avatar Pelle Koster
Browse files

Merge branch 'feature/DBOARD3-900-make-report-prettier' into 'develop'

Feature/DBOARD3-925 make report prettier

See merge request live-projects/brian-polling-manager!7
parents 70d162df 9a717345
No related branches found
No related tags found
No related merge requests found
...@@ -32,21 +32,18 @@ sent to the OC. ...@@ -32,21 +32,18 @@ sent to the OC.
--config PATH Path to a config file for this tool. The schema this config --config PATH Path to a config file for this tool. The schema this config
file must adhere to can be found in file must adhere to can be found in
``brian_polling_manager.error_report.config.ERROR_REPORT_CONFIG_SCHEMA`` ``brian_polling_manager.error_report.config.ERROR_REPORT_CONFIG_SCHEMA``
--email/--no-email Either send an email using the email config, or print to
stdout. Default (--email)
[2024-04-09] This tool is the successor of a bash-script that was used before. That *) There are some rules about which routers/interfaces to include and exclude. See the
script has some peculiarities in it's output and as of this new version mimics the
output of the earlier tool as much as possible.
*) There are some rules which routers/interfaces to include and exclude. See the
`get_relevant_interfaces`_ function for more details. `get_relevant_interfaces`_ function for more details.
""" """
from datetime import datetime from datetime import datetime
import json
import logging import logging
import os
import pathlib import pathlib
from typing import Sequence import sys
from typing import Sequence, Tuple
from brian_polling_manager.influx import influx_client from brian_polling_manager.influx import influx_client
from brian_polling_manager.inventory import load_interfaces from brian_polling_manager.inventory import load_interfaces
import click import click
...@@ -143,43 +140,12 @@ PROCESSED_ERROR_COUNTERS_SCHEMA = { ...@@ -143,43 +140,12 @@ PROCESSED_ERROR_COUNTERS_SCHEMA = {
} }
LOGGING_DEFAULT_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {"simple": {"format": "%(asctime)s - %(levelname)s - %(message)s"}},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "INFO",
"formatter": "simple",
"stream": "ext://sys.stdout",
},
},
"loggers": {
"brian_polling_manager": {
"level": "INFO",
"handlers": ["console"],
"propagate": False,
}
},
"root": {"level": "INFO", "handlers": ["console"]},
}
def setup_logging(): def setup_logging():
""" logging.basicConfig(
set up logging using the configured filename stream=sys.stderr,
level="INFO",
if LOGGING_CONFIG is defined in the environment, use this for format="%(asctime)s - %(levelname)s - %(message)s",
the filename, otherwise use LOGGING_DEFAULT_CONFIG )
"""
logging_config = LOGGING_DEFAULT_CONFIG
if "LOGGING_CONFIG" in os.environ:
filename = os.environ["LOGGING_CONFIG"]
with open(filename) as f:
logging_config = json.loads(f.read())
logging.config.dictConfig(logging_config)
def get_error_points(client: InfluxDBClient, time_window: str): def get_error_points(client: InfluxDBClient, time_window: str):
...@@ -251,6 +217,8 @@ def interface_errors( ...@@ -251,6 +217,8 @@ def interface_errors(
} }
result = {"interfaces": [], "excluded_interfaces": []} result = {"interfaces": [], "excluded_interfaces": []}
new_errors = {}
for (router, ifc), info in interface_info.items(): for (router, ifc), info in interface_info.items():
try: try:
today = todays_data[(router, ifc)] today = todays_data[(router, ifc)]
...@@ -273,46 +241,60 @@ def interface_errors( ...@@ -273,46 +241,60 @@ def interface_errors(
"description": info["description"], "description": info["description"],
} }
if not is_excluded_interface(info["description"], exclusions): if is_excluded_interface(info, exclusions):
nonzero_errors = {err: val for err, val in today.items() if val > 0}
counters["error_counters"] = nonzero_errors
if any(yesterday.values()):
# we have existing errors
# This is strictly not the most correct way to determine differences.
# during the day the error count may have reset and diffs may actually
# be negative, but we ignore those because that is (mostly) how it was
# done in the orginal bash script
diff = {
err: (val - yesterday[err])
for err, val in nonzero_errors.items()
if (val - yesterday[err]) > 0
}
if not diff:
# Skip interface if it does not have any increased error counters
continue
counters["diff"] = diff
result["interfaces"].append(counters)
else:
logger.info(f"Found excluded interface {router} - {ifc}") logger.info(f"Found excluded interface {router} - {ifc}")
result["excluded_interfaces"].append(counters) result["excluded_interfaces"].append(counters)
continue
nonzero_errors = {err: val for err, val in today.items() if val > 0}
counters["error_counters"] = nonzero_errors
new_errors[(router, ifc)] = sum(nonzero_errors.values())
if any(yesterday.values()):
# we have existing errors
# This is strictly not the most correct way to determine differences.
# during the day the error count may have reset and diffs may actually
# be negative, but we ignore those because that is (mostly) how it was
# done in the orginal bash script
diff = {
err: (val - yesterday[err])
for err, val in nonzero_errors.items()
if (val - yesterday[err]) > 0
}
if not diff:
# Skip interface if it does not have any increased error counters
continue
counters["diff"] = diff
new_errors[(router, ifc)] = sum(diff.values())
result["interfaces"].append(counters)
result["interfaces"].sort(
key=lambda i: (
-new_errors[(i["router"], i["interface"])],
i["router"],
i["interface"],
),
)
return result return result
def is_excluded_interface(description: str, exclusions: Sequence[str]): def is_excluded_interface(ifc, exclusions: Sequence[Tuple[str, str]]):
"""Some interfaces generate a lot of noise and should be excluded""" """Some interfaces generate a lot of noise and should be excluded"""
# We may want to put this logic inside inventory provider router, interface = ifc["router"], ifc["name"]
return any(excl.lower() in description.lower() for excl in exclusions)
return any(
router.lower() == excl[0].lower() and interface.lower() == excl[1].lower()
for excl in exclusions
)
def get_relevant_interfaces(hosts): def get_relevant_interfaces(hosts):
"""Get interface info from inventory provider. Some interfaces are considered """Get interface info from inventory provider. Some interfaces are considered
irrelevant based on their description""" irrelevant based on their description
"""
return _filter_and_sort_interfaces(load_interfaces(hosts)) return _filter_and_sort_interfaces(load_interfaces(hosts))
...@@ -338,7 +320,7 @@ def _filter_and_sort_interfaces(interfaces): ...@@ -338,7 +320,7 @@ def _filter_and_sort_interfaces(interfaces):
) )
def main(config: dict): def main(config: dict, send_mail: bool):
"""Main function for the error reporting script """Main function for the error reporting script
:param config: An instance of `ERROR_REPORT_CONFIG_SCHEMA` :param config: An instance of `ERROR_REPORT_CONFIG_SCHEMA`
...@@ -364,6 +346,10 @@ def main(config: dict): ...@@ -364,6 +346,10 @@ def main(config: dict):
all_error_counters, all_error_counters,
date=datetime.utcnow().strftime("%a %d %b %H:%M:%S UTC %Y"), date=datetime.utcnow().strftime("%a %d %b %H:%M:%S UTC %Y"),
) )
if not send_mail:
click.echo(body)
return
email = render_email(config["email"], html=body) email = render_email(config["email"], html=body)
logger.info("Sending email...") logger.info("Sending email...")
send_email(email, config=config["email"]) send_email(email, config=config["email"])
...@@ -383,10 +369,17 @@ def main(config: dict): ...@@ -383,10 +369,17 @@ def main(config: dict):
), ),
help="path to a config file", help="path to a config file",
) )
def cli(config): @click.option(
"--email/--no-email",
"send_mail",
default=True,
help="toggle sending out an email using the email configuration (--email), "
"or printing the report to stdout (--no-email). Default: --email",
)
def cli(config, send_mail):
setup_logging() setup_logging()
config = load(config_file=config) config = load(config_file=config)
main(config) main(config, send_mail=send_mail)
if __name__ == "__main__": if __name__ == "__main__":
......
...@@ -18,6 +18,6 @@ ...@@ -18,6 +18,6 @@
"password": "user-password" "password": "user-password"
}, },
"exclude-interfaces": [ "exclude-interfaces": [
"SOME DESCRIPTION PART" ["some.router", "some/ifc"]
] ]
} }
...@@ -17,6 +17,7 @@ ERROR_REPORT_CONFIG_SCHEMA = { ...@@ -17,6 +17,7 @@ ERROR_REPORT_CONFIG_SCHEMA = {
"to": {"$ref": "#/definitions/string-or-array"}, "to": {"$ref": "#/definitions/string-or-array"},
"cc": {"$ref": "#/definitions/string-or-array"}, "cc": {"$ref": "#/definitions/string-or-array"},
"hostname": {"type": "string"}, "hostname": {"type": "string"},
"port": {"type": "integer"},
"username": {"type": "string"}, "username": {"type": "string"},
"password": {"type": "string"}, "password": {"type": "string"},
"starttls": {"type": "boolean"}, "starttls": {"type": "boolean"},
...@@ -34,7 +35,7 @@ ERROR_REPORT_CONFIG_SCHEMA = { ...@@ -34,7 +35,7 @@ ERROR_REPORT_CONFIG_SCHEMA = {
{"type": "array", "items": {"type": "string"}, "minItems": 1}, {"type": "array", "items": {"type": "string"}, "minItems": 1},
] ]
}, },
"influx-db-measurement": { "influx-db-params": {
"type": "object", "type": "object",
"properties": { "properties": {
"ssl": {"type": "boolean"}, "ssl": {"type": "boolean"},
...@@ -64,10 +65,16 @@ ERROR_REPORT_CONFIG_SCHEMA = { ...@@ -64,10 +65,16 @@ ERROR_REPORT_CONFIG_SCHEMA = {
"items": {"type": "string", "format": "uri"}, "items": {"type": "string", "format": "uri"},
"minItems": 1, "minItems": 1,
}, },
"influx": {"$ref": "#/definitions/influx-db-measurement"}, "influx": {"$ref": "#/definitions/influx-db-params"},
"exclude-interfaces": { "exclude-interfaces": {
"type": "array", "type": "array",
"items": {"type": "string"}, "items": {
"type": "array",
"prefixItems": [
{"type": "string"},
{"type": "string"},
],
},
}, },
}, },
"required": ["email", "influx"], "required": ["email", "influx"],
......
{#-
We have a lot of hardcoded stuff in here. Ideally we don't want to use jinja here
at all but use pythons EmailMessage or MIMEBase classes to construct the email.
However, there are a bunch of peculiarities in the email message that make it hard to
do this, unless we can simplify the email. But for now we stick to the original email
message format
-#}
From: {{ config.from }}
Sender: {{ config.from }}
{%- if config.reply_to is defined %}
Reply-To: {{ config.reply_to }}
{%- endif %}
To: {{ config.to | join('; ') }}
{%- if config.cc is defined %}
Cc: {{ config.cc | join('; ') }}
{%- endif %}
Mime-Version: 1.0
Subject: {{ subject }}
X-Mailer: /home/neteam/code/juniper_errors_report/email.sh
Content-Type: multipart/mixed; boundary="-"
---
Content-Type: text/plain; format=flowed; charset=ISO-8859-1
Content-Disposition: inline
Content-Transfer-Encoding: 8bit
Hi,
The latest errors report is attached.
Regards,
neteam@neteam-server01.geant.org:/home/neteam/code/juniper_errors_report
---
Content-Type: text/plain; name="errors_report.html"
Content-Transfer-Encoding: base64
Content-Disposition: inline; filename="errors_report.html"
Content-MD5: {{ md5_hash }}
{{ b64_report | wordwrap(76) }}
\ No newline at end of file
<html> <!DOCTYPE html>
<body> <html lang="en">
<pre> <head>
{#- We mix tabs with spaces and have otherwise inconsistent whitespace to keep the <meta charset="utf-8" />
output as close as possible to the output of the original script <meta name="viewport" content="width=device-width, initial-scale=1" />
#} <title>GÉANT Interface error report</title>
{%- for ifc in interfaces %} <style>
================================= html {
{{ ifc.router }} font-family: monospace;
================================= font-size: 12pt;
{{ ifc.interface }} {{ ifc.description }} }
{%- if ifc.diff %}
{%- for err, diff in ifc.diff.items() %} section, footer {
{{ err }}{{ " " if err == "framing-errors" else "" }} {{ ifc.error_counters[err] }} Diff: {{ diff }} padding: 20pt 0 0 20pt;
{%- endfor %} }
{%- else %}
{%- for err, val in ifc.error_counters.items() %} .interface-block {
{{ err }}{{ " " if err == "framing-errors" else "" }} {{ val }} padding-bottom: 20pt;
{%- endfor %} }
{%- endif %}
{{ '' }}
{%- endfor %}
{%- if excluded_interfaces %} .interface-errors {
ROUTER,INTERFACE,FRAMING ERRORS,BIT ERROR SECONDS,ERRORED BLOCKS SECONDS,CRC ERRORS,TOTAL ERRORS,INPUT DISCARDS,INPUT DROPS,OUTPUT DROPS padding-left: 20pt;
{%- for ifc in excluded_interfaces %} }
{{ifc.router}},{{ifc.interface}},{{ ifc.error_counters.values() | join(',') }},{{ifc.description}}
{%- endfor %}
{%- endif %}
ul {
list-style-type: none;
}
h3, p, ul {
margin: 0pt;
}
Generated {{ date }} .error-table {
</pre> padding-left: 20pt;
</body> }
td {
padding-right: 10pt;
}
td.diff {
padding-left: 20pt;
}
</style>
</head>
<body>
<section>
<div class="interface-report">
{%- for ifc in interfaces %}
<div class="interface-block">
<h3>{{ ifc.router }}</h3>
<div class="interface-errors">
<p><strong>{{ ifc.interface }}</strong> {{ ifc.description }}</p>
<table class="error-table">
{%- if ifc.diff %}
{%- for err, diff in ifc.diff.items() %}
<tr>
<td>{{ err }}</td>
<td>{{ ifc.error_counters[err] }}</td>
<td class="diff">Diff:</td>
<td>{{ diff }}</td>
</tr>
{%- endfor %}
{%- else %}
{%- for err, val in ifc.error_counters.items() %}
<tr>
<td>{{ err }}</td>
<td>{{ val }}</td>
</tr>
{%- endfor %}
{%- endif %}
</table>
</div>
</div>
{%- endfor %}
</div>
</section>
{%- if excluded_interfaces %}
<section class="excluded">
<p>EXCLUDED INTERFACES</p>
<ul class="interface-list">
{%- for ifc in excluded_interfaces %}
<li>{{ ifc.router }} - {{ ifc.interface }} - {{ ifc.description }}</li>
{%- endfor %}
</ul>
</section>
{%- endif %}
<footer>Generated {{ date }}</footer>
</body>
</html> </html>
from email import encoders
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import logging import logging
import pathlib import pathlib
import smtplib import smtplib
import jinja2 import jinja2
import hashlib
import base64
THIS_DIR = pathlib.Path(__file__).parent THIS_DIR = pathlib.Path(__file__).parent
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -18,32 +20,43 @@ def render_html(errors, date): ...@@ -18,32 +20,43 @@ def render_html(errors, date):
:param errors: an instance of `PROCESSED_ERROR_COUNTERS_SCHEMA` :param errors: an instance of `PROCESSED_ERROR_COUNTERS_SCHEMA`
:param template_file: a jinja2 template to render :param template_file: a jinja2 template to render
""" """
env = jinja2.Environment( env = jinja2.Environment(loader=jinja2.FileSystemLoader(THIS_DIR))
# use CRLF since that was (explicitly) used by the original bash script, perhaps
# it's unnecessary
loader=jinja2.FileSystemLoader(THIS_DIR),
newline_sequence="\r\n",
)
template = env.get_template("error_report.html.jinja2") template = env.get_template("error_report.html.jinja2")
return template.render(**errors, date=date) return template.render(**errors, date=date)
def render_email( def render_email(
email_config: dict, html: str, subject="GEANT Juniper Interface Errors Report" email_config: dict, html: str, subject="GEANT Interface Errors Report"
): ):
env = jinja2.Environment(loader=jinja2.FileSystemLoader(THIS_DIR))
template = env.get_template("email.jinja2") body = "The latest errors report is attached."
html_as_bytes = html.encode()
md5_hash = hashlib.md5(html_as_bytes).hexdigest() message = MIMEMultipart()
b64_report = base64.b64encode(html_as_bytes).decode() message["From"] = email_config["from"]
return template.render( message["To"] = "; ".join(email_config["to"])
config=email_config, subject=subject, md5_hash=md5_hash, b64_report=b64_report if email_config.get("cc"):
) message["Cc"] = "; ".join(email_config["cc"])
message["Subject"] = subject
message.attach(MIMEText(body, "plain"))
part = MIMEBase("application", "octet-stream")
part.set_payload(html.encode())
encoders.encode_base64(part)
part.add_header("Content-Disposition", "attachment; filename= error_report.html")
message.attach(part)
return message.as_string()
def send_email(payload: str, config: dict): def send_email(payload: str, config: dict):
with smtplib.SMTP( with smtplib.SMTP(
host=config["hostname"], port=25, timeout=SMTP_TIMEOUT_SECONDS host=config["hostname"],
port=config.get("port", 25),
timeout=SMTP_TIMEOUT_SECONDS,
) as server: ) as server:
if config.get("starttls"): if config.get("starttls"):
server.starttls() server.starttls()
......
<html> <!DOCTYPE html>
<body> <html lang="en">
<pre> <head>
================================= <meta charset="utf-8" />
mx1.ams.nl.geant.net <meta name="viewport" content="width=device-width, initial-scale=1" />
================================= <title>GÉANT Interface error report</title>
ae1 PHY blah blah <style>
framing-errors 4 Diff: 2 html {
input-drops 2 Diff: 1 font-family: monospace;
font-size: 12pt;
}
================================= section, footer {
mx1.fra.de.geant.net padding: 20pt 0 0 20pt;
================================= }
ae10 PHY blah blah foo
input-drops 3
.interface-block {
padding-bottom: 20pt;
}
.interface-errors {
padding-left: 20pt;
}
ul {
list-style-type: none;
}
Generated <some date> h3, p, ul {
</pre> margin: 0pt;
</body> }
.error-table {
padding-left: 20pt;
}
td {
padding-right: 10pt;
}
td.diff {
padding-left: 20pt;
}
</style>
</head>
<body>
<section>
<div class="interface-report">
<div class="interface-block">
<h3>mx1.ams.nl.geant.net</h3>
<div class="interface-errors">
<p><strong>ae1</strong> PHY blah blah</p>
<table class="error-table">
<tr>
<td>framing-errors</td>
<td>4</td>
<td class="diff">Diff:</td>
<td>2</td>
</tr>
<tr>
<td>input-drops</td>
<td>2</td>
<td class="diff">Diff:</td>
<td>1</td>
</tr>
</table>
</div>
</div>
<div class="interface-block">
<h3>mx1.fra.de.geant.net</h3>
<div class="interface-errors">
<p><strong>ae10</strong> PHY blah blah foo</p>
<table class="error-table">
<tr>
<td>input-drops</td>
<td>3</td>
</tr>
</table>
</div>
</div>
</div>
</section>
<footer>Generated SOME_DATE</footer>
</body>
</html> </html>
\ No newline at end of file
<html> <!DOCTYPE html>
<body> <html lang="en">
<pre> <head>
================================= <meta charset="utf-8" />
mx1.ams.nl.geant.net <meta name="viewport" content="width=device-width, initial-scale=1" />
================================= <title>GÉANT Interface error report</title>
ae1 PHY blah blah <style>
input-drops 2 html {
font-family: monospace;
font-size: 12pt;
}
ROUTER,INTERFACE,FRAMING ERRORS,BIT ERROR SECONDS,ERRORED BLOCKS SECONDS,CRC ERRORS,TOTAL ERRORS,INPUT DISCARDS,INPUT DROPS,OUTPUT DROPS section, footer {
mx1.fra.de.geant.net,ae10,1,2,3,4,5,6,7,8,PHY blah blah foo padding: 20pt 0 0 20pt;
}
.interface-block {
padding-bottom: 20pt;
}
.interface-errors {
padding-left: 20pt;
}
Generated <some date> ul {
</pre> list-style-type: none;
</body> }
h3, p, ul {
margin: 0pt;
}
.error-table {
padding-left: 20pt;
}
td {
padding-right: 10pt;
}
td.diff {
padding-left: 20pt;
}
</style>
</head>
<body>
<section>
<div class="interface-report">
<div class="interface-block">
<h3>mx1.ams.nl.geant.net</h3>
<div class="interface-errors">
<p><strong>ae1</strong> PHY blah blah</p>
<table class="error-table">
<tr>
<td>input-drops</td>
<td>2</td>
</tr>
</table>
</div>
</div>
</div>
</section>
<section class="excluded">
<p>EXCLUDED INTERFACES</p>
<ul class="interface-list">
<li>mx1.fra.de.geant.net - ae10 - PHY blah blah foo</li>
</ul>
</section>
<footer>Generated SOME_DATE</footer>
</body>
</html> </html>
\ No newline at end of file
import base64 import base64
import hashlib
import json import json
import pathlib import pathlib
...@@ -161,7 +160,7 @@ def test_validate_config(tmp_path): ...@@ -161,7 +160,7 @@ def test_validate_config(tmp_path):
"username": "some-username", "username": "some-username",
"password": "user-password", "password": "user-password",
}, },
"exclude-interfaces": ["SOME DESCRIPTION PART"], "exclude-interfaces": [["some.router", "some/ifc"]],
} }
config_file.write_text(json.dumps(content)) config_file.write_text(json.dumps(content))
result = config.load(config_file) result = config.load(config_file)
...@@ -184,7 +183,7 @@ def test_validate_config(tmp_path): ...@@ -184,7 +183,7 @@ def test_validate_config(tmp_path):
"username": "some-username", "username": "some-username",
"password": "user-password", "password": "user-password",
}, },
"exclude-interfaces": ["SOME DESCRIPTION PART"], "exclude-interfaces": [["some.router", "some/ifc"]],
} }
...@@ -206,18 +205,19 @@ def test_get_relevant_interfaces(full_inventory): ...@@ -206,18 +205,19 @@ def test_get_relevant_interfaces(full_inventory):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"description, exclusions, is_excluded", "router, interface, exclusions, is_excluded",
[ [
("DESC", [], False), ("rtr", "ifc", [], False),
("DESC", ["DESC"], True), ("rtr", "ifc", [("rtr", "ifc")], True),
("DESC", ["desc"], True), ("rtr", "ifc", [("RTR", "IFC")], True),
("DESC", ["DESCMORE"], False), ("rtr", "ifc", [("rtr", "ifc.2")], False),
("DESC", ["MORE", "DESC"], True), ("rtr", "ifc", [("rtr", "ifc.2"), ("rtr", "ifc")], True),
("", ["DESC"], False), ("rtr", "ifc", [("rtr2", "ifc")], False),
], ],
) )
def test_excluded_interface(description, exclusions, is_excluded): def test_excluded_interface(router, interface, exclusions, is_excluded):
assert is_excluded_interface(description, exclusions) == is_excluded interface = {"router": router, "name": interface}
assert is_excluded_interface(interface, exclusions) == is_excluded
def test_get_error_points(mock_influx_client, create_error_point): def test_get_error_points(mock_influx_client, create_error_point):
...@@ -287,8 +287,8 @@ def test_interface_errors_with_new_errors(create_error_point, get_interface_erro ...@@ -287,8 +287,8 @@ def test_interface_errors_with_new_errors(create_error_point, get_interface_erro
def test_interface_errors_with_multiple_interfaces( def test_interface_errors_with_multiple_interfaces(
create_error_point, get_interface_errors create_error_point, get_interface_errors
): ):
create_error_point("mx1.ams.nl.geant.net", "ae1", "today", framing_errors=1) create_error_point("mx1.ams.nl.geant.net", "ae1", "today", framing_errors=2)
create_error_point("mx1.fra.de.geant.net", "ae10", "today", framing_errors=2) create_error_point("mx1.fra.de.geant.net", "ae10", "today", framing_errors=1)
errors = get_interface_errors() errors = get_interface_errors()
assert errors["interfaces"] == [ assert errors["interfaces"] == [
{ {
...@@ -296,7 +296,7 @@ def test_interface_errors_with_multiple_interfaces( ...@@ -296,7 +296,7 @@ def test_interface_errors_with_multiple_interfaces(
"interface": "ae1", "interface": "ae1",
"description": "PHY blah blah", "description": "PHY blah blah",
"error_counters": { "error_counters": {
"framing-errors": 1, "framing-errors": 2,
}, },
}, },
{ {
...@@ -304,7 +304,7 @@ def test_interface_errors_with_multiple_interfaces( ...@@ -304,7 +304,7 @@ def test_interface_errors_with_multiple_interfaces(
"interface": "ae10", "interface": "ae10",
"description": "PHY blah blah foo", "description": "PHY blah blah foo",
"error_counters": { "error_counters": {
"framing-errors": 2, "framing-errors": 1,
}, },
}, },
] ]
...@@ -400,7 +400,7 @@ def test_processes_excluded_interface(create_error_point, get_interface_errors): ...@@ -400,7 +400,7 @@ def test_processes_excluded_interface(create_error_point, get_interface_errors):
"mx1.fra.de.geant.net", "ae10", "today", input_drops=3, framing_errors=4 "mx1.fra.de.geant.net", "ae10", "today", input_drops=3, framing_errors=4
) # this interface is excluded through its description ) # this interface is excluded through its description
errors = get_interface_errors(exclusions=["foo"]) errors = get_interface_errors(exclusions=[("mx1.fra.de.geant.net", "ae10")])
assert errors["interfaces"] == [ assert errors["interfaces"] == [
{ {
"router": "mx1.ams.nl.geant.net", "router": "mx1.ams.nl.geant.net",
...@@ -428,6 +428,41 @@ def test_processes_excluded_interface(create_error_point, get_interface_errors): ...@@ -428,6 +428,41 @@ def test_processes_excluded_interface(create_error_point, get_interface_errors):
] ]
def test_orders_errors_by_highest_number(create_error_point, get_interface_errors):
create_error_point(
"mx1.ams.nl.geant.net", "ae1", "today", input_drops=10, framing_errors=2
)
create_error_point(
"mx1.fra.de.geant.net", "ae10", "today", input_drops=10, framing_errors=2
)
create_error_point(
"mx1.ams.nl.geant.net", "ae1", "yesterday", input_drops=1, framing_errors=2
)
errors = get_interface_errors()
interfaces = [(e["router"], e["interface"]) for e in errors["interfaces"]]
assert interfaces == [
("mx1.fra.de.geant.net", "ae10"),
("mx1.ams.nl.geant.net", "ae1"),
]
def test_some_more_order_test(create_error_point, get_interface_errors):
create_error_point(
"mx1.ams.nl.geant.net", "ae1", "yesterday", input_drops=1, framing_errors=2
)
create_error_point(
"mx1.ams.nl.geant.net", "ae1", "today", input_drops=2, framing_errors=4
)
create_error_point("mx1.fra.de.geant.net", "ae10", "today", input_drops=3)
errors = get_interface_errors()
interfaces = [(e["router"], e["interface"]) for e in errors["interfaces"]]
assert interfaces == [
("mx1.ams.nl.geant.net", "ae1"),
("mx1.fra.de.geant.net", "ae10"),
]
def test_render_html(create_error_point, get_interface_errors): def test_render_html(create_error_point, get_interface_errors):
create_error_point( create_error_point(
"mx1.ams.nl.geant.net", "ae1", "yesterday", input_drops=1, framing_errors=2 "mx1.ams.nl.geant.net", "ae1", "yesterday", input_drops=1, framing_errors=2
...@@ -438,11 +473,11 @@ def test_render_html(create_error_point, get_interface_errors): ...@@ -438,11 +473,11 @@ def test_render_html(create_error_point, get_interface_errors):
create_error_point("mx1.fra.de.geant.net", "ae10", "today", input_drops=3) create_error_point("mx1.fra.de.geant.net", "ae10", "today", input_drops=3)
errors = get_interface_errors() errors = get_interface_errors()
result = render_html(errors=errors, date="<some date>") result = render_html(errors=errors, date="SOME_DATE")
# The expected value contains mixed tabs and spaces. We put it in a separate file # The expected value contains mixed tabs and spaces. We put it in a separate file
# to comply with flake8 # to comply with flake8
expected = (DATA_DIR / "test_render_html-expected.html").read_text() expected = (DATA_DIR / "test_render_html-expected.html").read_text()
assert result == expected.replace("\n", "\r\n") assert result == expected
def test_render_html_with_exclusions(create_error_point, get_interface_errors): def test_render_html_with_exclusions(create_error_point, get_interface_errors):
...@@ -452,35 +487,25 @@ def test_render_html_with_exclusions(create_error_point, get_interface_errors): ...@@ -452,35 +487,25 @@ def test_render_html_with_exclusions(create_error_point, get_interface_errors):
"mx1.fra.de.geant.net", "mx1.fra.de.geant.net",
"ae10", "ae10",
"today", "today",
# mess up order of kwargs to test re-ordering
bit_error_seconds=2,
framing_errors=1, framing_errors=1,
input_crc_errors=4,
errored_blocks_seconds=3,
input_discards=6,
input_total_errors=5,
output_drops=8,
input_drops=7,
) )
errors = get_interface_errors(exclusions=["foo"]) errors = get_interface_errors(exclusions=[("mx1.fra.de.geant.net", "ae10")])
result = render_html(errors=errors, date="<some date>") result = render_html(errors=errors, date="SOME_DATE")
# The expected value contains mixed tabs and spaces. We put it in a separate file # The expected value contains mixed tabs and spaces. We put it in a separate file
# to comply with flake8 # to comply with flake8
expected = (DATA_DIR / "test_render_html_with_exclusions-expected.html").read_text() expected = (DATA_DIR / "test_render_html_with_exclusions-expected.html").read_text()
assert result == expected.replace("\n", "\r\n") assert result == expected
def test_render_email(): def test_render_email():
body = "<SOME_BODY>" body = "<SOME_BODY>"
md5 = hashlib.md5(body.encode()).hexdigest()
b64 = base64.b64encode(body.encode()).decode() b64 = base64.b64encode(body.encode()).decode()
config = {"from": "someone@geant.org", "to": ["someone.else@geant.org"]} config = {"from": "someone@geant.org", "to": ["someone.else@geant.org"]}
result = render_email(config, html=body, subject="<subject>") result = render_email(config, html=body, subject="<subject>")
assert "From: someone@geant.org" in result assert "From: someone@geant.org" in result
assert "To: someone.else@geant.org" in result assert "To: someone.else@geant.org" in result
assert md5 in result
assert b64 in result assert b64 in result
...@@ -590,7 +615,7 @@ def config_file(tmp_path): ...@@ -590,7 +615,7 @@ def config_file(tmp_path):
"username": "some-username", "username": "some-username",
"password": "user-password", "password": "user-password",
}, },
"exclude-interfaces": ["FOO"], "exclude-interfaces": [("mx1.fra.de.geant.net", "ae10")],
} }
path = tmp_path / "config.json" path = tmp_path / "config.json"
path.write_text(json.dumps(config)) path.write_text(json.dumps(config))
...@@ -623,9 +648,9 @@ def test_e2e( ...@@ -623,9 +648,9 @@ def test_e2e(
assert "The latest errors report is attached." in payload assert "The latest errors report is attached." in payload
report_b64 = payload.split("\n\n")[-1] report_b64 = payload.split("\n\n")[-2]
report = base64.b64decode(report_b64).decode() report = base64.b64decode(report_b64).decode()
assert "mx1.ams.nl.geant.net" in report assert "mx1.ams.nl.geant.net" in report
assert "ae1" in report assert "ae1" in report
assert "input-drops\t1" in report assert "<td>input-drops</td>" in report
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment