diff --git a/brian_polling_manager/error_report/cli.py b/brian_polling_manager/error_report/cli.py index d21de54007022c314ec1d1fcbb907fe30f87502b..2e59e4197324f0399b15912fb9c1d7a6d8938449 100644 --- a/brian_polling_manager/error_report/cli.py +++ b/brian_polling_manager/error_report/cli.py @@ -32,21 +32,18 @@ sent to the OC. --config PATH Path to a config file for this tool. The schema this config file must adhere to can be found in ``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 -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 +*) There are some rules about which routers/interfaces to include and exclude. See the `get_relevant_interfaces`_ function for more details. """ from datetime import datetime -import json import logging -import os 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.inventory import load_interfaces import click @@ -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(): - """ - set up logging using the configured filename - - if LOGGING_CONFIG is defined in the environment, use this for - 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) + logging.basicConfig( + stream=sys.stderr, + level="INFO", + format="%(asctime)s - %(levelname)s - %(message)s", + ) def get_error_points(client: InfluxDBClient, time_window: str): @@ -251,6 +217,8 @@ def interface_errors( } result = {"interfaces": [], "excluded_interfaces": []} + new_errors = {} + for (router, ifc), info in interface_info.items(): try: today = todays_data[(router, ifc)] @@ -273,46 +241,60 @@ def interface_errors( "description": info["description"], } - if not is_excluded_interface(info["description"], 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: + if is_excluded_interface(info, exclusions): logger.info(f"Found excluded interface {router} - {ifc}") 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 -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""" - # We may want to put this logic inside inventory provider - return any(excl.lower() in description.lower() for excl in exclusions) + router, interface = ifc["router"], ifc["name"] + + return any( + router.lower() == excl[0].lower() and interface.lower() == excl[1].lower() + for excl in exclusions + ) def get_relevant_interfaces(hosts): """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)) @@ -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 :param config: An instance of `ERROR_REPORT_CONFIG_SCHEMA` @@ -364,6 +346,10 @@ def main(config: dict): all_error_counters, 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) logger.info("Sending email...") send_email(email, config=config["email"]) @@ -383,10 +369,17 @@ def main(config: dict): ), 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() config = load(config_file=config) - main(config) + main(config, send_mail=send_mail) if __name__ == "__main__": diff --git a/brian_polling_manager/error_report/config-example.json b/brian_polling_manager/error_report/config-example.json index 190d08e6615fb0f2cc6f06884381605654a6d731..b9cc65d8bc8908cf43a714d1b0116359490630b4 100644 --- a/brian_polling_manager/error_report/config-example.json +++ b/brian_polling_manager/error_report/config-example.json @@ -18,6 +18,6 @@ "password": "user-password" }, "exclude-interfaces": [ - "SOME DESCRIPTION PART" + ["some.router", "some/ifc"] ] } diff --git a/brian_polling_manager/error_report/config.py b/brian_polling_manager/error_report/config.py index 2614a4e191af3191867ad90466b1fb3717de69b6..f0bcc5b5d961db4a24c2a82e20335f16ca8dec52 100644 --- a/brian_polling_manager/error_report/config.py +++ b/brian_polling_manager/error_report/config.py @@ -17,6 +17,7 @@ ERROR_REPORT_CONFIG_SCHEMA = { "to": {"$ref": "#/definitions/string-or-array"}, "cc": {"$ref": "#/definitions/string-or-array"}, "hostname": {"type": "string"}, + "port": {"type": "integer"}, "username": {"type": "string"}, "password": {"type": "string"}, "starttls": {"type": "boolean"}, @@ -34,7 +35,7 @@ ERROR_REPORT_CONFIG_SCHEMA = { {"type": "array", "items": {"type": "string"}, "minItems": 1}, ] }, - "influx-db-measurement": { + "influx-db-params": { "type": "object", "properties": { "ssl": {"type": "boolean"}, @@ -64,10 +65,16 @@ ERROR_REPORT_CONFIG_SCHEMA = { "items": {"type": "string", "format": "uri"}, "minItems": 1, }, - "influx": {"$ref": "#/definitions/influx-db-measurement"}, + "influx": {"$ref": "#/definitions/influx-db-params"}, "exclude-interfaces": { "type": "array", - "items": {"type": "string"}, + "items": { + "type": "array", + "prefixItems": [ + {"type": "string"}, + {"type": "string"}, + ], + }, }, }, "required": ["email", "influx"], diff --git a/brian_polling_manager/error_report/email.jinja2 b/brian_polling_manager/error_report/email.jinja2 deleted file mode 100644 index 90a06bfe30b8005a905b3a9b795cab7d2b436c94..0000000000000000000000000000000000000000 --- a/brian_polling_manager/error_report/email.jinja2 +++ /dev/null @@ -1,40 +0,0 @@ -{#- -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 diff --git a/brian_polling_manager/error_report/error_report.html.jinja2 b/brian_polling_manager/error_report/error_report.html.jinja2 index cb7371acbb375a023dc6fdc4262c9e3b6ff6a158..8314caecf84aa17562164ca25cbffa47061220d0 100644 --- a/brian_polling_manager/error_report/error_report.html.jinja2 +++ b/brian_polling_manager/error_report/error_report.html.jinja2 @@ -1,36 +1,91 @@ -<html> -<body> -<pre> -{#- We mix tabs with spaces and have otherwise inconsistent whitespace to keep the - output as close as possible to the output of the original script -#} -{%- for ifc in interfaces %} -================================= -{{ ifc.router }} -================================= - {{ ifc.interface }} {{ ifc.description }} - {%- if ifc.diff %} - {%- for err, diff in ifc.diff.items() %} - {{ err }}{{ " " if err == "framing-errors" else "" }} {{ ifc.error_counters[err] }} Diff: {{ diff }} - {%- endfor %} - {%- else %} - {%- for err, val in ifc.error_counters.items() %} - {{ err }}{{ " " if err == "framing-errors" else "" }} {{ val }} - {%- endfor %} - {%- endif %} -{{ '' }} -{%- endfor %} +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <title>GÉANT Interface error report</title> + <style> + html { + font-family: monospace; + font-size: 12pt; + } + + section, footer { + padding: 20pt 0 0 20pt; + } + + .interface-block { + padding-bottom: 20pt; + } -{%- if excluded_interfaces %} -ROUTER,INTERFACE,FRAMING ERRORS,BIT ERROR SECONDS,ERRORED BLOCKS SECONDS,CRC ERRORS,TOTAL ERRORS,INPUT DISCARDS,INPUT DROPS,OUTPUT DROPS -{%- for ifc in excluded_interfaces %} -{{ifc.router}},{{ifc.interface}},{{ ifc.error_counters.values() | join(',') }},{{ifc.description}} -{%- endfor %} -{%- endif %} + .interface-errors { + padding-left: 20pt; + } + ul { + list-style-type: none; + } + h3, p, ul { + margin: 0pt; + } -Generated {{ date }} -</pre> -</body> + .error-table { + padding-left: 20pt; + } + + 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> diff --git a/brian_polling_manager/error_report/report.py b/brian_polling_manager/error_report/report.py index 3c1764a64498f0912950c7c079be04f241b84e39..946d4594491216b47efe499ce6ff6968b4051cff 100644 --- a/brian_polling_manager/error_report/report.py +++ b/brian_polling_manager/error_report/report.py @@ -1,9 +1,11 @@ +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 pathlib import smtplib import jinja2 -import hashlib -import base64 THIS_DIR = pathlib.Path(__file__).parent logger = logging.getLogger(__name__) @@ -18,32 +20,43 @@ def render_html(errors, date): :param errors: an instance of `PROCESSED_ERROR_COUNTERS_SCHEMA` :param template_file: a jinja2 template to render """ - env = jinja2.Environment( - # 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", - ) + env = jinja2.Environment(loader=jinja2.FileSystemLoader(THIS_DIR)) template = env.get_template("error_report.html.jinja2") return template.render(**errors, date=date) 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") - html_as_bytes = html.encode() - md5_hash = hashlib.md5(html_as_bytes).hexdigest() - b64_report = base64.b64encode(html_as_bytes).decode() - return template.render( - config=email_config, subject=subject, md5_hash=md5_hash, b64_report=b64_report - ) + + body = "The latest errors report is attached." + + message = MIMEMultipart() + message["From"] = email_config["from"] + message["To"] = "; ".join(email_config["to"]) + 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): 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: if config.get("starttls"): server.starttls() diff --git a/test/error_report/data/test_render_html-expected.html b/test/error_report/data/test_render_html-expected.html index 3c6cdeda8b64e04fe798180d90c6b2047cf9ff3c..3a0a88538edef7819b70efcdac582dfd1458fabd 100644 --- a/test/error_report/data/test_render_html-expected.html +++ b/test/error_report/data/test_render_html-expected.html @@ -1,23 +1,86 @@ -<html> -<body> -<pre> -================================= -mx1.ams.nl.geant.net -================================= - ae1 PHY blah blah - framing-errors 4 Diff: 2 - input-drops 2 Diff: 1 +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <title>GÉANT Interface error report</title> + <style> + html { + font-family: monospace; + font-size: 12pt; + } -================================= -mx1.fra.de.geant.net -================================= - ae10 PHY blah blah foo - input-drops 3 + section, footer { + padding: 20pt 0 0 20pt; + } + .interface-block { + padding-bottom: 20pt; + } + .interface-errors { + padding-left: 20pt; + } + ul { + list-style-type: none; + } -Generated <some date> -</pre> -</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>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> \ No newline at end of file diff --git a/test/error_report/data/test_render_html_with_exclusions-expected.html b/test/error_report/data/test_render_html_with_exclusions-expected.html index bcd4e358a74e3f8cdc4c5a38862be7731845e9b7..2369a6fc18b5575f8cf77c68631943946a5c2d72 100644 --- a/test/error_report/data/test_render_html_with_exclusions-expected.html +++ b/test/error_report/data/test_render_html_with_exclusions-expected.html @@ -1,18 +1,72 @@ -<html> -<body> -<pre> -================================= -mx1.ams.nl.geant.net -================================= - ae1 PHY blah blah - input-drops 2 +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <title>GÉANT Interface error report</title> + <style> + 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 -mx1.fra.de.geant.net,ae10,1,2,3,4,5,6,7,8,PHY blah blah foo + section, footer { + padding: 20pt 0 0 20pt; + } + .interface-block { + padding-bottom: 20pt; + } + .interface-errors { + padding-left: 20pt; + } -Generated <some date> -</pre> -</body> + ul { + list-style-type: none; + } + + 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> \ No newline at end of file diff --git a/test/error_report/test_error_report.py b/test/error_report/test_error_report.py index 08e1dee23792b3b16c7d94cfef7e55dedea172da..4865948f1bfb47730207e3c9356e998a5d683573 100644 --- a/test/error_report/test_error_report.py +++ b/test/error_report/test_error_report.py @@ -1,5 +1,4 @@ import base64 -import hashlib import json import pathlib @@ -161,7 +160,7 @@ def test_validate_config(tmp_path): "username": "some-username", "password": "user-password", }, - "exclude-interfaces": ["SOME DESCRIPTION PART"], + "exclude-interfaces": [["some.router", "some/ifc"]], } config_file.write_text(json.dumps(content)) result = config.load(config_file) @@ -184,7 +183,7 @@ def test_validate_config(tmp_path): "username": "some-username", "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): @pytest.mark.parametrize( - "description, exclusions, is_excluded", + "router, interface, exclusions, is_excluded", [ - ("DESC", [], False), - ("DESC", ["DESC"], True), - ("DESC", ["desc"], True), - ("DESC", ["DESCMORE"], False), - ("DESC", ["MORE", "DESC"], True), - ("", ["DESC"], False), + ("rtr", "ifc", [], False), + ("rtr", "ifc", [("rtr", "ifc")], True), + ("rtr", "ifc", [("RTR", "IFC")], True), + ("rtr", "ifc", [("rtr", "ifc.2")], False), + ("rtr", "ifc", [("rtr", "ifc.2"), ("rtr", "ifc")], True), + ("rtr", "ifc", [("rtr2", "ifc")], False), ], ) -def test_excluded_interface(description, exclusions, is_excluded): - assert is_excluded_interface(description, exclusions) == is_excluded +def test_excluded_interface(router, interface, 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): @@ -287,8 +287,8 @@ def test_interface_errors_with_new_errors(create_error_point, get_interface_erro def test_interface_errors_with_multiple_interfaces( create_error_point, get_interface_errors ): - create_error_point("mx1.ams.nl.geant.net", "ae1", "today", framing_errors=1) - create_error_point("mx1.fra.de.geant.net", "ae10", "today", framing_errors=2) + 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=1) errors = get_interface_errors() assert errors["interfaces"] == [ { @@ -296,7 +296,7 @@ def test_interface_errors_with_multiple_interfaces( "interface": "ae1", "description": "PHY blah blah", "error_counters": { - "framing-errors": 1, + "framing-errors": 2, }, }, { @@ -304,7 +304,7 @@ def test_interface_errors_with_multiple_interfaces( "interface": "ae10", "description": "PHY blah blah foo", "error_counters": { - "framing-errors": 2, + "framing-errors": 1, }, }, ] @@ -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 ) # 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"] == [ { "router": "mx1.ams.nl.geant.net", @@ -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): create_error_point( "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): create_error_point("mx1.fra.de.geant.net", "ae10", "today", input_drops=3) 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 # to comply with flake8 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): @@ -452,35 +487,25 @@ def test_render_html_with_exclusions(create_error_point, get_interface_errors): "mx1.fra.de.geant.net", "ae10", "today", - # mess up order of kwargs to test re-ordering - bit_error_seconds=2, 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"]) - result = render_html(errors=errors, date="<some date>") + errors = get_interface_errors(exclusions=[("mx1.fra.de.geant.net", "ae10")]) + result = render_html(errors=errors, date="SOME_DATE") # The expected value contains mixed tabs and spaces. We put it in a separate file # to comply with flake8 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(): body = "<SOME_BODY>" - md5 = hashlib.md5(body.encode()).hexdigest() b64 = base64.b64encode(body.encode()).decode() config = {"from": "someone@geant.org", "to": ["someone.else@geant.org"]} result = render_email(config, html=body, subject="<subject>") assert "From: someone@geant.org" in result assert "To: someone.else@geant.org" in result - assert md5 in result assert b64 in result @@ -590,7 +615,7 @@ def config_file(tmp_path): "username": "some-username", "password": "user-password", }, - "exclude-interfaces": ["FOO"], + "exclude-interfaces": [("mx1.fra.de.geant.net", "ae10")], } path = tmp_path / "config.json" path.write_text(json.dumps(config)) @@ -623,9 +648,9 @@ def test_e2e( 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() assert "mx1.ams.nl.geant.net" in report assert "ae1" in report - assert "input-drops\t1" in report + assert "<td>input-drops</td>" in report