Skip to content

Commit 60fd6f3

Browse files
authored
Add support for the v1 alerts api. (#131)
- Add api class. - Add tests. - Add examples.
1 parent a5d9569 commit 60fd6f3

File tree

5 files changed

+371
-1
lines changed

5 files changed

+371
-1
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.23.0 (Dec 9th, 2024)
2+
3+
ENHANCEMENTS:
4+
* Adds support for Alerts
5+
16
## 0.22.0 (Oct 29th, 2024)
27

38
ENHANCEMENTS:

examples/alerts.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#
2+
# Copyright (c) 2024 NSONE, Inc.
3+
#
4+
# License under The MIT License (MIT). See LICENSE in project root.
5+
#
6+
7+
from ns1 import NS1
8+
9+
# NS1 will use config in ~/.nsone by default
10+
api = NS1()
11+
12+
# to specify an apikey here instead, use:
13+
# api = NS1(apiKey='<<CLEARTEXT API KEY>>')
14+
15+
# to load an alternate configuration file:
16+
# api = NS1(configFile='/etc/ns1/api.json')
17+
18+
# turn on "follow pagination". This will handle paginated responses for
19+
# zone list and the records for a zone retrieve. It's off by default to
20+
# avoid a breaking change
21+
config = api.config
22+
config["follow_pagination"] = True
23+
24+
# create a new zone, get a Zone object back
25+
# to use in a new alert
26+
zone = api.createZone(
27+
"example-secondary.com",
28+
secondary={
29+
"enabled": True,
30+
"primary_ip": "198.51.100.12",
31+
"primary_port": 53,
32+
"tsig": {
33+
"enabled": False,
34+
},
35+
},
36+
)
37+
print("Created zone: %s" % zone["name"])
38+
39+
# Create a notifier list.
40+
nl = api.notifylists().create(
41+
body={
42+
"name": "example",
43+
"notify_list": [
44+
{"type": "email", "config": {"email": "[email protected]"}}
45+
],
46+
}
47+
)
48+
print("Created notifier list with id: %s" % nl["id"])
49+
nl_id = nl["id"]
50+
51+
# Create an alert
52+
newAlert = api.alerts().create(
53+
name="example_alert",
54+
type="zone",
55+
subtype="transfer_failed",
56+
zone_names=["example-secondary.com"],
57+
notifier_list_ids=[nl_id],
58+
)
59+
alert_id = newAlert["id"]
60+
print("Created alert with id: %s" % alert_id)
61+
62+
# List alerts.
63+
alertList = api.alerts().list()
64+
print(alertList)
65+
for alert in alertList:
66+
print(alert["name"])
67+
68+
# Clean up.
69+
api.alerts().delete(alert_id)
70+
api.notifylists().delete(nl_id)
71+
zone.delete()

ns1/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# Copyright (c) 2014 NSONE, Inc.
2+
# Copyright (c) 2014, 2024 NSONE, Inc.
33
#
44
# License under The MIT License (MIT). See LICENSE in project root.
55
#
@@ -232,6 +232,16 @@ def redirect_certificates(self):
232232

233233
return ns1.rest.redirect.RedirectCertificates(self.config)
234234

235+
def alerts(self):
236+
"""
237+
Return a new raw REST interface to alert resources
238+
239+
:rtype: :py:class:`ns1.rest.alerts.Alerts`
240+
"""
241+
import ns1.rest.alerts
242+
243+
return ns1.rest.alerts.Alerts(self.config)
244+
235245
# HIGH LEVEL INTERFACE
236246
def loadZone(self, zone, callback=None, errback=None):
237247
"""

ns1/rest/alerts.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#
2+
# Copyright (c) 2024 NSONE, Inc.
3+
#
4+
# License under The MIT License (MIT). See LICENSE in project root.
5+
#
6+
from . import resource
7+
8+
9+
class Alerts(resource.BaseResource):
10+
ROOT = "../alerting/v1/alerts"
11+
PASSTHRU_FIELDS = [
12+
"name",
13+
"data",
14+
"notifier_list_ids",
15+
"record_ids",
16+
"zone_names",
17+
]
18+
19+
def _buildBody(self, alid, **kwargs):
20+
body = {}
21+
body["id"] = alid
22+
self._buildStdBody(body, kwargs)
23+
return body
24+
25+
def list(self, callback=None, errback=None):
26+
data = self._make_request(
27+
"GET",
28+
"%s" % (self.ROOT),
29+
callback=callback,
30+
errback=errback,
31+
pagination_handler=alert_list_pagination,
32+
)
33+
return data["results"]
34+
35+
def update(self, alid, callback=None, errback=None, **kwargs):
36+
body = self._buildBody(alid, **kwargs)
37+
38+
return self._make_request(
39+
"PATCH",
40+
"%s/%s" % (self.ROOT, alid),
41+
body=body,
42+
callback=callback,
43+
errback=errback,
44+
)
45+
46+
def create(
47+
self, name, type, subtype, callback=None, errback=None, **kwargs
48+
):
49+
body = {
50+
"name": name,
51+
"type": type,
52+
"subtype": subtype,
53+
}
54+
self._buildStdBody(body, kwargs)
55+
return self._make_request(
56+
"POST",
57+
"%s" % (self.ROOT),
58+
body=body,
59+
callback=callback,
60+
errback=errback,
61+
)
62+
63+
def retrieve(self, alert_id, callback=None, errback=None):
64+
return self._make_request(
65+
"GET",
66+
"%s/%s" % (self.ROOT, alert_id),
67+
callback=callback,
68+
errback=errback,
69+
)
70+
71+
def delete(self, alert_id, callback=None, errback=None):
72+
return self._make_request(
73+
"DELETE",
74+
"%s/%s" % (self.ROOT, alert_id),
75+
callback=callback,
76+
errback=errback,
77+
)
78+
79+
def test(self, alert_id, callback=None, errback=None):
80+
return self._make_request(
81+
"POST",
82+
"%s/%s/test" % (self.ROOT, alert_id),
83+
callback=callback,
84+
errback=errback,
85+
)
86+
87+
88+
# successive pages contain the next alerts in the results list
89+
def alert_list_pagination(curr_json, next_json):
90+
curr_json["results"].extend(next_json["results"])
91+
return curr_json

tests/unit/test_alerts.py

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
#
2+
# Copyright (c) 2024 NSONE, Inc.
3+
#
4+
# License under The MIT License (MIT). See LICENSE in project root.
5+
#
6+
import pytest
7+
8+
import ns1.rest.alerts
9+
10+
try: # Python 3.3 +
11+
import unittest.mock as mock
12+
except ImportError:
13+
import mock
14+
15+
16+
@pytest.fixture
17+
def alerts_config(config):
18+
config.loadFromDict(
19+
{
20+
"endpoint": "api.nsone.net",
21+
"default_key": "test1",
22+
"keys": {
23+
"test1": {
24+
"key": "key-1",
25+
"desc": "test key number 1",
26+
}
27+
},
28+
}
29+
)
30+
return config
31+
32+
33+
@pytest.mark.parametrize(
34+
"alert_id, url",
35+
[
36+
(
37+
"9d51efb4-a012-43b0-bcd9-6fad45227baf",
38+
"../alerting/v1/alerts/9d51efb4-a012-43b0-bcd9-6fad45227baf",
39+
)
40+
],
41+
)
42+
def test_rest_alert_retrieve(alerts_config, alert_id, url):
43+
a = ns1.rest.alerts.Alerts(alerts_config)
44+
a._make_request = mock.MagicMock()
45+
a.retrieve(alert_id)
46+
a._make_request.assert_called_once_with(
47+
"GET", url, callback=None, errback=None
48+
)
49+
50+
51+
def test_rest_alert_list(alerts_config):
52+
a = ns1.rest.alerts.Alerts(alerts_config)
53+
a._make_request = mock.MagicMock()
54+
a.list()
55+
a._make_request.assert_called_once_with(
56+
"GET",
57+
"../alerting/v1/alerts",
58+
callback=None,
59+
errback=None,
60+
pagination_handler=ns1.rest.alerts.alert_list_pagination,
61+
)
62+
63+
64+
@pytest.mark.parametrize(
65+
"name, type, subtype, url, alert_params",
66+
[
67+
(
68+
"test_alert",
69+
"zone",
70+
"transfer_failed",
71+
"../alerting/v1/alerts",
72+
{
73+
"zone_names": ["example-secondary.com"],
74+
"notifier_list_ids": ["6707da567cd4f300012cd7e4"],
75+
},
76+
),
77+
(
78+
"test_alert_with_data",
79+
"zone",
80+
"transfer_failed",
81+
"../alerting/v1/alerts",
82+
{
83+
"zone_names": ["example-secondary.com"],
84+
"notifier_list_ids": ["6707da567cd4f300012cd7e4"],
85+
"data": {"min": 20, "max": 80},
86+
},
87+
),
88+
],
89+
)
90+
def test_rest_alert_create(
91+
alerts_config, name, type, subtype, url, alert_params
92+
):
93+
a = ns1.rest.alerts.Alerts(alerts_config)
94+
a._make_request = mock.MagicMock()
95+
a.create(name=name, type=type, subtype=subtype, **alert_params)
96+
body = alert_params
97+
body["name"] = name
98+
body["type"] = type
99+
body["subtype"] = subtype
100+
a._make_request.assert_called_once_with(
101+
"POST",
102+
url,
103+
body=body,
104+
callback=None,
105+
errback=None,
106+
)
107+
108+
109+
@pytest.mark.parametrize(
110+
"alert_id, url",
111+
[
112+
(
113+
"9d51efb4-a012-43b0-bcd9-6fad45227baf",
114+
"../alerting/v1/alerts/9d51efb4-a012-43b0-bcd9-6fad45227baf",
115+
)
116+
],
117+
)
118+
def test_rest_alert_update(alerts_config, alert_id, url):
119+
a = ns1.rest.alerts.Alerts(alerts_config)
120+
a._make_request = mock.MagicMock()
121+
a.update(alert_id, name="newName")
122+
expectedBody = {"id": alert_id, "name": "newName"}
123+
a._make_request.assert_called_once_with(
124+
"PATCH",
125+
url,
126+
callback=None,
127+
errback=None,
128+
body=expectedBody,
129+
)
130+
131+
132+
@pytest.mark.parametrize(
133+
"alert_id, url",
134+
[
135+
(
136+
"9d51efb4-a012-43b0-bcd9-6fad45227baf",
137+
"../alerting/v1/alerts/9d51efb4-a012-43b0-bcd9-6fad45227baf",
138+
)
139+
],
140+
)
141+
def test_rest_alert_delete(alerts_config, alert_id, url):
142+
a = ns1.rest.alerts.Alerts(alerts_config)
143+
a._make_request = mock.MagicMock()
144+
a.delete(alert_id)
145+
a._make_request.assert_called_once_with(
146+
"DELETE", url, callback=None, errback=None
147+
)
148+
149+
150+
# Alerts have a alerts/<id>/test endpoint to verify the attached notifiers work
151+
@pytest.mark.parametrize(
152+
"alert_id, url",
153+
[
154+
(
155+
"9d51efb4-a012-43b0-bcd9-6fad45227baf",
156+
"../alerting/v1/alerts/9d51efb4-a012-43b0-bcd9-6fad45227baf/test",
157+
)
158+
],
159+
)
160+
def test_rest_alert_do_test(alerts_config, alert_id, url):
161+
a = ns1.rest.alerts.Alerts(alerts_config)
162+
a._make_request = mock.MagicMock()
163+
a.test(alert_id)
164+
a._make_request.assert_called_once_with(
165+
"POST", url, callback=None, errback=None
166+
)
167+
168+
169+
def test_rest_alerts_buildbody(alerts_config):
170+
a = ns1.rest.alerts.Alerts(alerts_config)
171+
alert_id = "9d51efb4-a012-43b0-bcd9-6fad45227baf"
172+
kwargs = {
173+
"data": {"max": 80, "min": 20},
174+
"name": "newName",
175+
"notifier_list_ids": [
176+
"6707da567cd4f300012cd7e4",
177+
"6707da567cd4f300012cd7e6",
178+
],
179+
"record_ids": ["6707da567cd4f300012cd7d4", "6707da567cd4f300012cd7d9"],
180+
"zone_names": ["www.example.com", "mail.example.com"],
181+
}
182+
expectedBody = {
183+
"id": alert_id,
184+
"name": "newName",
185+
"data": {"max": 80, "min": 20},
186+
"notifier_list_ids": [
187+
"6707da567cd4f300012cd7e4",
188+
"6707da567cd4f300012cd7e6",
189+
],
190+
"record_ids": ["6707da567cd4f300012cd7d4", "6707da567cd4f300012cd7d9"],
191+
"zone_names": ["www.example.com", "mail.example.com"],
192+
}
193+
assert a._buildBody(alert_id, **kwargs) == expectedBody

0 commit comments

Comments
 (0)