mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
allow to pass login/password which will be used to acquire token for further usage
This commit is contained in:
@@ -11,7 +11,9 @@ from __future__ import (
|
|||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from requests import Session
|
from requests import Session
|
||||||
|
import http
|
||||||
import logging
|
import logging
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
from ..record import Record
|
from ..record import Record
|
||||||
from .base import BaseProvider
|
from .base import BaseProvider
|
||||||
@@ -34,51 +36,82 @@ class GCoreClientNotFound(GCoreClientException):
|
|||||||
|
|
||||||
class GCoreClient(object):
|
class GCoreClient(object):
|
||||||
|
|
||||||
ROOT_ZONES = "/zones"
|
ROOT_ZONES = "zones"
|
||||||
|
|
||||||
def __init__(self, log, base_url, token):
|
def __init__(
|
||||||
session = Session()
|
self,
|
||||||
session.headers.update({"Authorization": "Bearer {}".format(token)})
|
log,
|
||||||
|
api_url,
|
||||||
|
auth_url,
|
||||||
|
token=None,
|
||||||
|
login=None,
|
||||||
|
password=None,
|
||||||
|
):
|
||||||
self.log = log
|
self.log = log
|
||||||
self._session = session
|
self._session = Session()
|
||||||
self._base_url = base_url
|
self._api_url = api_url
|
||||||
|
if token is not None:
|
||||||
|
self._session.headers.update(
|
||||||
|
{"Authorization": "APIKey {}".format(token)}
|
||||||
|
)
|
||||||
|
elif login is not None and password is not None:
|
||||||
|
token = self._auth(auth_url, login, password)
|
||||||
|
self._session.headers.update(
|
||||||
|
{"Authorization": "Bearer {}".format(token)}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError("either token or login & password must be set")
|
||||||
|
|
||||||
def _request(self, method, path, params={}, data=None):
|
def _auth(self, url, login, password):
|
||||||
url = "{}{}".format(self._base_url, path)
|
# well, can't use _request, since API returns 400 if credentials
|
||||||
|
# invalid which will be logged, but we don't want do this
|
||||||
|
r = self._session.request(
|
||||||
|
"POST",
|
||||||
|
self._build_url(url, "auth", "jwt", "login"),
|
||||||
|
json={"username": login, "password": password},
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json()["access"]
|
||||||
|
|
||||||
|
def _request(self, method, url, params=None, data=None):
|
||||||
r = self._session.request(
|
r = self._session.request(
|
||||||
method, url, params=params, json=data, timeout=30.0
|
method, url, params=params, json=data, timeout=30.0
|
||||||
)
|
)
|
||||||
if r.status_code == 400:
|
if r.status_code == http.HTTPStatus.BAD_REQUEST:
|
||||||
self.log.error(
|
self.log.error(
|
||||||
"bad request %r has been sent to %r: %s", data, url, r.text
|
"bad request %r has been sent to %r: %s", data, url, r.text
|
||||||
)
|
)
|
||||||
raise GCoreClientBadRequest(r)
|
raise GCoreClientBadRequest(r)
|
||||||
elif r.status_code == 404:
|
elif r.status_code == http.HTTPStatus.NOT_FOUND:
|
||||||
self.log.error(
|
self.log.error("resource %r not found: %s", url, r.text)
|
||||||
"resource %r not found: %s", url, r.text
|
|
||||||
)
|
|
||||||
raise GCoreClientNotFound(r)
|
raise GCoreClientNotFound(r)
|
||||||
elif r.status_code == 500:
|
elif r.status_code == http.HTTPStatus.INTERNAL_SERVER_ERROR:
|
||||||
self.log.error(
|
self.log.error("server error no %r to %r: %s", data, url, r.text)
|
||||||
"server error no %r to %r: %s", data, url, r.text
|
|
||||||
)
|
|
||||||
raise GCoreClientException(r)
|
raise GCoreClientException(r)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
return r
|
return r
|
||||||
|
|
||||||
def zone(self, zone_name):
|
def zone(self, zone_name):
|
||||||
return self._request(
|
return self._request(
|
||||||
"GET", "{}/{}".format(self.ROOT_ZONES, zone_name)
|
"GET", self._build_url(self._api_url, self.ROOT_ZONES, zone_name)
|
||||||
).json()
|
).json()
|
||||||
|
|
||||||
def zone_create(self, zone_name):
|
def zone_create(self, zone_name):
|
||||||
return self._request(
|
return self._request(
|
||||||
"POST", self.ROOT_ZONES, data={"name": zone_name}
|
"POST",
|
||||||
|
self._build_url(self._api_url, self.ROOT_ZONES),
|
||||||
|
data={"name": zone_name},
|
||||||
).json()
|
).json()
|
||||||
|
|
||||||
def zone_records(self, zone_name):
|
def zone_records(self, zone_name):
|
||||||
rrsets = self._request(
|
rrsets = self._request(
|
||||||
"GET", "{}/{}/rrsets?all=true".format(self.ROOT_ZONES, zone_name)
|
"GET",
|
||||||
|
"{}".format(
|
||||||
|
self._build_url(
|
||||||
|
self._api_url, self.ROOT_ZONES, zone_name, "rrsets"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
params={"all": "true"},
|
||||||
).json()
|
).json()
|
||||||
records = rrsets["rrsets"]
|
records = rrsets["rrsets"]
|
||||||
return records
|
return records
|
||||||
@@ -97,10 +130,17 @@ class GCoreClient(object):
|
|||||||
self._request("DELETE", self._rrset_url(zone_name, rrset_name, type_))
|
self._request("DELETE", self._rrset_url(zone_name, rrset_name, type_))
|
||||||
|
|
||||||
def _rrset_url(self, zone_name, rrset_name, type_):
|
def _rrset_url(self, zone_name, rrset_name, type_):
|
||||||
return "{}/{}/{}/{}".format(
|
return self._build_url(
|
||||||
self.ROOT_ZONES, zone_name, rrset_name, type_
|
self._api_url, self.ROOT_ZONES, zone_name, rrset_name, type_
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_url(base, *items):
|
||||||
|
for i in items:
|
||||||
|
base = base.strip("/") + "/"
|
||||||
|
base = urllib.parse.urljoin(base, i)
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
class GCoreProvider(BaseProvider):
|
class GCoreProvider(BaseProvider):
|
||||||
"""
|
"""
|
||||||
@@ -108,8 +148,12 @@ class GCoreProvider(BaseProvider):
|
|||||||
|
|
||||||
gcore:
|
gcore:
|
||||||
class: octodns.provider.gcore.GCoreProvider
|
class: octodns.provider.gcore.GCoreProvider
|
||||||
# Your API key (required)
|
# Your API key
|
||||||
token: XXXXXXXXXXXX
|
token: XXXXXXXXXXXX
|
||||||
|
# or login + password
|
||||||
|
login: XXXXXXXXXXXX
|
||||||
|
password: XXXXXXXXXXXX
|
||||||
|
# auth_url: https://api.gcdn.co
|
||||||
# url: https://dnsapi.gcorelabs.com/v2
|
# url: https://dnsapi.gcorelabs.com/v2
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -117,12 +161,18 @@ class GCoreProvider(BaseProvider):
|
|||||||
SUPPORTS_DYNAMIC = False
|
SUPPORTS_DYNAMIC = False
|
||||||
SUPPORTS = set(("A", "AAAA"))
|
SUPPORTS = set(("A", "AAAA"))
|
||||||
|
|
||||||
def __init__(self, id, token, *args, **kwargs):
|
def __init__(self, id, *args, **kwargs):
|
||||||
base_url = kwargs.pop("url", "https://dnsapi.gcorelabs.com/v2")
|
token = kwargs.pop("token", None)
|
||||||
|
login = kwargs.pop("login", None)
|
||||||
|
password = kwargs.pop("password", None)
|
||||||
|
api_url = kwargs.pop("url", "https://dnsapi.gcorelabs.com/v2")
|
||||||
|
auth_url = kwargs.pop("auth_url", "https://api.gcdn.co")
|
||||||
self.log = logging.getLogger("GCoreProvider[{}]".format(id))
|
self.log = logging.getLogger("GCoreProvider[{}]".format(id))
|
||||||
self.log.debug("__init__: id=%s, token=***", id)
|
self.log.debug("__init__: id=%s", id)
|
||||||
super(GCoreProvider, self).__init__(id, *args, **kwargs)
|
super(GCoreProvider, self).__init__(id, *args, **kwargs)
|
||||||
self._client = GCoreClient(self.log, base_url, token)
|
self._client = GCoreClient(
|
||||||
|
self.log, api_url, auth_url, token, login, password
|
||||||
|
)
|
||||||
|
|
||||||
def _data_for_single(self, _type, record):
|
def _data_for_single(self, _type, record):
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class TestGCoreProvider(TestCase):
|
|||||||
|
|
||||||
def test_populate(self):
|
def test_populate(self):
|
||||||
|
|
||||||
provider = GCoreProvider("test_id", "token")
|
provider = GCoreProvider("test_id", token="token")
|
||||||
|
|
||||||
# 400 - Bad Request.
|
# 400 - Bad Request.
|
||||||
with requests_mock() as mock:
|
with requests_mock() as mock:
|
||||||
@@ -52,7 +52,7 @@ class TestGCoreProvider(TestCase):
|
|||||||
|
|
||||||
with self.assertRaises(GCoreClientNotFound) as ctx:
|
with self.assertRaises(GCoreClientNotFound) as ctx:
|
||||||
zone = Zone("unit.tests.", [])
|
zone = Zone("unit.tests.", [])
|
||||||
provider._client.zone(zone)
|
provider._client.zone(zone.name)
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
'"error":"zone is not found"', text_type(ctx.exception)
|
'"error":"zone is not found"', text_type(ctx.exception)
|
||||||
)
|
)
|
||||||
@@ -66,6 +66,48 @@ class TestGCoreProvider(TestCase):
|
|||||||
provider.populate(zone)
|
provider.populate(zone)
|
||||||
self.assertEquals("Things caught fire", text_type(ctx.exception))
|
self.assertEquals("Things caught fire", text_type(ctx.exception))
|
||||||
|
|
||||||
|
# No credentials or token error
|
||||||
|
with requests_mock() as mock:
|
||||||
|
with self.assertRaises(ValueError) as ctx:
|
||||||
|
GCoreProvider("test_id")
|
||||||
|
self.assertEquals(
|
||||||
|
"either token or login & password must be set",
|
||||||
|
text_type(ctx.exception),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Auth with login password
|
||||||
|
with requests_mock() as mock:
|
||||||
|
|
||||||
|
def match_body(request):
|
||||||
|
return {"username": "foo", "password": "bar"} == request.json()
|
||||||
|
|
||||||
|
auth_url = "http://api/auth/jwt/login"
|
||||||
|
mock.post(
|
||||||
|
auth_url,
|
||||||
|
additional_matcher=match_body,
|
||||||
|
status_code=200,
|
||||||
|
json={"access": "access"},
|
||||||
|
)
|
||||||
|
|
||||||
|
providerPassword = GCoreProvider(
|
||||||
|
"test_id",
|
||||||
|
url="http://dns",
|
||||||
|
auth_url="http://api",
|
||||||
|
login="foo",
|
||||||
|
password="bar",
|
||||||
|
)
|
||||||
|
assert mock.called
|
||||||
|
|
||||||
|
# make sure token passed in header
|
||||||
|
zone_rrset_url = "http://dns/zones/unit.tests/rrsets?all=true"
|
||||||
|
mock.get(
|
||||||
|
zone_rrset_url,
|
||||||
|
request_headers={"Authorization": "Bearer access"},
|
||||||
|
status_code=404,
|
||||||
|
)
|
||||||
|
zone = Zone("unit.tests.", [])
|
||||||
|
assert not providerPassword.populate(zone)
|
||||||
|
|
||||||
# No diffs == no changes
|
# No diffs == no changes
|
||||||
with requests_mock() as mock:
|
with requests_mock() as mock:
|
||||||
base = "https://dnsapi.gcorelabs.com/v2/zones/unit.tests/rrsets"
|
base = "https://dnsapi.gcorelabs.com/v2/zones/unit.tests/rrsets"
|
||||||
@@ -97,7 +139,7 @@ class TestGCoreProvider(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_apply(self):
|
def test_apply(self):
|
||||||
provider = GCoreProvider("test_id", "token")
|
provider = GCoreProvider("test_id", url="http://api", token="token")
|
||||||
|
|
||||||
# Zone does not exists but can be created.
|
# Zone does not exists but can be created.
|
||||||
with requests_mock() as mock:
|
with requests_mock() as mock:
|
||||||
@@ -153,12 +195,16 @@ class TestGCoreProvider(TestCase):
|
|||||||
|
|
||||||
provider._client._request.assert_has_calls(
|
provider._client._request.assert_has_calls(
|
||||||
[
|
[
|
||||||
call("GET", "/zones/unit.tests/rrsets?all=true"),
|
call(
|
||||||
call("GET", "/zones/unit.tests"),
|
"GET",
|
||||||
call("POST", "/zones", data={"name": "unit.tests"}),
|
"http://api/zones/unit.tests/rrsets",
|
||||||
|
params={"all": "true"},
|
||||||
|
),
|
||||||
|
call("GET", "http://api/zones/unit.tests"),
|
||||||
|
call("POST", "http://api/zones", data={"name": "unit.tests"}),
|
||||||
call(
|
call(
|
||||||
"POST",
|
"POST",
|
||||||
"/zones/unit.tests/www.sub.unit.tests./A",
|
"http://api/zones/unit.tests/www.sub.unit.tests./A",
|
||||||
data={
|
data={
|
||||||
"ttl": 300,
|
"ttl": 300,
|
||||||
"resource_records": [{"content": ["2.2.3.6"]}],
|
"resource_records": [{"content": ["2.2.3.6"]}],
|
||||||
@@ -166,7 +212,7 @@ class TestGCoreProvider(TestCase):
|
|||||||
),
|
),
|
||||||
call(
|
call(
|
||||||
"POST",
|
"POST",
|
||||||
"/zones/unit.tests/www.unit.tests./A",
|
"http://api/zones/unit.tests/www.unit.tests./A",
|
||||||
data={
|
data={
|
||||||
"ttl": 300,
|
"ttl": 300,
|
||||||
"resource_records": [{"content": ["2.2.3.6"]}],
|
"resource_records": [{"content": ["2.2.3.6"]}],
|
||||||
@@ -174,7 +220,7 @@ class TestGCoreProvider(TestCase):
|
|||||||
),
|
),
|
||||||
call(
|
call(
|
||||||
"POST",
|
"POST",
|
||||||
"/zones/unit.tests/aaaa.unit.tests./AAAA",
|
"http://api/zones/unit.tests/aaaa.unit.tests./AAAA",
|
||||||
data={
|
data={
|
||||||
"ttl": 600,
|
"ttl": 600,
|
||||||
"resource_records": [
|
"resource_records": [
|
||||||
@@ -188,7 +234,7 @@ class TestGCoreProvider(TestCase):
|
|||||||
),
|
),
|
||||||
call(
|
call(
|
||||||
"POST",
|
"POST",
|
||||||
"/zones/unit.tests/unit.tests./A",
|
"http://api/zones/unit.tests/unit.tests./A",
|
||||||
data={
|
data={
|
||||||
"ttl": 300,
|
"ttl": 300,
|
||||||
"resource_records": [
|
"resource_records": [
|
||||||
@@ -239,10 +285,12 @@ class TestGCoreProvider(TestCase):
|
|||||||
|
|
||||||
provider._client._request.assert_has_calls(
|
provider._client._request.assert_has_calls(
|
||||||
[
|
[
|
||||||
call("DELETE", "/zones/unit.tests/www.unit.tests./A"),
|
call(
|
||||||
|
"DELETE", "http://api/zones/unit.tests/www.unit.tests./A"
|
||||||
|
),
|
||||||
call(
|
call(
|
||||||
"PUT",
|
"PUT",
|
||||||
"/zones/unit.tests/ttl.unit.tests./A",
|
"http://api/zones/unit.tests/ttl.unit.tests./A",
|
||||||
data={
|
data={
|
||||||
"ttl": 300,
|
"ttl": 300,
|
||||||
"resource_records": [{"content": ["3.2.3.4"]}],
|
"resource_records": [{"content": ["3.2.3.4"]}],
|
||||||
|
|||||||
Reference in New Issue
Block a user