Skip to content

Commit

Permalink
Merge pull request #126 from ns1/import_to_networks_views
Browse files Browse the repository at this point in the history
Zone file import - support missing params.
  • Loading branch information
hhellyer authored Oct 31, 2024
2 parents ac3974a + 27a205a commit 0e0ac36
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 12 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 0.22.0 (Oct 29th, 2024)

ENHANCEMENTS:
* Adds support for specifying a list of views when creating zones with or without a provided zone file.
* Adds support for specifying a zone name other than the FQDN when creating zones with or without a provided zone file.
* A specified list of networks for a zone was only applied to zone creation when a zone file was not provided.

## 0.21.0 (July 19th, 2024)

ENHANCEMENTS:
Expand Down
19 changes: 15 additions & 4 deletions ns1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
#
from .config import Config

version = "0.21.0"
version = "0.22.0"


class NS1:
Expand Down Expand Up @@ -271,7 +271,13 @@ def searchZone(
return rest_zone.search(query, type, expand, max, callback, errback)

def createZone(
self, zone, zoneFile=None, callback=None, errback=None, **kwargs
self,
zone,
zoneFile=None,
callback=None,
errback=None,
name=None,
**kwargs
):
"""
Create a new zone, and return an associated high level Zone object.
Expand All @@ -281,8 +287,9 @@ def createZone(
If zoneFile is specified, upload the specific zone definition file
to populate the zone with.
:param str zone: zone name, like 'example.com'
:param str zone: zone FQDN, like 'example.com'
:param str zoneFile: absolute path of a zone file
:param str name: zone name override, name will be zone FQDN if omitted
:keyword int retry: retry time
:keyword int refresh: refresh ttl
:keyword int expiry: expiry ttl
Expand All @@ -295,7 +302,11 @@ def createZone(
zone = ns1.zones.Zone(self.config, zone)

return zone.create(
zoneFile=zoneFile, callback=callback, errback=errback, **kwargs
zoneFile=zoneFile,
name=name,
callback=callback,
errback=errback,
**kwargs
)

def loadRecord(
Expand Down
33 changes: 30 additions & 3 deletions ns1/rest/zones.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,15 @@ class Zones(resource.BaseResource):
"link",
"primary_master",
"tags",
"views",
]
BOOL_FIELDS = ["dnssec"]

ZONEFILE_FIELDS = [
"networks",
"views",
]

def _buildBody(self, zone, **kwargs):
body = {}
body["zone"] = zone
Expand All @@ -34,19 +40,40 @@ def import_file(
self, zone, zoneFile, callback=None, errback=None, **kwargs
):
files = [("zonefile", (zoneFile, open(zoneFile, "rb"), "text/plain"))]
params = self._buildImportParams(kwargs)
return self._make_request(
"PUT",
"import/zonefile/%s" % (zone),
f"import/zonefile/{zone}",
files=files,
params=params,
callback=callback,
errback=errback,
)

def create(self, zone, callback=None, errback=None, **kwargs):
# Extra import args are specified as query parameters not fields in a JSON object.
def _buildImportParams(self, fields):
params = {}
# Arrays of values should be passed as multiple instances of the same
# parameter but the zonefile API expects parameters containing comma
# seperated values.
if fields.get("networks") is not None:
networks_strs = [str(network) for network in fields["networks"]]
networks_param = ",".join(networks_strs)
params["networks"] = networks_param
if fields.get("views") is not None:
views_param = ",".join(fields["views"])
params["views"] = views_param
if fields.get("name") is not None:
params["name"] = fields.get("name")
return params

def create(self, zone, callback=None, errback=None, name=None, **kwargs):
body = self._buildBody(zone, **kwargs)
if name is None:
name = zone
return self._make_request(
"PUT",
"%s/%s" % (self.ROOT, zone),
f"{self.ROOT}/{name}",
body=body,
callback=callback,
errback=errback,
Expand Down
22 changes: 17 additions & 5 deletions ns1/zones.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,21 @@ def success(result, *args):
self.zone, callback=success, errback=errback, **kwargs
)

def create(self, zoneFile=None, callback=None, errback=None, **kwargs):
def create(
self, zoneFile=None, callback=None, errback=None, name=None, **kwargs
):
"""
Create a new zone. Pass a list of keywords and their values to
configure. For the list of keywords available for zone configuration,
see :attr:`ns1.rest.zones.Zones.INT_FIELDS` and
see :attr:`ns1.rest.zones.Zones.INT_FIELDS`,
:attr:`ns1.rest.zones.Zones.BOOL_FIELDS` and
:attr:`ns1.rest.zones.Zones.PASSTHRU_FIELDS`
If zoneFile is passed, it should be a zone text file on the local disk
that will be used to populate the created zone file.
Use `name` to pass a unique name for the zone otherwise this will
default to the zone FQDN.
If zoneFile is passed, it should be a zone text file on the local
disk that will be used to populate the created zone file. When a
zoneFile is passed only `name` and
:attr:`ns1.rest.zones.Zones.ZONEFILE_FIELDS` are supported.
"""
if self.data:
raise ZoneException("zone already loaded")
Expand All @@ -115,11 +122,16 @@ def success(result, *args):
zoneFile,
callback=success,
errback=errback,
name=name,
**kwargs
)
else:
return self._rest.create(
self.zone, callback=success, errback=errback, **kwargs
self.zone,
callback=success,
errback=errback,
name=name,
**kwargs
)

def __getattr__(self, item):
Expand Down
128 changes: 128 additions & 0 deletions tests/unit/test_zone.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import ns1.rest.zones
import pytest
import os

try: # Python 3.3 +
import unittest.mock as mock
Expand Down Expand Up @@ -64,6 +65,133 @@ def test_rest_zone_version_list(zones_config, zone, url):
)


@pytest.mark.parametrize(
"zone, name, url",
[
("test.zone", None, "zones/test.zone"),
("test.zone", "test.name", "zones/test.name"),
],
)
def test_rest_zone_create(zones_config, zone, name, url):
z = ns1.rest.zones.Zones(zones_config)
z._make_request = mock.MagicMock()
z.create(zone, name=name)
z._make_request.assert_called_once_with(
"PUT", url, body={"zone": zone}, callback=None, errback=None
)


@pytest.mark.parametrize(
"zone, name, url, params",
[
("test.zone", None, "zones/test.zone", {}),
("test.zone", "test.name", "zones/test.name", {}),
("test.zone", "test2.name", "zones/test2.name", {"networks": [1, 2]}),
(
"test.zone",
"test3.name",
"zones/test3.name",
{"networks": [1, 2], "views": "testview"},
),
(
"test.zone",
"test4.name",
"zones/test4.name",
{"hostmaster": "example:example.com"},
),
],
)
def test_rest_zone_create_with_params(zones_config, zone, name, url, params):
z = ns1.rest.zones.Zones(zones_config)
z._make_request = mock.MagicMock()
z.create(zone, name=name, **params)
body = params
body["zone"] = zone
z._make_request.assert_called_once_with(
"PUT", url, body=body, callback=None, errback=None
)


@pytest.mark.parametrize(
"zone, name, url, networks, views",
[
("test.zone", None, "import/zonefile/test.zone", None, None),
("test.zone", "test.name", "import/zonefile/test.zone", None, None),
(
"test.zone",
"test.name",
"import/zonefile/test.zone",
[1, 2, 99],
None,
),
(
"test.zone",
"test.name",
"import/zonefile/test.zone",
None,
["view1", "view2"],
),
(
"test.zone",
"test.name",
"import/zonefile/test.zone",
[3, 4, 99],
["viewA", "viewB"],
),
],
)
def test_rest_zone_import_file(zones_config, zone, name, url, networks, views):
z = ns1.rest.zones.Zones(zones_config)
z._make_request = mock.MagicMock()
params = {}
networks_strs = None
if networks is not None:
networks_strs = map(str, networks)
params["networks"] = ",".join(networks_strs)
if views is not None:
params["views"] = ",".join(views)

zoneFilePath = "{}/../../examples/importzone.db".format(
os.path.dirname(os.path.abspath(__file__))
)

def cb():
# Should never be printed but provides a function body.
print("Callback invoked!")

# Test without zone name parameter
z.import_file(
zone,
zoneFilePath,
callback=cb,
errback=None,
networks=networks,
views=views,
)

z._make_request.assert_called_once_with(
"PUT", url, files=mock.ANY, callback=cb, errback=None, params=params
)

# Test with new zone name parameter (extra argument)
z._make_request.reset_mock()

if name is not None:
params["name"] = name
z.import_file(
zone,
zoneFilePath,
networks=networks,
views=views,
name=name,
callback=cb,
)

z._make_request.assert_called_once_with(
"PUT", url, files=mock.ANY, callback=cb, errback=None, params=params
)


@pytest.mark.parametrize(
"zone, url", [("test.zone", "zones/test.zone/versions?force=false")]
)
Expand Down

0 comments on commit 0e0ac36

Please sign in to comment.