Merge "Improve Dyntext coverage and removed unusable code"
This commit is contained in:
commit
9d96f5f955
|
@ -15,7 +15,6 @@
|
||||||
# under the License.
|
# under the License.
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from eventlet import Timeout
|
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from oslo_serialization import jsonutils
|
from oslo_serialization import jsonutils
|
||||||
import requests
|
import requests
|
||||||
|
@ -26,7 +25,6 @@ import designate.conf
|
||||||
from designate import exceptions
|
from designate import exceptions
|
||||||
from designate import utils
|
from designate import utils
|
||||||
|
|
||||||
|
|
||||||
CONF = designate.conf.CONF
|
CONF = designate.conf.CONF
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
CFG_GROUP_NAME = 'backend:dynect'
|
CFG_GROUP_NAME = 'backend:dynect'
|
||||||
|
@ -46,12 +44,9 @@ class DynClientError(exceptions.Backend):
|
||||||
self.url = url
|
self.url = url
|
||||||
self.method = method
|
self.method = method
|
||||||
self.details = details
|
self.details = details
|
||||||
formatted_string = '{} (HTTP {} to {} - {}) - {}'.format(
|
formatted_string = (
|
||||||
self.msgs,
|
f'{self.msgs} (HTTP {self.method} to {self.url} - '
|
||||||
self.method,
|
f'{self.http_status}) - {self.details}'
|
||||||
self.url,
|
|
||||||
self.http_status,
|
|
||||||
self.details
|
|
||||||
)
|
)
|
||||||
if job_id:
|
if job_id:
|
||||||
formatted_string += f' (Job-ID: {job_id})'
|
formatted_string += f' (Job-ID: {job_id})'
|
||||||
|
@ -101,16 +96,14 @@ class DynClient:
|
||||||
https://help.dynect.net/rest/
|
https://help.dynect.net/rest/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, customer_name, user_name, password,
|
def __init__(self, customer_name, user_name, password, timeout, timings,
|
||||||
endpoint="https://api.dynect.net:443",
|
verify=True, retries=1, pool_maxsize=10, pool_connections=10):
|
||||||
api_version='3.5.6', headers=None, verify=True, retries=1,
|
self.endpoint = 'https://api.dynect.net:443'
|
||||||
timeout=10, timings=False, pool_maxsize=10,
|
self.api_version = '3.5.6'
|
||||||
pool_connections=10):
|
|
||||||
self.customer_name = customer_name
|
self.customer_name = customer_name
|
||||||
self.user_name = user_name
|
self.user_name = user_name
|
||||||
self.password = password
|
self.password = password
|
||||||
self.endpoint = endpoint
|
|
||||||
self.api_version = api_version
|
|
||||||
|
|
||||||
self.times = [] # [("item", starttime, endtime), ...]
|
self.times = [] # [("item", starttime, endtime), ...]
|
||||||
self.timings = timings
|
self.timings = timings
|
||||||
|
@ -125,17 +118,17 @@ class DynClient:
|
||||||
session.headers = {
|
session.headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'API-Version': api_version,
|
'API-Version': self.api_version,
|
||||||
'User-Agent': 'DynECTClient'}
|
'User-Agent': 'DynECTClient'
|
||||||
|
}
|
||||||
|
|
||||||
if headers is not None:
|
adapter = HTTPAdapter(
|
||||||
session.headers.update(headers)
|
max_retries=int(retries),
|
||||||
|
pool_maxsize=int(pool_maxsize),
|
||||||
adapter = HTTPAdapter(max_retries=int(retries),
|
pool_connections=int(pool_connections),
|
||||||
pool_maxsize=int(pool_maxsize),
|
pool_block=True
|
||||||
pool_connections=int(pool_connections),
|
)
|
||||||
pool_block=True)
|
session.mount(self.endpoint, adapter)
|
||||||
session.mount(endpoint, adapter)
|
|
||||||
self.http = session
|
self.http = session
|
||||||
|
|
||||||
def _http_log_req(self, method, url, kwargs):
|
def _http_log_req(self, method, url, kwargs):
|
||||||
|
@ -149,25 +142,12 @@ class DynClient:
|
||||||
header = "-H '{}: {}'".format(element, kwargs['headers'][element])
|
header = "-H '{}: {}'".format(element, kwargs['headers'][element])
|
||||||
string_parts.append(header)
|
string_parts.append(header)
|
||||||
|
|
||||||
LOG.debug("REQ: %s", " ".join(string_parts))
|
LOG.debug('REQ: %s', ' '.join(string_parts))
|
||||||
if 'data' in kwargs:
|
if 'data' in kwargs:
|
||||||
LOG.debug("REQ BODY: %s\n" % (kwargs['data']))
|
LOG.debug('REQ BODY: %s\n', kwargs['data'])
|
||||||
|
|
||||||
def _http_log_resp(self, resp):
|
def _http_log_resp(self, resp):
|
||||||
LOG.debug(
|
LOG.debug('RESP: [%s] %s\n', resp.status_code, resp.headers)
|
||||||
"RESP: [%s] %s\n" %
|
|
||||||
(resp.status_code,
|
|
||||||
resp.headers))
|
|
||||||
if resp._content_consumed:
|
|
||||||
LOG.debug(
|
|
||||||
"RESP BODY: %s\n" %
|
|
||||||
resp.text)
|
|
||||||
|
|
||||||
def get_timings(self):
|
|
||||||
return self.times
|
|
||||||
|
|
||||||
def reset_timings(self):
|
|
||||||
self.times = []
|
|
||||||
|
|
||||||
def _request(self, method, url, **kwargs):
|
def _request(self, method, url, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -206,8 +186,7 @@ class DynClient:
|
||||||
else:
|
else:
|
||||||
self._http_log_req(method, url, kwargs)
|
self._http_log_req(method, url, kwargs)
|
||||||
|
|
||||||
if self.timings:
|
start_time = time.monotonic()
|
||||||
start_time = time.monotonic()
|
|
||||||
resp = self.http.request(method, url, **kwargs)
|
resp = self.http.request(method, url, **kwargs)
|
||||||
if self.timings:
|
if self.timings:
|
||||||
self.times.append((f"{method} {url}",
|
self.times.append((f"{method} {url}",
|
||||||
|
@ -215,35 +194,10 @@ class DynClient:
|
||||||
self._http_log_resp(resp)
|
self._http_log_resp(resp)
|
||||||
|
|
||||||
if resp.status_code >= 400:
|
if resp.status_code >= 400:
|
||||||
LOG.debug(
|
LOG.debug('Request returned failure status: %s', resp.status_code)
|
||||||
"Request returned failure status: %s",
|
|
||||||
resp.status_code)
|
|
||||||
raise DynClientError.from_response(resp)
|
raise DynClientError.from_response(resp)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
def poll_response(self, response):
|
|
||||||
"""
|
|
||||||
The API might return a job nr in the response in case of a async
|
|
||||||
response: https://github.com/fog/fog/issues/575
|
|
||||||
"""
|
|
||||||
status = response.status
|
|
||||||
|
|
||||||
timeout = Timeout(CONF[CFG_GROUP_NAME].job_timeout)
|
|
||||||
try:
|
|
||||||
while status == 307:
|
|
||||||
time.sleep(1)
|
|
||||||
url = response.headers.get('Location')
|
|
||||||
LOG.debug("Polling %s", url)
|
|
||||||
|
|
||||||
polled_response = self.get(url)
|
|
||||||
status = response.status
|
|
||||||
except Timeout as t:
|
|
||||||
if t == timeout:
|
|
||||||
raise DynTimeoutError('Timeout reached when pulling job.')
|
|
||||||
finally:
|
|
||||||
timeout.cancel()
|
|
||||||
return polled_response
|
|
||||||
|
|
||||||
def request(self, method, url, retries=2, **kwargs):
|
def request(self, method, url, retries=2, **kwargs):
|
||||||
if self.token is None and not self.authing:
|
if self.token is None and not self.authing:
|
||||||
self.login()
|
self.login()
|
||||||
|
@ -258,9 +212,6 @@ class DynClient:
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
if response.status_code == 307:
|
|
||||||
response = self.poll_response(response)
|
|
||||||
|
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
def login(self):
|
def login(self):
|
||||||
|
@ -282,18 +233,10 @@ class DynClient:
|
||||||
response = self.request('POST', *args, **kwargs)
|
response = self.request('POST', *args, **kwargs)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def get(self, *args, **kwargs):
|
|
||||||
response = self.request('GET', *args, **kwargs)
|
|
||||||
return response
|
|
||||||
|
|
||||||
def put(self, *args, **kwargs):
|
def put(self, *args, **kwargs):
|
||||||
response = self.request('PUT', *args, **kwargs)
|
response = self.request('PUT', *args, **kwargs)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def patch(self, *args, **kwargs):
|
|
||||||
response = self.request('PATCH', *args, **kwargs)
|
|
||||||
return response
|
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
response = self.request('DELETE', *args, **kwargs)
|
response = self.request('DELETE', *args, **kwargs)
|
||||||
return response
|
return response
|
||||||
|
@ -330,8 +273,13 @@ class DynECTBackend(base.Backend):
|
||||||
timings=CONF[CFG_GROUP_NAME].timings)
|
timings=CONF[CFG_GROUP_NAME].timings)
|
||||||
|
|
||||||
def create_zone(self, context, zone):
|
def create_zone(self, context, zone):
|
||||||
LOG.info('Creating zone %(d_id)s / %(d_name)s',
|
LOG.info(
|
||||||
{'d_id': zone['id'], 'd_name': zone['name']})
|
'Creating zone %(d_id)s / %(d_name)s',
|
||||||
|
{
|
||||||
|
'd_id': zone['id'],
|
||||||
|
'd_name': zone['name']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
url = '/Secondary/%s' % zone['name'].rstrip('.')
|
url = '/Secondary/%s' % zone['name'].rstrip('.')
|
||||||
data = {
|
data = {
|
||||||
|
@ -351,8 +299,10 @@ class DynECTBackend(base.Backend):
|
||||||
except DynClientError as e:
|
except DynClientError as e:
|
||||||
for emsg in e.msgs:
|
for emsg in e.msgs:
|
||||||
if emsg['ERR_CD'] == 'TARGET_EXISTS':
|
if emsg['ERR_CD'] == 'TARGET_EXISTS':
|
||||||
LOG.info("Zone already exists, updating existing "
|
LOG.info(
|
||||||
"zone instead %s", zone['name'])
|
'Zone already exists, updating existing zone '
|
||||||
|
'instead %s', zone['name']
|
||||||
|
)
|
||||||
client.put(url, data=data)
|
client.put(url, data=data)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
@ -362,17 +312,25 @@ class DynECTBackend(base.Backend):
|
||||||
client.logout()
|
client.logout()
|
||||||
|
|
||||||
def delete_zone(self, context, zone, zone_params=None):
|
def delete_zone(self, context, zone, zone_params=None):
|
||||||
LOG.info('Deleting zone %(d_id)s / %(d_name)s',
|
LOG.info(
|
||||||
{'d_id': zone['id'], 'd_name': zone['name']})
|
'Deleting zone %(d_id)s / %(d_name)s', {
|
||||||
|
'd_id': zone['id'],
|
||||||
|
'd_name': zone['name']
|
||||||
|
}
|
||||||
|
)
|
||||||
url = '/Zone/%s' % zone['name'].rstrip('.')
|
url = '/Zone/%s' % zone['name'].rstrip('.')
|
||||||
client = self.get_client()
|
client = self.get_client()
|
||||||
try:
|
try:
|
||||||
client.delete(url)
|
client.delete(url)
|
||||||
except DynClientError as e:
|
except DynClientError as e:
|
||||||
if e.http_status == 404:
|
if e.http_status == 404:
|
||||||
LOG.warning("Attempt to delete %(d_id)s / %(d_name)s "
|
LOG.warning(
|
||||||
"caused 404, ignoring.",
|
'Attempt to delete %(d_id)s / %(d_name)s caused 404, '
|
||||||
{'d_id': zone['id'], 'd_name': zone['name']})
|
'ignoring.', {
|
||||||
|
'd_id': zone['id'],
|
||||||
|
'd_name': zone['name']
|
||||||
|
}
|
||||||
|
)
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
|
@ -14,14 +14,21 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
from oslo_config import fixture as cfg_fixture
|
||||||
import oslotest.base
|
import oslotest.base
|
||||||
import requests_mock
|
import requests_mock
|
||||||
|
|
||||||
from designate.backend import impl_dynect
|
from designate.backend import impl_dynect
|
||||||
|
import designate.conf
|
||||||
from designate import context
|
from designate import context
|
||||||
|
from designate import exceptions
|
||||||
from designate import objects
|
from designate import objects
|
||||||
|
from designate.tests import base_fixtures
|
||||||
|
|
||||||
|
CONF = designate.conf.CONF
|
||||||
|
|
||||||
MASTERS = [
|
MASTERS = [
|
||||||
'192.0.2.1'
|
'192.0.2.1'
|
||||||
|
@ -146,10 +153,33 @@ ACTIVATE_SUCCESS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DynClientTestsCase(oslotest.base.BaseTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
def test_dyn_client_auth_error(self):
|
||||||
|
client = impl_dynect.DynClient(
|
||||||
|
'customer_name', 'user_name', 'password', 'timeout', 'timings'
|
||||||
|
)
|
||||||
|
client.token = 'fake'
|
||||||
|
client._request = mock.Mock()
|
||||||
|
client._request.side_effect = impl_dynect.DynClientAuthError()
|
||||||
|
|
||||||
|
self.assertRaisesRegex(
|
||||||
|
impl_dynect.DynClientAuthError,
|
||||||
|
r'None \(HTTP None to None - None\) - None',
|
||||||
|
client.request, 'POST', '/Test/'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DynECTTestsCase(oslotest.base.BaseTestCase):
|
class DynECTTestsCase(oslotest.base.BaseTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
|
self.useFixture(cfg_fixture.Config(CONF))
|
||||||
|
self.stdlog = base_fixtures.StandardLogging()
|
||||||
|
self.useFixture(self.stdlog)
|
||||||
|
|
||||||
self.context = mock.Mock()
|
self.context = mock.Mock()
|
||||||
self.admin_context = mock.Mock()
|
self.admin_context = mock.Mock()
|
||||||
mock.patch.object(
|
mock.patch.object(
|
||||||
|
@ -181,6 +211,17 @@ class DynECTTestsCase(oslotest.base.BaseTestCase):
|
||||||
objects.PoolTarget.from_dict(self.target)
|
objects.PoolTarget.from_dict(self.target)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_masters_only_allow_port_53(self):
|
||||||
|
self.target['masters'] = [
|
||||||
|
{'host': '192.0.2.1', 'port': 5354}
|
||||||
|
]
|
||||||
|
self.assertRaisesRegex(
|
||||||
|
exceptions.ConfigurationError,
|
||||||
|
'DynECT only supports mDNS instances on port 53',
|
||||||
|
impl_dynect.DynECTBackend,
|
||||||
|
objects.PoolTarget.from_dict(self.target)
|
||||||
|
)
|
||||||
|
|
||||||
@requests_mock.mock()
|
@requests_mock.mock()
|
||||||
def test_create_zone(self, req_mock):
|
def test_create_zone(self, req_mock):
|
||||||
req_mock.post(
|
req_mock.post(
|
||||||
|
@ -205,6 +246,65 @@ class DynECTTestsCase(oslotest.base.BaseTestCase):
|
||||||
|
|
||||||
self.backend.create_zone(self.context, self.zone)
|
self.backend.create_zone(self.context, self.zone)
|
||||||
|
|
||||||
|
@requests_mock.mock()
|
||||||
|
def test_create_zone_with_timings(self, req_mock):
|
||||||
|
CONF.set_override('timings', True, 'backend:dynect')
|
||||||
|
|
||||||
|
req_mock.post(
|
||||||
|
'%s/Session' % self.base_address,
|
||||||
|
json=LOGIN_SUCCESS,
|
||||||
|
)
|
||||||
|
req_mock.delete(
|
||||||
|
'%s/Session' % self.base_address,
|
||||||
|
json=LOGOUT_SUCCESS,
|
||||||
|
)
|
||||||
|
|
||||||
|
req_mock.post(
|
||||||
|
'%s/Secondary/example.com' % self.base_address,
|
||||||
|
json=ZONE_CREATE,
|
||||||
|
status_code=200,
|
||||||
|
)
|
||||||
|
req_mock.put(
|
||||||
|
'%s/Secondary/example.com' % self.base_address,
|
||||||
|
json=ACTIVATE_SUCCESS,
|
||||||
|
status_code=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.backend.create_zone(self.context, self.zone)
|
||||||
|
|
||||||
|
@requests_mock.mock()
|
||||||
|
def test_create_zone_missing_contact_and_tsig(self, req_mock):
|
||||||
|
self.target['options'] = [
|
||||||
|
{'key': 'username', 'value': 'example'},
|
||||||
|
{'key': 'password', 'value': 'secret'},
|
||||||
|
]
|
||||||
|
|
||||||
|
backend = impl_dynect.DynECTBackend(
|
||||||
|
objects.PoolTarget.from_dict(self.target)
|
||||||
|
)
|
||||||
|
|
||||||
|
req_mock.post(
|
||||||
|
'%s/Session' % self.base_address,
|
||||||
|
json=LOGIN_SUCCESS,
|
||||||
|
)
|
||||||
|
req_mock.delete(
|
||||||
|
'%s/Session' % self.base_address,
|
||||||
|
json=LOGOUT_SUCCESS,
|
||||||
|
)
|
||||||
|
|
||||||
|
req_mock.post(
|
||||||
|
'%s/Secondary/example.com' % self.base_address,
|
||||||
|
json=ZONE_CREATE,
|
||||||
|
status_code=200,
|
||||||
|
)
|
||||||
|
req_mock.put(
|
||||||
|
'%s/Secondary/example.com' % self.base_address,
|
||||||
|
json=ACTIVATE_SUCCESS,
|
||||||
|
status_code=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
backend.create_zone(self.context, self.zone)
|
||||||
|
|
||||||
@requests_mock.mock()
|
@requests_mock.mock()
|
||||||
def test_create_zone_raise_dynclienterror(self, req_mock):
|
def test_create_zone_raise_dynclienterror(self, req_mock):
|
||||||
# https://api.dynect.net:443/REST/Session
|
# https://api.dynect.net:443/REST/Session
|
||||||
|
@ -273,6 +373,36 @@ class DynECTTestsCase(oslotest.base.BaseTestCase):
|
||||||
|
|
||||||
self.backend.delete_zone(self.context, self.zone)
|
self.backend.delete_zone(self.context, self.zone)
|
||||||
|
|
||||||
|
@requests_mock.mock()
|
||||||
|
def test_delete_zone_not_found(self, req_mock):
|
||||||
|
req_mock.post(
|
||||||
|
'%s/Session' % self.base_address,
|
||||||
|
json=LOGIN_SUCCESS,
|
||||||
|
)
|
||||||
|
req_mock.delete(
|
||||||
|
'%s/Session' % self.base_address,
|
||||||
|
json=LOGOUT_SUCCESS,
|
||||||
|
)
|
||||||
|
|
||||||
|
req_mock.delete(
|
||||||
|
'%s/Zone/example.com' % self.base_address,
|
||||||
|
json=INVALID_ZONE_DELETE_DATA,
|
||||||
|
status_code=404
|
||||||
|
)
|
||||||
|
req_mock.put(
|
||||||
|
'%s/Secondary/example.com' % self.base_address,
|
||||||
|
json=ACTIVATE_SUCCESS,
|
||||||
|
status_code=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.backend.delete_zone(self.context, self.zone)
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
'Attempt to delete e2bed4dc-9d01-11e4-89d3-123b93f75cba / '
|
||||||
|
'example.com. caused 404, ignoring.',
|
||||||
|
self.stdlog.logger.output
|
||||||
|
)
|
||||||
|
|
||||||
@requests_mock.mock()
|
@requests_mock.mock()
|
||||||
def test_delete_zone_raise_dynclienterror(self, req_mock):
|
def test_delete_zone_raise_dynclienterror(self, req_mock):
|
||||||
req_mock.post(
|
req_mock.post(
|
||||||
|
@ -300,6 +430,28 @@ class DynECTTestsCase(oslotest.base.BaseTestCase):
|
||||||
self.backend.delete_zone, self.context, self.zone,
|
self.backend.delete_zone, self.context, self.zone,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@requests_mock.mock()
|
||||||
|
def test_request(self, req_mock):
|
||||||
|
req_mock.post(
|
||||||
|
'%s/Session' % self.base_address,
|
||||||
|
json=LOGIN_SUCCESS,
|
||||||
|
)
|
||||||
|
req_mock.post(
|
||||||
|
'https://api.dynect.net:443/'
|
||||||
|
)
|
||||||
|
req_mock.post(
|
||||||
|
'https://api.dynect.net:443/REST'
|
||||||
|
)
|
||||||
|
|
||||||
|
client = self.backend.get_client()
|
||||||
|
|
||||||
|
self.assertEqual(200,
|
||||||
|
client._request(
|
||||||
|
'POST',
|
||||||
|
'https://api.dynect.net:443/REST').status_code
|
||||||
|
)
|
||||||
|
self.assertEqual(200, client._request('POST', '').status_code)
|
||||||
|
|
||||||
def test_error_from_response(self):
|
def test_error_from_response(self):
|
||||||
error_data = dict(INVALID_ZONE_DELETE_DATA)
|
error_data = dict(INVALID_ZONE_DELETE_DATA)
|
||||||
mock_response = mock.Mock()
|
mock_response = mock.Mock()
|
||||||
|
|
Loading…
Reference in New Issue