Skip to content

Commit b7e90be

Browse files
Merge pull request #129 from networktocode/develop
Release v2.0.8
2 parents 0bf1a3c + 6c93c08 commit b7e90be

25 files changed

+1389
-46
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## v2.0.8 - 2021-12-09
4+
5+
### Fixed
6+
7+
- #115 - Add default `status` and `sequence` values for iCal notifications missing these fields
8+
- #124 - Handle encoded non-ASCII characters in email subjects.
9+
- #126 - Ignore a class of non-maintenance-notification emails from Telia.
10+
- #127 - Improve handling of Equinix and Lumen notifications.
11+
- #128 - Add capability to set `RE-SCHEDULED` status for Verizon rescheduled notifications.
12+
313
## v2.0.7 - 2021-12-01
414

515
### Fixed

README.md

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,26 @@ during a NANOG meeting that aimed to promote the usage of the iCalendar format.
1919
proposed iCalendar format, the parser is straight-forward and there is no need to define custom logic, but this library
2020
enables supporting other providers that are not using this proposed practice, getting the same outcome.
2121

22-
You can leverage this library in your automation framework to process circuit maintenance notifications, and use the standardized [`Maintenance`](https://github.com/networktocode/circuit-maintenance-parser/blob/develop/circuit_maintenance_parser/output.py) to handle your received circuit maintenance notifications in a simple way. Every `maintenance` object contains, at least, the following attributes:
22+
You can leverage this library in your automation framework to process circuit maintenance notifications, and use the standardized [`Maintenance`](https://github.com/networktocode/circuit-maintenance-parser/blob/develop/circuit_maintenance_parser/output.py) model to handle your received circuit maintenance notifications in a simple way. Every `Maintenance` object contains the following attributes:
2323

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.
26-
- **maintenance_id**: contains text that uniquely identifies the maintenance that is the subject of the notification.
26+
- **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.
2727
- **circuits**: list of circuits affected by the maintenance notification and their specific impact.
28-
- **status**: defines the overall status or confirmation for the maintenance.
29-
- **start**: timestamp that defines the start date of the maintenance in GMT.
30-
- **end**: timestamp that defines the end date of the maintenance in GMT.
31-
- **stamp**: timestamp that defines the update date of the maintenance in GMT.
28+
- **start**: timestamp that defines the starting date/time of the maintenance in GMT.
29+
- **end**: timestamp that defines the ending date/time of the maintenance in GMT.
30+
- **stamp**: timestamp that defines the update date/time of the maintenance in GMT.
3231
- **organizer**: defines the contact information included in the original notification.
32+
- **status**: defines the overall status or confirmation for the maintenance.¹
33+
- **summary**: human-readable details about this maintenance notification. May be an empty string.
34+
- **sequence**: a sequence number for notifications involving this maintenance window. In practice this is generally redundant with the **stamp** field, and will be defaulted to `1` for most non-iCalendar parsed notifications.²
35+
- **uid**: a unique (?) identifer for a thread of related notifications. In practice this is generally redundant with the **maintenance_id** field, and will be defaulted to `0` for most non-iCalendar parsed notifications.
3336

34-
> Please, refer to the [BCOP](https://github.com/jda/maintnote-std/blob/master/standard.md) to more details about these attributes.
37+
> Please, refer to the [BCOP](https://github.com/jda/maintnote-std/blob/master/standard.md) to more details about the standardized meaning of these attributes.
38+
39+
¹ Per the BCOP, **status** (`X-MAINTNOTE_STATUS`) is an optional field in iCalendar notifications. However, a `Maintenance` object will always contain a `status` value; in the case where an iCalendar notification omits this field, the `status` will be set to `"NO-CHANGE"`, and it's up to the consumer of this library to determine how to appropriately handle this case. Parsers of other notification formats are responsible for setting an appropriate value for this field based on the notification contents, and may or may not include `"NO-CHANGE"` as one of the possible reported values.
40+
41+
² Per the BCOP, **sequence** is a mandatory field in iCalendar notifications. However, some NSPs have been seen to send notifications which, while otherwise consistent with the BCOP, omit the `SEQUENCE` field; in such cases, this library will report a sequence number of `-1`.
3542

3643
## Workflow
3744

circuit_maintenance_parser/data.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,16 @@ def init_from_emailmessage(cls: Type["NotificationData"], email_message) -> Opti
7575
cls.walk_email(email_message, data_parts)
7676

7777
# Adding extra headers that are interesting to be parsed
78-
data_parts.add(DataPart(EMAIL_HEADER_SUBJECT, email_message["Subject"].encode()))
78+
data_parts.add(
79+
DataPart(
80+
EMAIL_HEADER_SUBJECT,
81+
# decode_header() handles conversion from RFC2047 ASCII representation of non-ASCII content to
82+
# a list of (string, charset) tuples.
83+
# make_header() merges these back into a single Header object containing this text
84+
# str() gets the simple Unicode representation of the Header.
85+
str(email.header.make_header(email.header.decode_header(email_message["Subject"]))).encode(),
86+
)
87+
)
7988
data_parts.add(DataPart(EMAIL_HEADER_DATE, email_message["Date"].encode()))
8089
# Ensure the data parts are processed in a consistent order
8190
return cls(data_parts=sorted(data_parts, key=lambda part: part.type))

circuit_maintenance_parser/output.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ class Status(str, Enum):
3737
- "CONFIRMED": Indicates maintenance event is definite.
3838
- "CANCELLED": Indicates maintenance event was cancelled.
3939
- "IN-PROCESS": Indicates maintenance event is in process (e.g. open).
40-
- "COMPLETED":Indicates maintenance event completed (e.g. closed).
40+
- "COMPLETED": Indicates maintenance event completed (e.g. closed).
4141
- "RE-SCHEDULED": Indicates maintenance event was re-scheduled.
42+
- "NO-CHANGE": Indicates status is unchanged from a previous notification (dummy value)
4243
"""
4344

4445
TENTATIVE = "TENTATIVE"
@@ -48,6 +49,8 @@ class Status(str, Enum):
4849
COMPLETED = "COMPLETED"
4950
RE_SCHEDULED = "RE-SCHEDULED"
5051

52+
NO_CHANGE = "NO-CHANGE"
53+
5154

5255
class CircuitImpact(BaseModel, extra=Extra.forbid):
5356
"""CircuitImpact class.
@@ -96,13 +99,13 @@ class Maintenance(BaseModel, extra=Extra.forbid):
9699
account: identifies an account associated with the service that is the subject of the maintenance notification
97100
maintenance_id: contains text that uniquely identifies the maintenance that is the subject of the notification
98101
circuits: list of circuits affected by the maintenance notification and their specific impact
99-
status: defines the overall status or confirmation for the maintenance
100102
start: timestamp that defines the start date of the maintenance in GMT
101103
end: timestamp that defines the end date of the maintenance in GMT
102104
stamp: timestamp that defines the update date of the maintenance in GMT
103105
organizer: defines the contact information included in the original notification
104106
105107
Optional attributes:
108+
status: defines the overall status or confirmation for the maintenance
106109
summary: description of the maintenace notification
107110
uid: specific unique identifier for each notification
108111
sequence: sequence number - initially zero - to serialize updates in case they are received or processed out of
@@ -123,18 +126,18 @@ class Maintenance(BaseModel, extra=Extra.forbid):
123126
... summary="This is a maintenance notification",
124127
... uid="1111",
125128
... )
126-
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'>)], status=<Status.COMPLETED: 'COMPLETED'>, start=1533704400, end=1533712380, stamp=1533595768, organizer='[email protected]', uid='1111', sequence=1, summary='This is a maintenance notification')
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='[email protected]', status=<Status.COMPLETED: 'COMPLETED'>, uid='1111', sequence=1, summary='This is a maintenance notification')
127130
"""
128131

129132
provider: StrictStr
130133
account: StrictStr
131134
maintenance_id: StrictStr
132135
circuits: List[CircuitImpact]
133-
status: Status
134136
start: StrictInt
135137
end: StrictInt
136138
stamp: StrictInt
137139
organizer: StrictStr
140+
status: Status
138141

139142
# Non mandatory attributes
140143
uid: StrictStr = "0"

circuit_maintenance_parser/parser.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,14 +110,18 @@ def parse_ical(gcal: Calendar) -> List[Dict]:
110110
"provider": str(component.get("X-MAINTNOTE-PROVIDER")),
111111
"account": str(component.get("X-MAINTNOTE-ACCOUNT")),
112112
"maintenance_id": str(component.get("X-MAINTNOTE-MAINTENANCE-ID")),
113-
"status": Status(component.get("X-MAINTNOTE-STATUS")),
113+
# status may be omitted, per the BCOP
114+
"status": Status(component.get("X-MAINTNOTE-STATUS", "NO-CHANGE")),
114115
"start": round(component.get("DTSTART").dt.timestamp()),
115116
"end": round(component.get("DTEND").dt.timestamp()),
116117
"stamp": round(component.get("DTSTAMP").dt.timestamp()),
117118
"summary": str(component.get("SUMMARY")),
118119
"organizer": str(component.get("ORGANIZER")),
119120
"uid": str(component.get("UID")),
120-
"sequence": int(component.get("SEQUENCE")),
121+
# per the BCOP sequence is mandatory, but we have real examples where it's omitted,
122+
# usually in combination with an omitted status. In that case let's treat the sequence as -1,
123+
# i.e. older than all known updates.
124+
"sequence": int(component.get("SEQUENCE", -1)),
121125
}
122126

123127
data = {key: value for key, value in data.items() if value != "None"}

circuit_maintenance_parser/parsers/equinix.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,15 @@ def parse_subject(self, subject: str) -> List[Dict]:
112112
if maintenance_id:
113113
data["maintenance_id"] = maintenance_id[1]
114114
data["summary"] = subject.strip().replace("\n", "")
115-
if "COMPLETED" in subject:
115+
if "completed" in subject.lower():
116116
data["status"] = Status.COMPLETED
117-
if "SCHEDULED" in subject or "REMINDER" in subject:
117+
elif "rescheduled" in subject.lower():
118+
data["status"] = Status.RE_SCHEDULED
119+
elif "scheduled" in subject.lower() or "reminder" in subject.lower():
118120
data["status"] = Status.CONFIRMED
121+
else:
122+
# Some Equinix notifications don't clearly state a status in their subject.
123+
# From inspection of examples, it looks like "Confirmed" would be the most appropriate in this case.
124+
data["status"] = Status.CONFIRMED
125+
119126
return [data]

circuit_maintenance_parser/parsers/lumen.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def parse_tables(self, tables: ResultSet, data: Dict):
9393
data["status"] = Status("COMPLETED")
9494
elif status_string == "Postponed":
9595
data["status"] = Status("RE-SCHEDULED")
96-
elif status_string == "Not Completed":
96+
elif status_string in ["Not Completed", "Cancelled"]:
9797
data["status"] = Status("CANCELLED")
9898
elif status_string == "Alternate Night":
9999
data["status"] = Status("RE-SCHEDULED")

circuit_maintenance_parser/parsers/verizon.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,14 @@ def parse_tables(self, tables: ResultSet, data: Dict): # pylint: disable=too-ma
4242
if not cells_text:
4343
continue
4444
if cells_text[0].startswith("Description of Maintenance"):
45-
data["summary"] = cells_text[1]
45+
data["summary"] = cells_text[1].replace("&nbsp;", "")
4646
elif cells_text[0].startswith("Verizon MASTARS Request number:"):
4747
data["maintenance_id"] = cells_text[1]
4848
elif cells_text[0].startswith("Attention:"):
4949
if "maintenance was not completed" in cells_text[0]:
5050
data["status"] = Status("CANCELLED")
51+
elif "request has been rescheduled" in cells_text[0]:
52+
data["status"] = Status("RE-SCHEDULED")
5153
elif cells_text[0].startswith("Maintenance Date/Time (GMT):"):
5254
maintenance_time = cells_text[1].split("-")
5355
start = parser.parse(maintenance_time[0].strip())

circuit_maintenance_parser/provider.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,8 @@ class Sparkle(GenericProvider):
301301
class Telia(GenericProvider):
302302
"""Telia provider custom class."""
303303

304+
_exclude_filter = {EMAIL_HEADER_SUBJECT: ["Disturbance Information"]}
305+
304306
_default_organizer = "[email protected]"
305307

306308

0 commit comments

Comments
 (0)