Skip to content

Commit

Permalink
Feature/crown castle fiber (#259)
Browse files Browse the repository at this point in the history
* Add Crown Castle Fiber parser & provider

* Crown Castle unfortunately sends some completion notices w/o circuit IDs
This is similar to EUNetworks sending cancellation notices without
circuit IDs, see PR#243.

* Handle emergency maintenance notifications

* Get past pylint
  • Loading branch information
jmaslak authored Dec 28, 2023
1 parent 91fee6a commit 19279ed
Show file tree
Hide file tree
Showing 23 changed files with 1,741 additions and 3 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ You can leverage this library in your automation framework to process circuit ma
- **provider**: identifies the provider of the service that is the subject of the maintenance notification.
- **account**: identifies an account associated with the service that is the subject of the maintenance notification.
- **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.
- **circuits**: list of circuits affected by the maintenance notification and their specific impact. Note that in a maintenance canceled notification, some providers omit the circuit list, so this may be blank for maintenance notifications with a status of CANCELLED.
- **circuits**: list of circuits affected by the maintenance notification and their specific impact. Note that in a maintenance canceled or completed notification, some providers omit the circuit list, so this may be blank for maintenance notifications with a status of CANCELLED or COMPLETED.
- **start**: timestamp that defines the starting date/time of the maintenance in GMT.
- **end**: timestamp that defines the ending date/time of the maintenance in GMT.
- **stamp**: timestamp that defines the update date/time of the maintenance in GMT.
Expand Down Expand Up @@ -71,6 +71,7 @@ By default, there is a `GenericProvider` that supports a `SimpleProcessor` using
- BSO
- Cogent
- Colt
- Crown Castle Fiber
- Equinix
- EXA (formerly GTT)
- HGC
Expand Down
2 changes: 2 additions & 0 deletions circuit_maintenance_parser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
BSO,
Cogent,
Colt,
CrownCastle,
Equinix,
EUNetworks,
GTT,
Expand Down Expand Up @@ -39,6 +40,7 @@
BSO,
Cogent,
Colt,
CrownCastle,
Equinix,
EUNetworks,
GTT,
Expand Down
5 changes: 3 additions & 2 deletions circuit_maintenance_parser/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ class Maintenance(BaseModel, extra=Extra.forbid):
account: identifies an account associated with the service that is the subject of the maintenance notification
maintenance_id: contains text that uniquely identifies the maintenance that is the subject of the notification
circuits: list of circuits affected by the maintenance notification and their specific impact. Note this can be
an empty list for notifications with a CANCELLED status if the provider does not populate the circuit list.
an empty list for notifications with a CANCELLED or COMPLETED status if the provider does not populate the
circuit list.
status: defines the overall status or confirmation for the maintenance
start: timestamp that defines the start date of the maintenance in GMT
end: timestamp that defines the end date of the maintenance in GMT
Expand Down Expand Up @@ -184,7 +185,7 @@ def validate_empty_strings(cls, value):
@validator("circuits")
def validate_empty_circuits(cls, value, values):
"""Validate non-cancel notifications have a populated circuit list."""
if len(value) < 1 and values["status"] != "CANCELLED":
if len(value) < 1 and str(values["status"]) in ("CANCELLED", "COMPLETED"):
raise ValueError("At least one circuit has to be included in the maintenance")
return value

Expand Down
90 changes: 90 additions & 0 deletions circuit_maintenance_parser/parsers/crowncastle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""Crown Castle Fiber parser."""
import logging
import re
from datetime import datetime

from circuit_maintenance_parser.parser import Html, Impact, CircuitImpact, Status

# pylint: disable=too-many-nested-blocks, too-many-branches

logger = logging.getLogger(__name__)


class HtmlParserCrownCastle1(Html):
"""Notifications Parser for Crown Castle Fiber notifications."""

def parse_html(self, soup):
"""Execute parsing."""
data = {}
data["circuits"] = []

data["status"] = self.get_status(soup)

for paragraph in soup.find_all("p"):
for pstring in paragraph.strings:
search = re.match(r"^Dear (.*),", pstring)
if search:
data["account"] = search.group(1)

self.parse_strong(soup, data)

table = soup.find("table", "timezonegrid")
for row in table.find_all("tr"):
cols = row.find_all("td")
if len(cols) == 5:
if cols[4].string.strip() == "GMT":
st_dt = cols[0].string.strip() + " " + cols[1].string.strip() + " GMT"
en_dt = cols[2].string.strip() + " " + cols[3].string.strip() + " GMT"
data["start"] = self.dt2ts(datetime.strptime(st_dt, "%m/%d/%Y %I:%M %p %Z"))
data["end"] = self.dt2ts(datetime.strptime(en_dt, "%m/%d/%Y %I:%M %p %Z"))

table = soup.find("table", id="circuitgrid")
if table is not None:
for row in table.find_all("tr"):
cols = row.find_all("td")
if len(cols) == 6:
if cols[4].string.strip() == "None":
impact = Impact("NO-IMPACT")
else:
impact = Impact("OUTAGE")
data["circuits"].append(CircuitImpact(impact=impact, circuit_id=cols[0].string.strip()))

return [data]

def parse_strong(self, soup, data):
"""Parse the strong tags, to find summary and maintenance ID info."""
for strong in soup.find_all("strong"):
if strong.string.strip() == "Ticket Number:":
data["maintenance_id"] = strong.next_sibling.strip()
if strong.string.strip() == "Description:":
summary = strong.parent.next_sibling.next_sibling.contents[0].string.strip()
summary = re.sub(r"[\n\r]", "", summary)
data["summary"] = summary
if strong.string.strip().startswith("Work Description:"):
for sibling in strong.parent.next_siblings:
summary = "".join(sibling.strings)
summary = re.sub(r"[\n\r]", "", summary)
if summary != "":
data["summary"] = summary
break

def get_status(self, soup):
"""Get the status of the maintenance."""
for paragraph in soup.find_all("p"):
for pstring in paragraph.strings:
if "has been completed." in pstring:
return Status("COMPLETED")

for underline in soup.find_all("u"):
if underline.string is None:
continue
if underline.string.strip() == "Maintenance Notification":
return Status("CONFIRMED")
if underline.string.strip() == "Emergency Notification":
return Status("CONFIRMED")
if underline.string.strip() == "Maintenance Notification - Rescheduled Event":
return Status("RE-SCHEDULED")
if underline.string.strip() == "Maintenance Cancellation Notification":
return Status("CANCELLED")

return Status("NO-CHANGE")
10 changes: 10 additions & 0 deletions circuit_maintenance_parser/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from circuit_maintenance_parser.parsers.bso import HtmlParserBSO1
from circuit_maintenance_parser.parsers.cogent import HtmlParserCogent1, TextParserCogent1, SubjectParserCogent1
from circuit_maintenance_parser.parsers.colt import CsvParserColt1, SubjectParserColt1, SubjectParserColt2
from circuit_maintenance_parser.parsers.crowncastle import HtmlParserCrownCastle1
from circuit_maintenance_parser.parsers.equinix import HtmlParserEquinix, SubjectParserEquinix
from circuit_maintenance_parser.parsers.gtt import HtmlParserGTT1
from circuit_maintenance_parser.parsers.hgc import HtmlParserHGC1, HtmlParserHGC2, SubjectParserHGC1
Expand Down Expand Up @@ -225,6 +226,15 @@ class Colt(GenericProvider):
_default_organizer = "[email protected]"


class CrownCastle(GenericProvider):
"""Crown Castle Fiber provider custom class."""

_processors: List[GenericProcessor] = [
CombinedProcessor(data_parsers=[EmailDateParser, HtmlParserCrownCastle1]),
]
_default_organizer = "[email protected]"


class Equinix(GenericProvider):
"""Equinix provider custom class."""

Expand Down
116 changes: 116 additions & 0 deletions tests/unit/data/crowncastle/crowncastle1.eml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
Delivered-To: [email protected]
Message-ID: <[email protected]>
MIME-Version: 1.0
From: Change Management <[email protected]>
To: NOC <[email protected]>
Date: Tue, 10 Dec 2023 01:44:00 -0500
Subject: Crown Castle Fiber Scheduled Maintenance Notification: CM20231201000 on 1/3/2024 3:00 AM EST
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: base64

PCFET0NUWVBFIGh0bWw+DQo8aHRtbCBsYW5nPSJlbiI+DQo8aGVhZD4NCjxtZXRhIGh0dHAtZXF1
aXY9IkNvbnRlbnQtVHlwZSIgY29udGVudD0idGV4dC9odG1sOyBjaGFyc2V0PXV0Zi04Ij4NCjx0
aXRsZT48L3RpdGxlPg0KPHN0eWxlPgogICAgICAgIC50aW1lem9uZWdyaWQsICNjaXJjdWl0Z3Jp
ZCB7CiAgICAgICAgICAgIGZvbnQtZmFtaWx5OiBBcmlhbDsKICAgICAgICAgICAgYm9yZGVyLWNv
bGxhcHNlOiBjb2xsYXBzZTsKICAgICAgICAgICAgZm9udC1zaXplOiAxMnB4OwogICAgICAgIH0K
CiAgICAgICAgICAgIC50aW1lem9uZWdyaWQgdGQsIC50aW1lem9uZWdyaWQgdGgsICNjaXJjdWl0
Z3JpZCB0ZCwgI2NpcmN1aXRncmlkIHRoIHsKICAgICAgICAgICAgICAgIGJvcmRlcjogMXB4IHNv
bGlkICNkZGQ7CiAgICAgICAgICAgICAgICBwYWRkaW5nOiA4cHg7CiAgICAgICAgICAgIH0KCiAg
ICAgICAgICAgIC50aW1lem9uZWdyaWQgdGhlYWQsICNjaXJjdWl0Z3JpZCB0aGVhZCB7CiAgICAg
ICAgICAgICAgICBwYWRkaW5nLXRvcDogMnB4OwogICAgICAgICAgICAgICAgcGFkZGluZy1ib3R0
b206IDJweDsKICAgICAgICAgICAgICAgIGJvcmRlcjogMXB4IHNvbGlkICNkZGQ7CiAgICAgICAg
ICAgICAgICB0ZXh0LWFsaWduOiBsZWZ0OwogICAgICAgICAgICB9CgogICAgICAgIGJvZHkgewog
ICAgICAgICAgICBmb250LWZhbWlseTogQXJpYWw7CiAgICAgICAgICAgIGJvcmRlci1jb2xsYXBz
ZTogY29sbGFwc2U7CiAgICAgICAgICAgIGZvbnQtc2l6ZTogMTJweDsKICAgICAgICB9CiAgICA8
L3N0eWxlPg0KPC9oZWFkPg0KPGJvZHk+DQo8ZGl2Pg0KPHRhYmxlIHN0eWxlPSJ3aWR0aDogOTAl
OyBib3JkZXI6bm9uZTsgYm9yZGVyLXNwYWNpbmc6MDsgcGFkZGluZzowOyI+DQo8dGJvZHk+DQo8
dHI+DQo8dGQgdmFsaWduPSJ0b3AiPg0KPHAgYWxpZ249ImNlbnRlciI+PGI+PHU+PHNwYW4+PGlt
ZyBzcmM9Imh0dHBzOi8vdGVtcGdvLmNyb3duY2FzdGxlLmNvbS9ycy8zNDMtTFFSLTY1MC9pbWFn
ZXMvaW1hZ2UwMV9DQ0xvZ28yLnBuZyIgaGVpZ2h0PSI2NSIgd2lkdGg9IjI0NiI+PC9zcGFuPjwv
dT48L2I+PC9wPg0KPC90ZD4NCjwvdHI+DQo8dHI+DQo8dGQgdmFsaWduPSJ0b3AiPg0KPHAgYWxp
Z249ImNlbnRlciI+Jm5ic3A7PC9wPg0KPC90ZD4NCjwvdHI+DQo8dHI+DQo8dGQgdmFsaWduPSJ0
b3AiPg0KPHAgYWxpZ249ImNlbnRlciI+PGI+PHNwYW4gc3R5bGU9ImNvbG9yOiM1QTY3NzE7Zm9u
dC1zaXplOjI0cHgiPjx1Pk1haW50ZW5hbmNlIE5vdGlmaWNhdGlvbjwvdT48L3NwYW4+PC9iPjwv
cD4NCjwvdGQ+DQo8L3RyPg0KPHRyPg0KPHRkPg0KPHA+PC9wPg0KPC90ZD4NCjwvdHI+DQo8dHI+
DQo8dGQ+DQo8cD48L3A+DQo8L3RkPg0KPC90cj4NCjx0cj4NCjx0ZCB2YWxpZ249InRvcCI+DQo8
cD4mbmJzcDs8L3A+DQo8cD4mbmJzcDs8L3A+DQo8cD48L3A+DQo8cD48L3A+DQo8cD5EZWFyIEV4
YW1wbGUgQ3VzdG9tZXIsIDxicj4NCjxicj4NClRoaXMgbm90aWNlIGlzIGJlaW5nIHNlbnQgdG8g
bm90aWZ5IHlvdSBvZiB0aGUgZm9sbG93aW5nIHBsYW5uZWQgbWFpbnRlbmFuY2UgZXZlbnQgb24g
dGhlIENyb3duIENhc3RsZSBGaWJlciBuZXR3b3JrLg0KPGJyPg0KPGJyPg0KPC9wPg0KPHA+PHN0
cm9uZz5UaWNrZXQgTnVtYmVyOiA8L3N0cm9uZz5DTTIwMjMxMjAxMDAwPC9wPg0KPHA+PHN0cm9u
Zz5Mb2NhdGlvbiBvZiBXb3JrOiA8L3N0cm9uZz5Bbnl3aGVyZSwgRkw8L3A+DQo8cD48L3A+DQo8
cD4NCjx0YWJsZSBjbGFzcz0idGltZXpvbmVncmlkIj4NCjx0Ym9keT4NCjx0cj4NCjx0aCBjb2xz
cGFuPSIyIj5TY2hlZHVsZWQgU3RhcnQgRGF0ZSAmYW1wOyBUaW1lIDwvdGg+DQo8dGggY29sc3Bh
bj0iMiI+U2NoZWR1bGVkIEVuZCBEYXRlICZhbXA7IFRpbWUgPC90aD4NCjx0aD5UaW1lIFpvbmUg
PC90aD4NCjwvdHI+DQo8dHI+DQo8dGQ+MS8zLzIwMjQgPC90ZD4NCjx0ZD4zOjAwIEFNIDwvdGQ+
DQo8dGQ+MS8zLzIwMjQgPC90ZD4NCjx0ZD45OjAwIEFNIDwvdGQ+DQo8dGQ+RWFzdGVybiA8L3Rk
Pg0KPC90cj4NCjx0cj4NCjx0ZD4xLzMvMjAyNCA8L3RkPg0KPHRkPjI6MDAgQU0gPC90ZD4NCjx0
ZD4xLzMvMjAyNCA8L3RkPg0KPHRkPjg6MDAgQU0gPC90ZD4NCjx0ZD5DZW50cmFsIDwvdGQ+DQo8
L3RyPg0KPHRyPg0KPHRkPjEvMy8yMDI0IDwvdGQ+DQo8dGQ+MTowMCBBTSA8L3RkPg0KPHRkPjEv
My8yMDI0IDwvdGQ+DQo8dGQ+NzowMCBBTSA8L3RkPg0KPHRkPk1vdW50YWluIDwvdGQ+DQo8L3Ry
Pg0KPHRyPg0KPHRkPjEvMy8yMDI0IDwvdGQ+DQo8dGQ+MTI6MDAgQU0gPC90ZD4NCjx0ZD4xLzMv
MjAyNCA8L3RkPg0KPHRkPjY6MDAgQU0gPC90ZD4NCjx0ZD5QYWNpZmljIDwvdGQ+DQo8L3RyPg0K
PHRyPg0KPHRkPjEvMy8yMDI0IDwvdGQ+DQo8dGQ+ODowMCBBTSA8L3RkPg0KPHRkPjEvMy8yMDI0
IDwvdGQ+DQo8dGQ+MjowMCBQTSA8L3RkPg0KPHRkPkdNVCA8L3RkPg0KPC90cj4NCjwvdGJvZHk+
DQo8L3RhYmxlPg0KPC9wPg0KPHA+PHN0cm9uZz5FeHBlY3RlZCBDdXN0b21lciBJbXBhY3Q6PC9z
dHJvbmc+IFBvdGVudGlhbCBTZXJ2aWNlIEFmZmVjdGluZzwvcD4NCjxwPjxzdHJvbmc+RXhwZWN0
ZWQgSW1wYWN0IER1cmF0aW9uOjwvc3Ryb25nPiBOb25lIEV4cGVjdGVkPC9wPg0KPHA+PC9wPg0K
PHA+PHN0cm9uZz5Xb3JrPC9zdHJvbmc+IDxzdHJvbmc+RGVzY3JpcHRpb246PC9zdHJvbmc+PC9w
Pg0KPHA+Q3Jvd24gQ2FzdGxlIEZpYmVyIHdpbGwgYmUgY29uZHVjdGluZyBhIHJlcXVpcmVkIG1h
aW50ZW5hbmNlIGF0IHRoZSBhYm92ZS1saXN0ZWQgbG9jYXRpb24gZm9yIHJvdXRpbmUgc3BsaWNp
bmcuIEFsdGhvdWdoIG5vIGltcGFjdCB0byB5b3VyIGNpcmN1aXQocykgbGlzdGVkIGJlbG93IGlz
IGV4cGVjdGVkLCB0aGlzIG1haW50ZW5hbmNlIGlzIGRlZW1lZCBwb3RlbnRpYWxseSBzZXJ2aWNl
IGFmZmVjdGluZy4gV2UgYXBvbG9naXplIGZvciBhbnkNCiByZXN1bHRpbmcgaW5jb252ZW5pZW5j
ZSBhbmQgYXNzdXJlIHlvdSBvZiBvdXIgZWZmb3J0cyB0byBtaW5pbWl6ZSBhbnkgc2VydmljZSBk
aXNydXB0aW9uLjwvcD4NCjxwPjxicj4NCkN1c3RvbWVyIENpcmN1aXRzOiA8L3A+DQo8cD4NCjx0
YWJsZSBpZD0iY2lyY3VpdGdyaWQiPg0KPHRoZWFkPg0KPHRyPg0KPHRoPkNpcmN1aXQgSUQ8L3Ro
Pg0KPHRoPkFjdGl2ZSBQcm9kdWN0PC90aD4NCjx0aD5BIExvY2F0aW9uPC90aD4NCjx0aD5aIExv
Y2F0aW9uPC90aD4NCjx0aD5JbXBhY3Q8L3RoPg0KPHRoPk5vdGVzPC90aD4NCjwvdHI+DQo8L3Ro
ZWFkPg0KPHRib2R5Pg0KPHRyPg0KPHRkPjAwMDAwMC1ERi1BQkNERkwwMS1EQ0JBRkwwMjwvdGQ+
DQo8dGQ+RGFyayBGaWJlciAvIFBvaW50IHRvIFBvaW50IC8gTi9BPC90ZD4NCjx0ZD4xMjMgTWFp
biwgQmFzZW1lbnQsIEFueXdoZXJlLCBGTCAxMjM0NTwvdGQ+DQo8dGQ+NTY3IE1haW4sIDFzdCBG
bG9vciwgQW55d2hlcmUsIEZMIDU0MzIxPC90ZD4NCjx0ZD5Ob25lPC90ZD4NCjx0ZD48L3RkPg0K
PC90cj4NCjx0cj4NCjx0ZD4xMTExMTEtREZBQS1DQ0Y8L3RkPg0KPHRkPkRhcmsgRmliZXIgLyBQ
b2ludCB0byBQb2ludCAvIE4vQTwvdGQ+DQo8dGQ+MjM0IE1haW4sIEJhc2VtZW50LCBBbnl3aGVy
ZSwgRkwgMTIzNDU8L3RkPg0KPHRkPjY3OCBNYWluLCAxc3QgRmxvb3IsIEFueXdoZXJlLCBGTCA1
NDMyMTwvdGQ+DQo8dGQ+Tm9uZTwvdGQ+DQo8dGQ+PC90ZD4NCjwvdHI+DQo8L3Rib2R5Pg0KPC90
YWJsZT4NCjwvcD4NCjxwPjwvcD4NCjxwPjwvcD4NCjxwPjwvcD4NCjxwPjxzdHJvbmc+RGFyay1G
aWJlciBDdXN0b21lcnM6PC9zdHJvbmc+IDxicj4NCjxicj4NCkRhcmsgRmliZXIgc2VydmljZXMg
Y2Fubm90IGJlIG1vbml0b3JlZCBieSBDcm93biBDYXN0bGUsIHdlIGFyZSByZWxpYW50IG9uIGN1
c3RvbWVyIGZlZWQgYmFjayBmb3IgY29uZmlybWF0aW9uIHRoYXQgc2VydmljZXMgaGF2ZSByZXN0
b3JlZC4gVG8gZW5zdXJlIHlvdXIgc2VydmljZXMgaGF2ZSByZXN0b3JlZCwgd2UgYXJlIHJlcXVl
c3RpbmcgdGhhdCB5b3UgcHJvdmlkZSBhIGNvbnRhY3Qgd2hpY2ggaXMgYm90aCBmYW1pbGlhciB3
aXRoIHRoZSBzZXJ2aWNlDQogYW5kIHdvdWxkIGJlIGFibGUgdG8gcHJvbXB0bHkgY29uZmlybSBz
ZXJ2aWNlIHJlc3RvcmF0aW9uIG9uY2UgdGhlIENNIGlzIGNvbXBsZXRlLiBUaGUgcmVxdWVzdCBm
b3IgY29uZmlybWF0aW9uIG1heSBiZSBuZWVkZWQgYWZ0ZXIgaG91cnMsIHBsZWFzZSBwcm92aWRl
IGJvdGggYSBuYW1lIGFuZCBjb250YWN0IHBob25lIG51bWJlciBpbiByZXNwb25zZSB0byB0aGlz
IGVtYWlsLg0KPGJyPg0KPGJyPg0KSWYgeW91IGhhdmUgYW55IHF1ZXN0aW9ucyBvciBjb25jZXJu
cyBwcmlvciB0byB0aGlzIGV2ZW50LCBwbGVhc2UgcmVwbHkgdG8gdGhpcyBub3RpZmljYXRpb24g
YXMgc29vbiBhcyBwb3NzaWJsZS4NCjxicj4NCjxicj4NCkJ5IHJlc3BvbmRpbmcgdG8gdGhpcyBu
b3RpZmljYXRpb24gaW4gYSB0aW1lbHkgbWFubmVyLCBDcm93biBDYXN0bGUgRmliZXIgQ2hhbmdl
IE1hbmFnZW1lbnQgY2FuIGF0dGVtcHQgdG8gcmVzb2x2ZSBhbnkgcG90ZW50aWFsIGNvbmZsaWN0
cyB0aGF0IG1heSBhcmlzZS4NCjxicj4NCjxicj4NCklmIHlvdSBoYXZlIGFueSBxdWVzdGlvbnMs
IGNvbmNlcm5zIG9yIGlzc3VlcyBiZWZvcmUsIGR1cmluZyBvciBhZnRlciB0aGlzIG1haW50ZW5h
bmNlIHdpbmRvdywgcGxlYXNlIGNvbnRhY3Qgb3VyIENoYW5nZSBNYW5hZ2VtZW50IERlcGFydG1l
bnQgYXQgMS01MDgtNjIxLTE4ODggYW5kIHJlZmVyZW5jZSB0aGlzIHRpY2tldCBudW1iZXIuDQo8
YnI+DQo8YnI+DQpJZiB5b3UgaGF2ZSBhbnkgc2VydmljZS9wZXJmb3JtYW5jZSByZWxhdGVkIGlz
c3VlcyBhZnRlciB0aGlzIG1haW50ZW5hbmNlIHdpbmRvdywgcGxlYXNlIGNvbnRhY3Qgb3VyIE5l
dHdvcmsgT3BlcmF0aW9ucyBDZW50ZXIgYXQgMS04NTUtOTMtRklCRVIgKDEtODU1LTkzMy00MjM3
KSBhbmQgcmVmZXJlbmNlIHRoaXMgdGlja2V0IG51bWJlci4NCjwvcD4NCjxwPjwvcD4NCjxicj4N
ClRoYW5rIFlvdSw8YnI+DQo8YnI+DQo8aT5DaGFuZ2UgQ29udHJvbDxicj4NCkVtYWlsOiA8YSBj
bGFzcz0iYXV0by1zdHlsZTEiIGhyZWY9Im1haWx0bzpmaWJlcmNoYW5nZW1nbXRAY3Jvd25jYXN0
bGUuY29tIj48c3BhbiBzdHlsZT0iY29sb3I6IzAwNjZjYzsiPmZpYmVyY2hhbmdlbWdtdEBjcm93
bmNhc3RsZS5jb208L3NwYW4+PC9hPjxicj4NCjUwOC02MjEtMTg4ODwvaT4NCjxwPjwvcD4NCjxw
PjxpbWcgd2lkdGg9IjIwOCIgaGVpZ2h0PSI1NSIgc3JjPSJodHRwczovL3RlbXBnby5jcm93bmNh
c3RsZS5jb20vcnMvMzQzLUxRUi02NTAvaW1hZ2VzL2ltYWdlMDFfQ0NMb2dvMi5wbmciPjwvcD4N
CjwvdGQ+DQo8L3RyPg0KPC90Ym9keT4NCjwvdGFibGU+DQo8L2Rpdj4NCjxwPjwvcD4NCjxwIGFs
aWduPSJjZW50ZXIiPjxpPjxzcGFuIHN0eWxlPSJjb2xvcjpibGFjaztmb250LXNpemU6OS41cHgi
PlBsZWFzZSBub3RlOiBldmVyeSBtYWludGVuYW5jZSBlbnRhaWxzIGEgY2VydGFpbiBsZXZlbCBv
ZiByaXNrIGFuZCBhbHRob3VnaCBDcm93biBDYXN0bGUgRmliZXIgbWFrZXMgZXZlcnkgZWZmb3J0
IHRvIHByb3ZpZGUgYWNjdXJhdGUgZXhwZWN0ZWQgY3VzdG9tZXIgaW1wYWN0LCBjb25kaXRpb25z
IG91dHNpZGUgb2YgQ3Jvd24gQ2FzdGxlDQogRmliZXIncyBjb250cm9sIG1heSBjYXVzZSBpbXBh
Y3QgdG8gYmUgZ3JlYXRlciB0aGFuIGFudGljaXBhdGVkLjwvc3Bhbj48L2k+PC9wPg0KVGhpcyBl
bWFpbCBtYXkgY29udGFpbiBjb25maWRlbnRpYWwgb3IgcHJpdmlsZWdlZCBtYXRlcmlhbC4gVXNl
IG9yIGRpc2Nsb3N1cmUgb2YgaXQgYnkgYW55b25lIG90aGVyIHRoYW4gdGhlIHJlY2lwaWVudCBp
cyB1bmF1dGhvcml6ZWQuIElmIHlvdSBhcmUgbm90IGFuIGludGVuZGVkIHJlY2lwaWVudCwgcGxl
YXNlIGRlbGV0ZSB0aGlzIGVtYWlsLg0KPC9ib2R5Pg0KPC9odG1sPg0K
25 changes: 25 additions & 0 deletions tests/unit/data/crowncastle/crowncastle1_result.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[
{
"account": "Example Customer",
"circuits": [
{
"circuit_id": "000000-DF-ABCDFL01-DCBAFL02",
"impact": "NO-IMPACT"
},
{
"circuit_id": "111111-DFAA-CCF",
"impact": "NO-IMPACT"
}
],
"end": 1704290400,
"maintenance_id": "CM20231201000",
"organizer": "[email protected]",
"provider": "crowncastle",
"sequence": 1,
"stamp": 1702190640,
"start": 1704268800,
"status": "CONFIRMED",
"summary": "Crown Castle Fiber will be conducting a required maintenance at the above-listed location for routine splicing. Although no impact to your circuit(s) listed below is expected, this maintenance is deemed potentially service affecting. We apologize for any resulting inconvenience and assure you of our efforts to minimize any service disruption.",
"uid": "0"
}
]
Loading

0 comments on commit 19279ed

Please sign in to comment.