Skip to content

Commit 19279ed

Browse files
authored
Feature/crown castle fiber (#259)
* 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
1 parent 91fee6a commit 19279ed

23 files changed

Lines changed: 1741 additions & 3 deletions

README.md

Lines changed: 2 additions & 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. 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.
27+
- **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.
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.
@@ -71,6 +71,7 @@ By default, there is a `GenericProvider` that supports a `SimpleProcessor` using
7171
- BSO
7272
- Cogent
7373
- Colt
74+
- Crown Castle Fiber
7475
- Equinix
7576
- EXA (formerly GTT)
7677
- HGC

circuit_maintenance_parser/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
BSO,
1313
Cogent,
1414
Colt,
15+
CrownCastle,
1516
Equinix,
1617
EUNetworks,
1718
GTT,
@@ -39,6 +40,7 @@
3940
BSO,
4041
Cogent,
4142
Colt,
43+
CrownCastle,
4244
Equinix,
4345
EUNetworks,
4446
GTT,

circuit_maintenance_parser/output.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@ class Maintenance(BaseModel, extra=Extra.forbid):
108108
account: identifies an account associated with the service that is the subject of the maintenance notification
109109
maintenance_id: contains text that uniquely identifies the maintenance that is the subject of the notification
110110
circuits: list of circuits affected by the maintenance notification and their specific impact. Note this can be
111-
an empty list for notifications with a CANCELLED status if the provider does not populate the circuit list.
111+
an empty list for notifications with a CANCELLED or COMPLETED status if the provider does not populate the
112+
circuit list.
112113
status: defines the overall status or confirmation for the maintenance
113114
start: timestamp that defines the start date of the maintenance in GMT
114115
end: timestamp that defines the end date of the maintenance in GMT
@@ -184,7 +185,7 @@ def validate_empty_strings(cls, value):
184185
@validator("circuits")
185186
def validate_empty_circuits(cls, value, values):
186187
"""Validate non-cancel notifications have a populated circuit list."""
187-
if len(value) < 1 and values["status"] != "CANCELLED":
188+
if len(value) < 1 and str(values["status"]) in ("CANCELLED", "COMPLETED"):
188189
raise ValueError("At least one circuit has to be included in the maintenance")
189190
return value
190191

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""Crown Castle Fiber parser."""
2+
import logging
3+
import re
4+
from datetime import datetime
5+
6+
from circuit_maintenance_parser.parser import Html, Impact, CircuitImpact, Status
7+
8+
# pylint: disable=too-many-nested-blocks, too-many-branches
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
class HtmlParserCrownCastle1(Html):
14+
"""Notifications Parser for Crown Castle Fiber notifications."""
15+
16+
def parse_html(self, soup):
17+
"""Execute parsing."""
18+
data = {}
19+
data["circuits"] = []
20+
21+
data["status"] = self.get_status(soup)
22+
23+
for paragraph in soup.find_all("p"):
24+
for pstring in paragraph.strings:
25+
search = re.match(r"^Dear (.*),", pstring)
26+
if search:
27+
data["account"] = search.group(1)
28+
29+
self.parse_strong(soup, data)
30+
31+
table = soup.find("table", "timezonegrid")
32+
for row in table.find_all("tr"):
33+
cols = row.find_all("td")
34+
if len(cols) == 5:
35+
if cols[4].string.strip() == "GMT":
36+
st_dt = cols[0].string.strip() + " " + cols[1].string.strip() + " GMT"
37+
en_dt = cols[2].string.strip() + " " + cols[3].string.strip() + " GMT"
38+
data["start"] = self.dt2ts(datetime.strptime(st_dt, "%m/%d/%Y %I:%M %p %Z"))
39+
data["end"] = self.dt2ts(datetime.strptime(en_dt, "%m/%d/%Y %I:%M %p %Z"))
40+
41+
table = soup.find("table", id="circuitgrid")
42+
if table is not None:
43+
for row in table.find_all("tr"):
44+
cols = row.find_all("td")
45+
if len(cols) == 6:
46+
if cols[4].string.strip() == "None":
47+
impact = Impact("NO-IMPACT")
48+
else:
49+
impact = Impact("OUTAGE")
50+
data["circuits"].append(CircuitImpact(impact=impact, circuit_id=cols[0].string.strip()))
51+
52+
return [data]
53+
54+
def parse_strong(self, soup, data):
55+
"""Parse the strong tags, to find summary and maintenance ID info."""
56+
for strong in soup.find_all("strong"):
57+
if strong.string.strip() == "Ticket Number:":
58+
data["maintenance_id"] = strong.next_sibling.strip()
59+
if strong.string.strip() == "Description:":
60+
summary = strong.parent.next_sibling.next_sibling.contents[0].string.strip()
61+
summary = re.sub(r"[\n\r]", "", summary)
62+
data["summary"] = summary
63+
if strong.string.strip().startswith("Work Description:"):
64+
for sibling in strong.parent.next_siblings:
65+
summary = "".join(sibling.strings)
66+
summary = re.sub(r"[\n\r]", "", summary)
67+
if summary != "":
68+
data["summary"] = summary
69+
break
70+
71+
def get_status(self, soup):
72+
"""Get the status of the maintenance."""
73+
for paragraph in soup.find_all("p"):
74+
for pstring in paragraph.strings:
75+
if "has been completed." in pstring:
76+
return Status("COMPLETED")
77+
78+
for underline in soup.find_all("u"):
79+
if underline.string is None:
80+
continue
81+
if underline.string.strip() == "Maintenance Notification":
82+
return Status("CONFIRMED")
83+
if underline.string.strip() == "Emergency Notification":
84+
return Status("CONFIRMED")
85+
if underline.string.strip() == "Maintenance Notification - Rescheduled Event":
86+
return Status("RE-SCHEDULED")
87+
if underline.string.strip() == "Maintenance Cancellation Notification":
88+
return Status("CANCELLED")
89+
90+
return Status("NO-CHANGE")

circuit_maintenance_parser/provider.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from circuit_maintenance_parser.parsers.bso import HtmlParserBSO1
2424
from circuit_maintenance_parser.parsers.cogent import HtmlParserCogent1, TextParserCogent1, SubjectParserCogent1
2525
from circuit_maintenance_parser.parsers.colt import CsvParserColt1, SubjectParserColt1, SubjectParserColt2
26+
from circuit_maintenance_parser.parsers.crowncastle import HtmlParserCrownCastle1
2627
from circuit_maintenance_parser.parsers.equinix import HtmlParserEquinix, SubjectParserEquinix
2728
from circuit_maintenance_parser.parsers.gtt import HtmlParserGTT1
2829
from circuit_maintenance_parser.parsers.hgc import HtmlParserHGC1, HtmlParserHGC2, SubjectParserHGC1
@@ -225,6 +226,15 @@ class Colt(GenericProvider):
225226
_default_organizer = "PlannedWorks@colt.net"
226227

227228

229+
class CrownCastle(GenericProvider):
230+
"""Crown Castle Fiber provider custom class."""
231+
232+
_processors: List[GenericProcessor] = [
233+
CombinedProcessor(data_parsers=[EmailDateParser, HtmlParserCrownCastle1]),
234+
]
235+
_default_organizer = "fiberchangemgmt@crowncastle.com"
236+
237+
228238
class Equinix(GenericProvider):
229239
"""Equinix provider custom class."""
230240

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
Delivered-To: noc@example.com
2+
Message-ID: <1@FIBERCHANGEMGMT.CROWNCASTLE.COM>
3+
MIME-Version: 1.0
4+
From: Change Management <fiberChangemgmt@crowncastle.com>
5+
To: NOC <noc@example.com>
6+
Date: Tue, 10 Dec 2023 01:44:00 -0500
7+
Subject: Crown Castle Fiber Scheduled Maintenance Notification: CM20231201000 on 1/3/2024 3:00 AM EST
8+
Content-Type: text/html; charset="utf-8"
9+
Content-Transfer-Encoding: base64
10+
11+
PCFET0NUWVBFIGh0bWw+DQo8aHRtbCBsYW5nPSJlbiI+DQo8aGVhZD4NCjxtZXRhIGh0dHAtZXF1
12+
aXY9IkNvbnRlbnQtVHlwZSIgY29udGVudD0idGV4dC9odG1sOyBjaGFyc2V0PXV0Zi04Ij4NCjx0
13+
aXRsZT48L3RpdGxlPg0KPHN0eWxlPgogICAgICAgIC50aW1lem9uZWdyaWQsICNjaXJjdWl0Z3Jp
14+
ZCB7CiAgICAgICAgICAgIGZvbnQtZmFtaWx5OiBBcmlhbDsKICAgICAgICAgICAgYm9yZGVyLWNv
15+
bGxhcHNlOiBjb2xsYXBzZTsKICAgICAgICAgICAgZm9udC1zaXplOiAxMnB4OwogICAgICAgIH0K
16+
CiAgICAgICAgICAgIC50aW1lem9uZWdyaWQgdGQsIC50aW1lem9uZWdyaWQgdGgsICNjaXJjdWl0
17+
Z3JpZCB0ZCwgI2NpcmN1aXRncmlkIHRoIHsKICAgICAgICAgICAgICAgIGJvcmRlcjogMXB4IHNv
18+
bGlkICNkZGQ7CiAgICAgICAgICAgICAgICBwYWRkaW5nOiA4cHg7CiAgICAgICAgICAgIH0KCiAg
19+
ICAgICAgICAgIC50aW1lem9uZWdyaWQgdGhlYWQsICNjaXJjdWl0Z3JpZCB0aGVhZCB7CiAgICAg
20+
ICAgICAgICAgICBwYWRkaW5nLXRvcDogMnB4OwogICAgICAgICAgICAgICAgcGFkZGluZy1ib3R0
21+
b206IDJweDsKICAgICAgICAgICAgICAgIGJvcmRlcjogMXB4IHNvbGlkICNkZGQ7CiAgICAgICAg
22+
ICAgICAgICB0ZXh0LWFsaWduOiBsZWZ0OwogICAgICAgICAgICB9CgogICAgICAgIGJvZHkgewog
23+
ICAgICAgICAgICBmb250LWZhbWlseTogQXJpYWw7CiAgICAgICAgICAgIGJvcmRlci1jb2xsYXBz
24+
ZTogY29sbGFwc2U7CiAgICAgICAgICAgIGZvbnQtc2l6ZTogMTJweDsKICAgICAgICB9CiAgICA8
25+
L3N0eWxlPg0KPC9oZWFkPg0KPGJvZHk+DQo8ZGl2Pg0KPHRhYmxlIHN0eWxlPSJ3aWR0aDogOTAl
26+
OyBib3JkZXI6bm9uZTsgYm9yZGVyLXNwYWNpbmc6MDsgcGFkZGluZzowOyI+DQo8dGJvZHk+DQo8
27+
dHI+DQo8dGQgdmFsaWduPSJ0b3AiPg0KPHAgYWxpZ249ImNlbnRlciI+PGI+PHU+PHNwYW4+PGlt
28+
ZyBzcmM9Imh0dHBzOi8vdGVtcGdvLmNyb3duY2FzdGxlLmNvbS9ycy8zNDMtTFFSLTY1MC9pbWFn
29+
ZXMvaW1hZ2UwMV9DQ0xvZ28yLnBuZyIgaGVpZ2h0PSI2NSIgd2lkdGg9IjI0NiI+PC9zcGFuPjwv
30+
dT48L2I+PC9wPg0KPC90ZD4NCjwvdHI+DQo8dHI+DQo8dGQgdmFsaWduPSJ0b3AiPg0KPHAgYWxp
31+
Z249ImNlbnRlciI+Jm5ic3A7PC9wPg0KPC90ZD4NCjwvdHI+DQo8dHI+DQo8dGQgdmFsaWduPSJ0
32+
b3AiPg0KPHAgYWxpZ249ImNlbnRlciI+PGI+PHNwYW4gc3R5bGU9ImNvbG9yOiM1QTY3NzE7Zm9u
33+
dC1zaXplOjI0cHgiPjx1Pk1haW50ZW5hbmNlIE5vdGlmaWNhdGlvbjwvdT48L3NwYW4+PC9iPjwv
34+
cD4NCjwvdGQ+DQo8L3RyPg0KPHRyPg0KPHRkPg0KPHA+PC9wPg0KPC90ZD4NCjwvdHI+DQo8dHI+
35+
DQo8dGQ+DQo8cD48L3A+DQo8L3RkPg0KPC90cj4NCjx0cj4NCjx0ZCB2YWxpZ249InRvcCI+DQo8
36+
cD4mbmJzcDs8L3A+DQo8cD4mbmJzcDs8L3A+DQo8cD48L3A+DQo8cD48L3A+DQo8cD5EZWFyIEV4
37+
YW1wbGUgQ3VzdG9tZXIsIDxicj4NCjxicj4NClRoaXMgbm90aWNlIGlzIGJlaW5nIHNlbnQgdG8g
38+
bm90aWZ5IHlvdSBvZiB0aGUgZm9sbG93aW5nIHBsYW5uZWQgbWFpbnRlbmFuY2UgZXZlbnQgb24g
39+
dGhlIENyb3duIENhc3RsZSBGaWJlciBuZXR3b3JrLg0KPGJyPg0KPGJyPg0KPC9wPg0KPHA+PHN0
40+
cm9uZz5UaWNrZXQgTnVtYmVyOiA8L3N0cm9uZz5DTTIwMjMxMjAxMDAwPC9wPg0KPHA+PHN0cm9u
41+
Zz5Mb2NhdGlvbiBvZiBXb3JrOiA8L3N0cm9uZz5Bbnl3aGVyZSwgRkw8L3A+DQo8cD48L3A+DQo8
42+
cD4NCjx0YWJsZSBjbGFzcz0idGltZXpvbmVncmlkIj4NCjx0Ym9keT4NCjx0cj4NCjx0aCBjb2xz
43+
cGFuPSIyIj5TY2hlZHVsZWQgU3RhcnQgRGF0ZSAmYW1wOyBUaW1lIDwvdGg+DQo8dGggY29sc3Bh
44+
bj0iMiI+U2NoZWR1bGVkIEVuZCBEYXRlICZhbXA7IFRpbWUgPC90aD4NCjx0aD5UaW1lIFpvbmUg
45+
PC90aD4NCjwvdHI+DQo8dHI+DQo8dGQ+MS8zLzIwMjQgPC90ZD4NCjx0ZD4zOjAwIEFNIDwvdGQ+
46+
DQo8dGQ+MS8zLzIwMjQgPC90ZD4NCjx0ZD45OjAwIEFNIDwvdGQ+DQo8dGQ+RWFzdGVybiA8L3Rk
47+
Pg0KPC90cj4NCjx0cj4NCjx0ZD4xLzMvMjAyNCA8L3RkPg0KPHRkPjI6MDAgQU0gPC90ZD4NCjx0
48+
ZD4xLzMvMjAyNCA8L3RkPg0KPHRkPjg6MDAgQU0gPC90ZD4NCjx0ZD5DZW50cmFsIDwvdGQ+DQo8
49+
L3RyPg0KPHRyPg0KPHRkPjEvMy8yMDI0IDwvdGQ+DQo8dGQ+MTowMCBBTSA8L3RkPg0KPHRkPjEv
50+
My8yMDI0IDwvdGQ+DQo8dGQ+NzowMCBBTSA8L3RkPg0KPHRkPk1vdW50YWluIDwvdGQ+DQo8L3Ry
51+
Pg0KPHRyPg0KPHRkPjEvMy8yMDI0IDwvdGQ+DQo8dGQ+MTI6MDAgQU0gPC90ZD4NCjx0ZD4xLzMv
52+
MjAyNCA8L3RkPg0KPHRkPjY6MDAgQU0gPC90ZD4NCjx0ZD5QYWNpZmljIDwvdGQ+DQo8L3RyPg0K
53+
PHRyPg0KPHRkPjEvMy8yMDI0IDwvdGQ+DQo8dGQ+ODowMCBBTSA8L3RkPg0KPHRkPjEvMy8yMDI0
54+
IDwvdGQ+DQo8dGQ+MjowMCBQTSA8L3RkPg0KPHRkPkdNVCA8L3RkPg0KPC90cj4NCjwvdGJvZHk+
55+
DQo8L3RhYmxlPg0KPC9wPg0KPHA+PHN0cm9uZz5FeHBlY3RlZCBDdXN0b21lciBJbXBhY3Q6PC9z
56+
dHJvbmc+IFBvdGVudGlhbCBTZXJ2aWNlIEFmZmVjdGluZzwvcD4NCjxwPjxzdHJvbmc+RXhwZWN0
57+
ZWQgSW1wYWN0IER1cmF0aW9uOjwvc3Ryb25nPiBOb25lIEV4cGVjdGVkPC9wPg0KPHA+PC9wPg0K
58+
PHA+PHN0cm9uZz5Xb3JrPC9zdHJvbmc+IDxzdHJvbmc+RGVzY3JpcHRpb246PC9zdHJvbmc+PC9w
59+
Pg0KPHA+Q3Jvd24gQ2FzdGxlIEZpYmVyIHdpbGwgYmUgY29uZHVjdGluZyBhIHJlcXVpcmVkIG1h
60+
aW50ZW5hbmNlIGF0IHRoZSBhYm92ZS1saXN0ZWQgbG9jYXRpb24gZm9yIHJvdXRpbmUgc3BsaWNp
61+
bmcuIEFsdGhvdWdoIG5vIGltcGFjdCB0byB5b3VyIGNpcmN1aXQocykgbGlzdGVkIGJlbG93IGlz
62+
IGV4cGVjdGVkLCB0aGlzIG1haW50ZW5hbmNlIGlzIGRlZW1lZCBwb3RlbnRpYWxseSBzZXJ2aWNl
63+
IGFmZmVjdGluZy4gV2UgYXBvbG9naXplIGZvciBhbnkNCiByZXN1bHRpbmcgaW5jb252ZW5pZW5j
64+
ZSBhbmQgYXNzdXJlIHlvdSBvZiBvdXIgZWZmb3J0cyB0byBtaW5pbWl6ZSBhbnkgc2VydmljZSBk
65+
aXNydXB0aW9uLjwvcD4NCjxwPjxicj4NCkN1c3RvbWVyIENpcmN1aXRzOiA8L3A+DQo8cD4NCjx0
66+
YWJsZSBpZD0iY2lyY3VpdGdyaWQiPg0KPHRoZWFkPg0KPHRyPg0KPHRoPkNpcmN1aXQgSUQ8L3Ro
67+
Pg0KPHRoPkFjdGl2ZSBQcm9kdWN0PC90aD4NCjx0aD5BIExvY2F0aW9uPC90aD4NCjx0aD5aIExv
68+
Y2F0aW9uPC90aD4NCjx0aD5JbXBhY3Q8L3RoPg0KPHRoPk5vdGVzPC90aD4NCjwvdHI+DQo8L3Ro
69+
ZWFkPg0KPHRib2R5Pg0KPHRyPg0KPHRkPjAwMDAwMC1ERi1BQkNERkwwMS1EQ0JBRkwwMjwvdGQ+
70+
DQo8dGQ+RGFyayBGaWJlciAvIFBvaW50IHRvIFBvaW50IC8gTi9BPC90ZD4NCjx0ZD4xMjMgTWFp
71+
biwgQmFzZW1lbnQsIEFueXdoZXJlLCBGTCAxMjM0NTwvdGQ+DQo8dGQ+NTY3IE1haW4sIDFzdCBG
72+
bG9vciwgQW55d2hlcmUsIEZMIDU0MzIxPC90ZD4NCjx0ZD5Ob25lPC90ZD4NCjx0ZD48L3RkPg0K
73+
PC90cj4NCjx0cj4NCjx0ZD4xMTExMTEtREZBQS1DQ0Y8L3RkPg0KPHRkPkRhcmsgRmliZXIgLyBQ
74+
b2ludCB0byBQb2ludCAvIE4vQTwvdGQ+DQo8dGQ+MjM0IE1haW4sIEJhc2VtZW50LCBBbnl3aGVy
75+
ZSwgRkwgMTIzNDU8L3RkPg0KPHRkPjY3OCBNYWluLCAxc3QgRmxvb3IsIEFueXdoZXJlLCBGTCA1
76+
NDMyMTwvdGQ+DQo8dGQ+Tm9uZTwvdGQ+DQo8dGQ+PC90ZD4NCjwvdHI+DQo8L3Rib2R5Pg0KPC90
77+
YWJsZT4NCjwvcD4NCjxwPjwvcD4NCjxwPjwvcD4NCjxwPjwvcD4NCjxwPjxzdHJvbmc+RGFyay1G
78+
aWJlciBDdXN0b21lcnM6PC9zdHJvbmc+IDxicj4NCjxicj4NCkRhcmsgRmliZXIgc2VydmljZXMg
79+
Y2Fubm90IGJlIG1vbml0b3JlZCBieSBDcm93biBDYXN0bGUsIHdlIGFyZSByZWxpYW50IG9uIGN1
80+
c3RvbWVyIGZlZWQgYmFjayBmb3IgY29uZmlybWF0aW9uIHRoYXQgc2VydmljZXMgaGF2ZSByZXN0
81+
b3JlZC4gVG8gZW5zdXJlIHlvdXIgc2VydmljZXMgaGF2ZSByZXN0b3JlZCwgd2UgYXJlIHJlcXVl
82+
c3RpbmcgdGhhdCB5b3UgcHJvdmlkZSBhIGNvbnRhY3Qgd2hpY2ggaXMgYm90aCBmYW1pbGlhciB3
83+
aXRoIHRoZSBzZXJ2aWNlDQogYW5kIHdvdWxkIGJlIGFibGUgdG8gcHJvbXB0bHkgY29uZmlybSBz
84+
ZXJ2aWNlIHJlc3RvcmF0aW9uIG9uY2UgdGhlIENNIGlzIGNvbXBsZXRlLiBUaGUgcmVxdWVzdCBm
85+
b3IgY29uZmlybWF0aW9uIG1heSBiZSBuZWVkZWQgYWZ0ZXIgaG91cnMsIHBsZWFzZSBwcm92aWRl
86+
IGJvdGggYSBuYW1lIGFuZCBjb250YWN0IHBob25lIG51bWJlciBpbiByZXNwb25zZSB0byB0aGlz
87+
IGVtYWlsLg0KPGJyPg0KPGJyPg0KSWYgeW91IGhhdmUgYW55IHF1ZXN0aW9ucyBvciBjb25jZXJu
88+
cyBwcmlvciB0byB0aGlzIGV2ZW50LCBwbGVhc2UgcmVwbHkgdG8gdGhpcyBub3RpZmljYXRpb24g
89+
YXMgc29vbiBhcyBwb3NzaWJsZS4NCjxicj4NCjxicj4NCkJ5IHJlc3BvbmRpbmcgdG8gdGhpcyBu
90+
b3RpZmljYXRpb24gaW4gYSB0aW1lbHkgbWFubmVyLCBDcm93biBDYXN0bGUgRmliZXIgQ2hhbmdl
91+
IE1hbmFnZW1lbnQgY2FuIGF0dGVtcHQgdG8gcmVzb2x2ZSBhbnkgcG90ZW50aWFsIGNvbmZsaWN0
92+
cyB0aGF0IG1heSBhcmlzZS4NCjxicj4NCjxicj4NCklmIHlvdSBoYXZlIGFueSBxdWVzdGlvbnMs
93+
IGNvbmNlcm5zIG9yIGlzc3VlcyBiZWZvcmUsIGR1cmluZyBvciBhZnRlciB0aGlzIG1haW50ZW5h
94+
bmNlIHdpbmRvdywgcGxlYXNlIGNvbnRhY3Qgb3VyIENoYW5nZSBNYW5hZ2VtZW50IERlcGFydG1l
95+
bnQgYXQgMS01MDgtNjIxLTE4ODggYW5kIHJlZmVyZW5jZSB0aGlzIHRpY2tldCBudW1iZXIuDQo8
96+
YnI+DQo8YnI+DQpJZiB5b3UgaGF2ZSBhbnkgc2VydmljZS9wZXJmb3JtYW5jZSByZWxhdGVkIGlz
97+
c3VlcyBhZnRlciB0aGlzIG1haW50ZW5hbmNlIHdpbmRvdywgcGxlYXNlIGNvbnRhY3Qgb3VyIE5l
98+
dHdvcmsgT3BlcmF0aW9ucyBDZW50ZXIgYXQgMS04NTUtOTMtRklCRVIgKDEtODU1LTkzMy00MjM3
99+
KSBhbmQgcmVmZXJlbmNlIHRoaXMgdGlja2V0IG51bWJlci4NCjwvcD4NCjxwPjwvcD4NCjxicj4N
100+
ClRoYW5rIFlvdSw8YnI+DQo8YnI+DQo8aT5DaGFuZ2UgQ29udHJvbDxicj4NCkVtYWlsOiA8YSBj
101+
bGFzcz0iYXV0by1zdHlsZTEiIGhyZWY9Im1haWx0bzpmaWJlcmNoYW5nZW1nbXRAY3Jvd25jYXN0
102+
bGUuY29tIj48c3BhbiBzdHlsZT0iY29sb3I6IzAwNjZjYzsiPmZpYmVyY2hhbmdlbWdtdEBjcm93
103+
bmNhc3RsZS5jb208L3NwYW4+PC9hPjxicj4NCjUwOC02MjEtMTg4ODwvaT4NCjxwPjwvcD4NCjxw
104+
PjxpbWcgd2lkdGg9IjIwOCIgaGVpZ2h0PSI1NSIgc3JjPSJodHRwczovL3RlbXBnby5jcm93bmNh
105+
c3RsZS5jb20vcnMvMzQzLUxRUi02NTAvaW1hZ2VzL2ltYWdlMDFfQ0NMb2dvMi5wbmciPjwvcD4N
106+
CjwvdGQ+DQo8L3RyPg0KPC90Ym9keT4NCjwvdGFibGU+DQo8L2Rpdj4NCjxwPjwvcD4NCjxwIGFs
107+
aWduPSJjZW50ZXIiPjxpPjxzcGFuIHN0eWxlPSJjb2xvcjpibGFjaztmb250LXNpemU6OS41cHgi
108+
PlBsZWFzZSBub3RlOiBldmVyeSBtYWludGVuYW5jZSBlbnRhaWxzIGEgY2VydGFpbiBsZXZlbCBv
109+
ZiByaXNrIGFuZCBhbHRob3VnaCBDcm93biBDYXN0bGUgRmliZXIgbWFrZXMgZXZlcnkgZWZmb3J0
110+
IHRvIHByb3ZpZGUgYWNjdXJhdGUgZXhwZWN0ZWQgY3VzdG9tZXIgaW1wYWN0LCBjb25kaXRpb25z
111+
IG91dHNpZGUgb2YgQ3Jvd24gQ2FzdGxlDQogRmliZXIncyBjb250cm9sIG1heSBjYXVzZSBpbXBh
112+
Y3QgdG8gYmUgZ3JlYXRlciB0aGFuIGFudGljaXBhdGVkLjwvc3Bhbj48L2k+PC9wPg0KVGhpcyBl
113+
bWFpbCBtYXkgY29udGFpbiBjb25maWRlbnRpYWwgb3IgcHJpdmlsZWdlZCBtYXRlcmlhbC4gVXNl
114+
IG9yIGRpc2Nsb3N1cmUgb2YgaXQgYnkgYW55b25lIG90aGVyIHRoYW4gdGhlIHJlY2lwaWVudCBp
115+
cyB1bmF1dGhvcml6ZWQuIElmIHlvdSBhcmUgbm90IGFuIGludGVuZGVkIHJlY2lwaWVudCwgcGxl
116+
YXNlIGRlbGV0ZSB0aGlzIGVtYWlsLg0KPC9ib2R5Pg0KPC9odG1sPg0K
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[
2+
{
3+
"account": "Example Customer",
4+
"circuits": [
5+
{
6+
"circuit_id": "000000-DF-ABCDFL01-DCBAFL02",
7+
"impact": "NO-IMPACT"
8+
},
9+
{
10+
"circuit_id": "111111-DFAA-CCF",
11+
"impact": "NO-IMPACT"
12+
}
13+
],
14+
"end": 1704290400,
15+
"maintenance_id": "CM20231201000",
16+
"organizer": "fiberchangemgmt@crowncastle.com",
17+
"provider": "crowncastle",
18+
"sequence": 1,
19+
"stamp": 1702190640,
20+
"start": 1704268800,
21+
"status": "CONFIRMED",
22+
"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.",
23+
"uid": "0"
24+
}
25+
]

0 commit comments

Comments
 (0)