Skip to content

Commit e611c91

Browse files
authored
Handle broken EUNetworks cancellation messages (#243)
* Handle broken EUNetworks cancellation messages * Code formatting should fit black's constraints * Rework to allow empty circuit lists in cancelled notifications without a circuit list
1 parent a5dee28 commit e611c91

9 files changed

Lines changed: 135 additions & 12 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ You can leverage this library in your automation framework to process circuit ma
2424
- **provider**: identifies the provider of the service that is the subject of the maintenance notification.
2525
- **account**: identifies an account associated with the service that is the subject of the maintenance notification.
2626
- **maintenance_id**: contains text that uniquely identifies (at least within the context of a specific provider) the maintenance that is the subject of the notification.
27-
- **circuits**: list of circuits affected by the maintenance notification and their specific impact.
27+
- **circuits**: list of circuits affected by the maintenance notification and their specific impact. Note that in a maintenance cancelled notification, some providers omit the circuit list, so this may be blank for maintenance notifications with a status of CANCELLED.
2828
- **start**: timestamp that defines the starting date/time of the maintenance in GMT.
2929
- **end**: timestamp that defines the ending date/time of the maintenance in GMT.
3030
- **stamp**: timestamp that defines the update date/time of the maintenance in GMT.

circuit_maintenance_parser/output.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,15 @@ class Maintenance(BaseModel, extra=Extra.forbid):
9898
provider: identifies the provider of the service that is the subject of the maintenance notification
9999
account: identifies an account associated with the service that is the subject of the maintenance notification
100100
maintenance_id: contains text that uniquely identifies the maintenance that is the subject of the notification
101-
circuits: list of circuits affected by the maintenance notification and their specific impact
101+
circuits: list of circuits affected by the maintenance notification and their specific impact. Note this can be
102+
an empty list for notifications with a CANCELLED status if the provider does not populate the circuit list.
103+
status: defines the overall status or confirmation for the maintenance
102104
start: timestamp that defines the start date of the maintenance in GMT
103105
end: timestamp that defines the end date of the maintenance in GMT
104106
stamp: timestamp that defines the update date of the maintenance in GMT
105107
organizer: defines the contact information included in the original notification
106108
107109
Optional attributes:
108-
status: defines the overall status or confirmation for the maintenance
109110
summary: description of the maintenace notification
110111
uid: specific unique identifier for each notification
111112
sequence: sequence number - initially zero - to serialize updates in case they are received or processed out of
@@ -126,18 +127,18 @@ class Maintenance(BaseModel, extra=Extra.forbid):
126127
... summary="This is a maintenance notification",
127128
... uid="1111",
128129
... )
129-
Maintenance(provider='A random NSP', account='12345000', maintenance_id='VNOC-1-99999999999', circuits=[CircuitImpact(circuit_id='123', impact=<Impact.NO_IMPACT: 'NO-IMPACT'>), CircuitImpact(circuit_id='456', impact=<Impact.OUTAGE: 'OUTAGE'>)], start=1533704400, end=1533712380, stamp=1533595768, organizer='myemail@example.com', status=<Status.COMPLETED: 'COMPLETED'>, uid='1111', sequence=1, summary='This is a maintenance notification')
130+
Maintenance(provider='A random NSP', account='12345000', maintenance_id='VNOC-1-99999999999', status=<Status.COMPLETED: 'COMPLETED'>, circuits=[CircuitImpact(circuit_id='123', impact=<Impact.NO_IMPACT: 'NO-IMPACT'>), CircuitImpact(circuit_id='456', impact=<Impact.OUTAGE: 'OUTAGE'>)], start=1533704400, end=1533712380, stamp=1533595768, organizer='myemail@example.com', uid='1111', sequence=1, summary='This is a maintenance notification')
130131
"""
131132

132133
provider: StrictStr
133134
account: StrictStr
134135
maintenance_id: StrictStr
136+
status: Status
135137
circuits: List[CircuitImpact]
136138
start: StrictInt
137139
end: StrictInt
138140
stamp: StrictInt
139141
organizer: StrictStr
140-
status: Status
141142

142143
# Non mandatory attributes
143144
uid: StrictStr = "0"
@@ -160,9 +161,9 @@ def validate_empty_strings(cls, value):
160161
return value
161162

162163
@validator("circuits")
163-
def validate_empty_circuits(cls, value):
164-
"""Validate emptry strings."""
165-
if len(value) < 1:
164+
def validate_empty_circuits(cls, value, values):
165+
"""Validate non-cancel notifications have a populated circuit list."""
166+
if len(value) < 1 and values["status"] != "CANCELLED":
166167
raise ValueError("At least one circuit has to be included in the maintenance")
167168
return value
168169

circuit_maintenance_parser/parser.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,11 @@ def parse_ical(gcal: Calendar) -> List[Dict]:
127127
data = {key: value for key, value in data.items() if value != "None"}
128128

129129
# In a VEVENT sometimes there are mutliple object ID with custom impacts
130-
circuits = component.get("X-MAINTNOTE-OBJECT-ID")
130+
# In addition, while circuits should always be populated according to the BCOP, sometimes
131+
# they are not in the real world, at least in maintenances with a CANCELLED status. Thus
132+
# we allow empty circuit lists, but will validate elsewhere that they are only empty in a
133+
# maintenance object with a CANCELLED status.
134+
circuits = component.get("X-MAINTNOTE-OBJECT-ID", [])
131135
if isinstance(circuits, list):
132136
data["circuits"] = [
133137
CircuitImpact(
@@ -136,7 +140,7 @@ def parse_ical(gcal: Calendar) -> List[Dict]:
136140
object.params.get("X-MAINTNOTE-OBJECT-IMPACT", component.get("X-MAINTNOTE-IMPACT"))
137141
),
138142
)
139-
for object in component.get("X-MAINTNOTE-OBJECT-ID")
143+
for object in component.get("X-MAINTNOTE-OBJECT-ID", [])
140144
]
141145
else:
142146
data["circuits"] = [
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
Delivered-To: example@example.com
2+
Return-Path: <random@example.com>
3+
Date: Fri, 6 Oct 2023 09:57:00 +0000 (GMT)
4+
From: Maintenance <maintenance@eunetworks.com>
5+
To: user@example.com"
6+
Message-ID: <aba@example.net>
7+
Subject: CANCELLED euNetworks Emergency Work: 02345678
8+
MIME-Version: 1.0
9+
Content-Type: multipart/mixed;
10+
boundary="----=_Part_9999_1445545000.1696586220000"
11+
X-Priority: 3
12+
X-Sender: postmaster@salesforce.com
13+
X-mail_abuse_inquiries: http://www.salesforce.com/company/abuse.jsp
14+
Reply-To: Maintenance <maintenance@eunetworks.com>
15+
16+
------=_Part_9999_1445545000.1696586220000
17+
Content-Type: multipart/alternative;
18+
boundary="----=_Part_9998_1422331000.1696586220000"
19+
20+
------=_Part_9998_1422331000.1696586220000
21+
Content-Type: text/plain; charset=ISO-8859-1
22+
Content-Transfer-Encoding: quoted-printable
23+
24+
Maintenance Announcement
25+
Dear Customer,
26+
27+
Please be advised that the previously announced works have been cancelled. =
28+
If an alternative date should be required, this will be announced in time.
29+
30+
TRIMMED STUFF HERE.
31+
32+
33+
ref:00D200000000A05.500N1000002uvs4:ref
34+
------=_Part_9998_1422331000.1696586220000
35+
Content-Type: text/html; charset=ISO-8859-1
36+
Content-Transfer-Encoding: quoted-printable
37+
38+
TRIMMED STUFF HERE
39+
------=_Part_9998_1422331000.1696586220000--
40+
41+
------=_Part_9999_1445545000.1696586220000
42+
Content-Type: text/calendar; name="X-02345678.ics"
43+
Content-Transfer-Encoding: 7bit
44+
Content-Disposition: attachment; filename="X-02345678.ics"
45+
46+
BEGIN:VCALENDAR
47+
VERSION:2.0
48+
PRODID:-//Microsoft Corporation//Outlook 11.0 MIMEDIR//EN
49+
BEGIN:VEVENT
50+
SUMMARY: Maintenance note
51+
DTSTART;VALUE=DATE-TIME:20231006T230000Z
52+
DTEND;VALUE=DATE-TIME:20231007T170000Z
53+
DTSTAMP;VALUE=DATE-TIME:20231006T095600Z
54+
UID:02345678@euNetworks.com
55+
SEQUENCE:2
56+
X-MAINTNOTE-PROVIDER:euNetworks.com
57+
X-MAINTNOTE-ACCOUNT:A Company
58+
X-MAINTNOTE-MAINTENANCE-ID:02345678
59+
X-MAINTNOTE-STATUS:CANCELLED
60+
ORGANIZER;CN=noc@eunetworks.com:mailto:noc@eunetworks.com
61+
END:VEVENT
62+
END:VCALENDAR
63+
------=_Part_9999_1445545000.1696586220000--
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[
2+
{
3+
"account": "A Company",
4+
"circuits": [],
5+
"end": 1696698000,
6+
"maintenance_id": "02345678",
7+
"organizer": "mailto:noc@eunetworks.com",
8+
"provider": "euNetworks.com",
9+
"sequence": 2,
10+
"stamp": 1696586160,
11+
"start": 1696633200,
12+
"status": "CANCELLED",
13+
"summary": " Maintenance note ",
14+
"uid": "02345678@euNetworks.com"
15+
}
16+
]

tests/unit/data/ical/ical7

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
BEGIN:VCALENDAR
2+
VERSION:2.0
3+
PRODID:-//Microsoft Corporation//Outlook 11.0 MIMEDIR//EN
4+
BEGIN:VEVENT
5+
SUMMARY:Maintenance note
6+
DTSTART;VALUE=DATE-TIME:20231006T230000Z
7+
DTEND;VALUE=DATE-TIME:20231007T170000Z
8+
DTSTAMP;VALUE=DATE-TIME:20231006T095600Z
9+
UID:02345678@euNetworks.com
10+
SEQUENCE:2
11+
X-MAINTNOTE-PROVIDER:euNetworks.com
12+
X-MAINTNOTE-ACCOUNT:Bogus Corp
13+
X-MAINTNOTE-MAINTENANCE-ID:02345678
14+
X-MAINTNOTE-IMPACT:OUTAGE
15+
X-MAINTNOTE-STATUS:CANCELLED
16+
ORGANIZER;CN=noc@eunetworks.com:mailto:noc@eunetworks.com
17+
END:VEVENT
18+
END:VCALENDAR
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[
2+
{
3+
"account": "Bogus Corp",
4+
"circuits": [],
5+
"end": 1696698000,
6+
"maintenance_id": "02345678",
7+
"organizer": "mailto:noc@eunetworks.com",
8+
"provider": "euNetworks.com",
9+
"sequence": 2,
10+
"stamp": 1696586160,
11+
"start": 1696633200,
12+
"status": "CANCELLED",
13+
"summary": "Maintenance note",
14+
"uid": "02345678@euNetworks.com"
15+
}
16+
]

tests/unit/test_e2e.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,10 +274,10 @@
274274
(
275275
EUNetworks,
276276
[
277-
("ical", GENERIC_ICAL_DATA_PATH),
277+
("email", Path(dir_path, "data", "eunetworks", "eunetworks_cancel.eml")),
278278
],
279279
[
280-
GENERIC_ICAL_RESULT_PATH,
280+
Path(dir_path, "data", "eunetworks", "eunetworks_cancel.json"),
281281
],
282282
),
283283
# EXA / GTT

tests/unit/test_parsers.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@
6868
Path(dir_path, "data", "ical", "ical6"),
6969
Path(dir_path, "data", "ical", "ical6_result.json"),
7070
),
71+
(
72+
ICal,
73+
Path(dir_path, "data", "ical", "ical7"),
74+
Path(dir_path, "data", "ical", "ical7_result.json"),
75+
),
7176
# AquaComms
7277
(
7378
HtmlParserAquaComms1,

0 commit comments

Comments
 (0)