Skip to content

Commit 964e6f0

Browse files
Merge pull request #125 from networktocode/gfm-issue-115
Make 'status' optional, accept iCal notifications with no 'sequence' either
2 parents 6f01221 + 6d58d74 commit 964e6f0

File tree

8 files changed

+68
-9
lines changed

8 files changed

+68
-9
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Fixed
66

7+
- #115 - Add default `status` and `sequence` values for iCal notifications missing these fields
78
- #124 - Handle encoded non-ASCII characters in email subjects.
89
- #126 - Ignore a class of non-maintenance-notification emails from Telia.
910

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,24 @@ You can leverage this library in your automation framework to process circuit ma
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 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.
2928
- **start**: timestamp that defines the start date of the maintenance in GMT.
3029
- **end**: timestamp that defines the end date of the maintenance in GMT.
3130
- **stamp**: timestamp that defines the update date of the maintenance in GMT.
3231
- **organizer**: defines the contact information included in the original notification.
3332

33+
and may additionally contain the following attributes:
34+
35+
- **status**: defines the overall status or confirmation for the maintenance.¹
36+
- **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-iCal parsed notifications.²
37+
- **summary**: human-readable details about this maintenance notification.
38+
- **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-iCal parsed notifications.
39+
3440
> Please, refer to the [BCOP](https://github.com/jda/maintnote-std/blob/master/standard.md) to more details about these attributes.
3541
42+
¹ Per the BCOP, **status** (`X-MAINTNOTE_STATUS`) is an optional field in notifications. However, a `Maintenance` object will always contain a `status` value; in the case where the source 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.
43+
44+
² Per the BCOP, **sequence** is a mandatory field in 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`.
45+
3646
## Workflow
3747

3848
1. We instantiate a `Provider`, directly or via the `init_provider` method, that depending on the selected type will return the corresponding instance.

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,20 +126,20 @@ 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
138140

139141
# Non mandatory attributes
142+
status: Status = Status.NO_CHANGE
140143
uid: StrictStr = "0"
141144
sequence: StrictInt = 1
142145
summary: StrictStr = ""

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"}

tests/unit/data/ical/ical6

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
BEGIN:VCALENDAR
2+
VERSION:2.0
3+
PRODID:Data::ICal 0.22
4+
BEGIN:VEVENT
5+
DESCRIPTION:NTT (AS2914) Regular Maintenance Notification: completed [GIN-C
6+
HG0123456]\nPlease refer to the email notification for more details regard
7+
ing maintenance or outage reason and impact.
8+
DTEND:20211115T205700Z
9+
DTSTAMP:20211115T205904Z
10+
DTSTART:20211115T180000Z
11+
SUMMARY:NTT (AS2914) Regular Maintenance Notification: completed [GIN-CHG01
12+
23456]
13+
UID:GIN-CHG0040894
14+
X-MAINTNOTE-ACCOUNT:47004321
15+
X-MAINTNOTE-IMPACT:OUTAGE
16+
X-MAINTNOTE-MAINTENANCE-ID:GIN-CHG0123456
17+
X-MAINTNOTE-OBJECT-ID;X-MAINTNOTE-OBJECT-IMPACT=OUTAGE:246810
18+
X-MAINTNOTE-PROVIDER:NTT (AS2914)
19+
END:VEVENT
20+
END:VCALENDAR
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[
2+
{
3+
"account": "47004321",
4+
"circuits": [
5+
{
6+
"circuit_id": "246810",
7+
"impact": "OUTAGE"
8+
}
9+
],
10+
"end": 1637009820,
11+
"maintenance_id": "GIN-CHG0123456",
12+
"provider": "NTT (AS2914)",
13+
"sequence": -1,
14+
"stamp": 1637009944,
15+
"start": 1636999200,
16+
"status": "NO-CHANGE",
17+
"summary": "NTT (AS2914) Regular Maintenance Notification: completed [GIN-CHG0123456]",
18+
"uid": "GIN-CHG0040894"
19+
}
20+
]

tests/unit/test_output.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@
3535
("provider", None, ValidationError),
3636
("provider", 1, ValidationError),
3737
("provider", "good", None),
38-
("status", None, ValidationError),
38+
# Non mandatory attributes
39+
("status", None, None),
3940
("status", 1, ValidationError),
4041
("status", "good", ValidationError),
4142
("status", "IN-PROCESS", None),
42-
# Non mandatory attributes
4343
("sequence", None, None),
4444
("sequence", "1", ValidationError),
4545
("sequence", 1, None),

tests/unit/test_parsers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
(ICal, Path(dir_path, "data", "ical", "ical3"), Path(dir_path, "data", "ical", "ical3_result.json"),),
4444
(ICal, Path(dir_path, "data", "ical", "ical4"), Path(dir_path, "data", "ical", "ical4_result.json"),),
4545
(ICal, Path(dir_path, "data", "ical", "ical5"), Path(dir_path, "data", "ical", "ical5_result.json"),),
46+
(ICal, Path(dir_path, "data", "ical", "ical6"), Path(dir_path, "data", "ical", "ical6_result.json"),),
4647
# AquaComms
4748
(
4849
HtmlParserAquaComms1,

0 commit comments

Comments
 (0)