Skip to content

Commit 2f89d32

Browse files
Merge pull request #92 from networktocode/develop
Version v2.0.3
2 parents 79fb880 + a3c66dd commit 2f89d32

23 files changed

+733
-106
lines changed

CHANGELOG.md

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

3+
## v2.0.3 - 2021-10-01
4+
5+
### Added
6+
7+
- #84 - New parser added for text. Added new provider `AWS` using `Text` and `EmailSubjectParser`
8+
- #91 - `Provider` now adds `_include_filter` and `_exclude_filter` attributes (using regex) to filter in and out notifications that are relevant to be parsed vs other that are not, avoiding false positives.
9+
10+
### Fixed
11+
12+
- #90 - Improved handling of Lumen scheduled maintenance notices
13+
314
## v2.0.2 - 2021-09-28
415

516
### Fixed

README.md

Lines changed: 155 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,31 @@ 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 on this library in your automation framework to process circuit maintenance notifications, and use the standarised output to handle your received circuit maintenance notifications in a simple way.
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:
23+
24+
- **provider**: identifies the provider of the service that is the subject of the maintenance notification.
25+
- **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.
27+
- **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.
32+
- **organizer**: defines the contact information included in the original notification.
33+
34+
> Please, refer to the [BCOP](https://github.com/jda/maintnote-std/blob/master/standard.md) to more details about these attributes.
2335
2436
## Workflow
2537

2638
1. We instantiate a `Provider`, directly or via the `init_provider` method, that depending on the selected type will return the corresponding instance.
27-
2. Each `Provider` have already defined multiple `Processors` that will be used to get the `Maintenances` when the `Provider.get_maintenances(data)` method is called.
28-
3. Each `Processor` class can have a pre defined logic to combine the data extracted from the notifications and create the final `Maintenance` object, and receives a `List` of multiple `Parsers` that will be to `parse` each type of data.
29-
4. Each `Parser` class supports one or more data types and implements the `Parser.parse()` method used to retrieve a `Dict` with the relevant key/values.
30-
5. When calling the `Provider.get_maintenances(data)`, the `data` argument is an instance of `NotificationData` (which is just a collection of multiple `DataParts`, each one with a `type` and a `content`) that will be used by the corresponding `Parser` when the `Processor` will try to match them.
39+
2. Get an instance of the `NotificationData` class. This instance groups together `DataParts` which each contain some content and a specific type (that will match a specific `Parser`). For example, a `NotificationData` might describe a received email message, with `DataParts` corresponding to the subject line and body of the email. There are factory methods to initialize a `NotificationData` describing a single chunk of binary data, as well as others to initialize one directly from a raw email message or `email.message.EmailMessage` instance.
40+
3. Each `Provider` uses one or more `Processors` that will be used to build `Maintenances` when the `Provider.get_maintenances(data)` method is called.
41+
4. Each `Processor` class uses one or more `Parsers` to process each type of data that it handles. It can have custom logic to combine the parsed data from multiple `Parsers` to create the final `Maintenance` object.
42+
5. Each `Parser` class supports one or a set of related data types, and implements the `Parser.parse()` method used to retrieve a `Dict` with the relevant keys/values.
43+
44+
<p align="center">
45+
<img src="https://raw.githubusercontent.com/nautobot/nautobot-plugin-circuit-maintenance/develop/docs/images/new_workflow.png" width="800" class="center">
46+
</p>
3147

3248
By default, there is a `GenericProvider` that support a `SimpleProcessor` using the standard `ICal` `Parser`, being the easiest path to start using the library in case the provider uses the reference iCalendar standard.
3349

@@ -43,6 +59,7 @@ By default, there is a `GenericProvider` that support a `SimpleProcessor` using
4359

4460
#### Supported providers based on other parsers
4561

62+
- AWS
4663
- AquaComms
4764
- Cogent
4865
- Colt
@@ -65,15 +82,39 @@ By default, there is a `GenericProvider` that support a `SimpleProcessor` using
6582
The library is available as a Python package in pypi and can be installed with pip:
6683
`pip install circuit-maintenance-parser`
6784

68-
## Usage
85+
## How to use it?
6986

70-
> Please, refer to the [BCOP](https://github.com/jda/maintnote-std/blob/master/standard.md) to understand the meaning
71-
> of the output attributes.
87+
The library requires two things:
7288

73-
## Python Library
89+
- The `notificationdata`: this is the data that the library will check to extract the maintenance notifications. It can be simple (only one data type and content, such as an iCalendar notification) or more complex (with multiple data parts of different types, such as from an email).
90+
- The `provider` identifier: used to select the proper `Provider` which contains the `processor` logic to take the proper `Parsers` and use the data that they extract. By default, the `GenericProvider` (used when no other provider type is defined) will support parsing of `iCalendar` notifications using the recommended format.
91+
92+
### Python Library
93+
94+
First step is to define the `Provider` that we will use to parse the notifications. As commented, there is a `GenericProvider` that implements the gold standard format and can be reused for any notification matching the expectations.
7495

7596
```python
76-
from circuit_maintenance_parser import init_provider, NotificationData
97+
from circuit_maintenance_parser import init_provider
98+
99+
generic_provider = init_provider()
100+
101+
type(generic_provider)
102+
<class 'circuit_maintenance_parser.provider.GenericProvider'>
103+
```
104+
105+
However, usually some `Providers` don't fully implement the standard and maybe some information is missing, for example the `organizer` email or maybe a custom logic to combine information is required, so we allow custom `Providers`:
106+
107+
```python
108+
ntt_provider = init_provider("ntt")
109+
110+
type(ntt_provider)
111+
<class 'circuit_maintenance_parser.provider.NTT'>
112+
```
113+
114+
Once we have the `Provider` ready, we need to initialize the data to process, we call it `NotificationData` and can be initialized from a simple content and type or from more complex structures, such as an email.
115+
116+
```python
117+
from circuit_maintenance_parser import NotificationData
77118

78119
raw_data = b"""BEGIN:VCALENDAR
79120
VERSION:2.0
@@ -97,93 +138,63 @@ END:VEVENT
97138
END:VCALENDAR
98139
"""
99140

100-
ntt_provider = init_provider("ntt")
101-
102141
data_to_process = NotificationData.init_from_raw("ical", raw_data)
103142

104-
maintenances = ntt_provider.get_maintenances(data_to_process)
143+
type(data_to_process)
144+
<class 'circuit_maintenance_parser.data.NotificationData'>
145+
```
146+
147+
Finally, with we retrieve the maintenances (it is a `List` because a notification can contain multiple maintenances) from the data calling the `get_maintenances` method from the `Provider` instance:
148+
149+
```python
150+
maintenances = generic_provider.get_maintenances(data_to_process)
105151

106152
print(maintenances[0].to_json())
107153
{
108-
"account": "137.035999173",
109-
"circuits": [
110-
{
111-
"circuit_id": "acme-widgets-as-a-service",
112-
"impact": "NO-IMPACT"
113-
},
114-
{
115-
"circuit_id": "acme-widgets-as-a-service-2",
116-
"impact": "OUTAGE"
117-
}
118-
],
119-
"end": 1444471200,
120-
"maintenance_id": "WorkOrder-31415",
121-
"organizer": "mailto:[email protected]",
122-
"provider": "example.com",
123-
"sequence": 1,
124-
"stamp": 1444435800,
125-
"start": 1444464000,
126-
"status": "TENTATIVE",
127-
"summary": "Maint Note Example",
128-
"uid": "42"
154+
"account": "137.035999173",
155+
"circuits": [
156+
{
157+
"circuit_id": "acme-widgets-as-a-service",
158+
"impact": "NO-IMPACT"
159+
},
160+
{
161+
"circuit_id": "acme-widgets-as-a-service-2",
162+
"impact": "OUTAGE"
163+
}
164+
],
165+
"end": 1444471200,
166+
"maintenance_id": "WorkOrder-31415",
167+
"organizer": "mailto:[email protected]",
168+
"provider": "example.com",
169+
"sequence": 1,
170+
"stamp": 1444435800,
171+
"start": 1444464000,
172+
"status": "TENTATIVE",
173+
"summary": "Maint Note Example",
174+
"uid": "42"
129175
}
130176
```
131177

132-
## CLI
178+
Notice that, either with the `GenericProvider` or `NTT` provider, we get the same result from the same data, because they are using exactly the same `Processor` and `Parser`. The only difference is that `NTT` notifications come without `organizer` and `provider` in the notification, and this info is fulfilled with some default values for the `Provider`, but in this case the original notification contains all the necessary information, so the defaults are not used.
133179

134-
```bash
135-
$ circuit-maintenance-parser --data-file tests/unit/data/ical/ical1 --data-type ical
136-
Circuit Maintenance Notification #0
137-
{
138-
"account": "137.035999173",
139-
"circuits": [
140-
{
141-
"circuit_id": "acme-widgets-as-a-service",
142-
"impact": "NO-IMPACT"
143-
}
144-
],
145-
"end": 1444471200,
146-
"maintenance_id": "WorkOrder-31415",
147-
"organizer": "mailto:[email protected]",
148-
"provider": "example.com",
149-
"sequence": 1,
150-
"stamp": 1444435800,
151-
"start": 1444464000,
152-
"status": "TENTATIVE",
153-
"summary": "Maint Note Example",
154-
"uid": "42"
155-
}
180+
```python
181+
ntt_maintenances = ntt_provider.get_maintenances(data_to_process)
182+
assert maintenances_ntt == maintenances
156183
```
157184

158-
```bash
159-
$ circuit-maintenance-parser --data-file tests/unit/data/zayo/zayo1.html --data-type html --provider-type zayo
160-
Circuit Maintenance Notification #0
161-
{
162-
"account": "clientX",
163-
"circuits": [
164-
{
165-
"circuit_id": "/OGYX/000000/ /ZYO /",
166-
"impact": "OUTAGE"
167-
}
168-
],
169-
"end": 1601035200,
170-
"maintenance_id": "TTN-00000000",
171-
"organizer": "[email protected]",
172-
"provider": "zayo",
173-
"sequence": 1,
174-
"stamp": 1599436800,
175-
"start": 1601017200,
176-
"status": "CONFIRMED",
177-
"summary": "Zayo will implement planned maintenance to troubleshoot and restore degraded span",
178-
"uid": "0"
179-
}
180-
```
185+
### CLI
186+
187+
There is also a `cli` entrypoint `circuit-maintenance-parser` which offers easy access to the library using few arguments:
188+
189+
- `data-file`: file storing the notification.
190+
- `data-type`: `ical`, `html` or `email`, depending on the data type.
191+
- `provider-type`: to choose the right `Provider`. If empty, the `GenericProvider` is used.
181192

182193
```bash
183194
circuit-maintenance-parser --data-file "/tmp/___ZAYO TTN-00000000 Planned MAINTENANCE NOTIFICATION___.eml" --data-type email --provider-type zayo
184195
Circuit Maintenance Notification #0
185196
{
186-
"account": "Linode",
197+
"account": "some account",
187198
"circuits": [
188199
{
189200
"circuit_id": "/OGYX/000000/ /ZYO /",
@@ -203,6 +214,68 @@ Circuit Maintenance Notification #0
203214
}
204215
```
205216

217+
## How to Extend the Library?
218+
219+
Even though the library aims to include support for as many providers as possible, it's likely that not all the thousands of NSP are supported and you may need to add support for some new one. Adding a new `Provider` is quite straightforward, and in the following example we are adding support for an imaginary provider, ABCDE, that uses HTML notifications.
220+
221+
First step is creating a new file: `circuit_maintenance_parser/parsers/abcde.py`. This file will contain all the custom parsers needed for the provider and it will import the base classes for each parser type from `circuit_maintenance_parser.parser`. In the example, we only need to import `Html` and in the child class implement the methods required by the class, in this case `parse_html()` which will return a `dict` with all the data that this `Parser` can extract. In this case we have to helper methods, `_parse_bs` and `_parse_tables` that implement the logic to navigate the notification data.
222+
223+
```python
224+
from typing import Dict
225+
import bs4 # type: ignore
226+
from bs4.element import ResultSet # type: ignore
227+
from circuit_maintenance_parser.parser import Html
228+
229+
class HtmlParserABCDE1(Html):
230+
def parse_html(self, soup: ResultSet) -> Dict:
231+
data = {}
232+
self._parse_bs(soup.find_all("b"), data)
233+
self._parse_tables(soup.find_all("table"), data)
234+
return [data]
235+
236+
def _parse_bs(self, btags: ResultSet, data: Dict):
237+
...
238+
239+
def _parse_tables(self, tables: ResultSet, data: Dict):
240+
...
241+
```
242+
243+
Next step is to create the new `Provider` by defining a new class in `circuit_maintenance_parser/provider.py`. This class that inherits from `GenericProvider` only needs to define two attributes:
244+
245+
- `_processors`: is a `list` of `Processor` instances that uses several data `Parsers`. In this example, we don't need to create a new custom `Processor` because the combined logic serves well (the most likely case), and we only need to use the new defined `HtmlParserABCDE1` and also the generic `EmailDateParser` that extract the email date. Also notice that you could have multiple `Processors` with different `Parsers` in this list, supporting several formats.
246+
- `_default_organizer`: this is a default helper to fill the `organizer` attribute in the `Maintenance` if the information is not part of the original notification.
247+
248+
```python
249+
class ABCDE(GenericProvider):
250+
_processors: List[GenericProcessor] = [
251+
CombinedProcessor(data_parsers=[EmailDateParser, HtmlParserABCDE1]),
252+
]
253+
_default_organizer = "[email protected]"
254+
```
255+
256+
And expose the new `Provider` in `circuit_maintenance_parser/__init__.py`:
257+
258+
```python
259+
from .provider import (
260+
GenericProvider,
261+
ABCDE,
262+
...
263+
)
264+
265+
SUPPORTED_PROVIDERS = (
266+
GenericProvider,
267+
ABCDE,
268+
...
269+
)
270+
```
271+
272+
Last, but not least, you should update the tests!
273+
274+
- Test the new `Parser` in `tests/unit/test_parsers.py`
275+
- Test the new `Provider` logic in `tests/unit/test_e2e.py`
276+
277+
... adding the necessary data samples in `tests/unit/data/abcde/`.
278+
206279
# Contributing
207280

208281
Pull requests are welcomed and automatically built and tested against multiple versions of Python through Travis CI.
@@ -225,6 +298,7 @@ The project is following Network to Code software development guidelines and is
225298
1. Define the `Parsers`(inheriting from some of the generic `Parsers` or a new one) that will extract the data from the notification, that could contain itself multiple `DataParts`. The `data_type` of the `Parser` and the `DataPart` have to match. The custom `Parsers` will be placed in the `parsers` folder.
226299
2. Update the `unit/test_parsers.py` with the new parsers, providing some data to test and validate the extracted data.
227300
3. Define a new `Provider` inheriting from the `GenericProvider`, defining the `Processors` and the respective `Parsers` to be used. Maybe you can reuse some of the generic `Processors` or maybe you will need to create a custom one. If this is the case, place it in the `processors` folder.
301+
- The `Provider` also supports the definition of a `_include_filter` and a `_exclude_filter` to limit the notifications that are actually processed, avoiding false positive errors for notification that are not relevant.
228302
4. Update the `unit/test_e2e.py` with the new provider, providing some data to test and validate the final `Maintenances` created.
229303
5. **Expose the new `Provider` class** updating the map `SUPPORTED_PROVIDERS` in `circuit_maintenance_parser/__init__.py` to officially expose the `Provider`.
230304

circuit_maintenance_parser/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from .provider import (
88
GenericProvider,
99
AquaComms,
10+
AWS,
1011
Cogent,
1112
Colt,
1213
EUNetworks,
@@ -29,6 +30,7 @@
2930
SUPPORTED_PROVIDERS = (
3031
GenericProvider,
3132
AquaComms,
33+
AWS,
3234
Cogent,
3335
Colt,
3436
EUNetworks,
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""Constants used in the library."""
2+
3+
EMAIL_HEADER_SUBJECT = "email-header-subject"
4+
EMAIL_HEADER_DATE = "email-header-date"

circuit_maintenance_parser/data.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import email
66
from pydantic import BaseModel, Extra
7+
from circuit_maintenance_parser.constants import EMAIL_HEADER_SUBJECT, EMAIL_HEADER_DATE
8+
79

810
logger = logging.getLogger(__name__)
911

@@ -73,9 +75,8 @@ def init_from_emailmessage(cls: Type["NotificationData"], email_message) -> Opti
7375
cls.walk_email(email_message, data_parts)
7476

7577
# Adding extra headers that are interesting to be parsed
76-
data_parts.add(DataPart("email-header-subject", email_message["Subject"].encode()))
77-
# TODO: Date could be used to extend the "Stamp" time of a notification when not available, but we need a parser
78-
data_parts.add(DataPart("email-header-date", email_message["Date"].encode()))
78+
data_parts.add(DataPart(EMAIL_HEADER_SUBJECT, email_message["Subject"].encode()))
79+
data_parts.add(DataPart(EMAIL_HEADER_DATE, email_message["Date"].encode()))
7980
return cls(data_parts=list(data_parts))
8081
except Exception: # pylint: disable=broad-except
8182
logger.exception("Error found initializing data from email message: %s", email_message)

0 commit comments

Comments
 (0)