Source code for designate.backend.impl_dynect

# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# Author: Endre Karlson <endre.karlson@hpe.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import time

from eventlet import Timeout
from oslo_config import cfg
from oslo_log import log as logging
from oslo_serialization import jsonutils
import requests
from requests.adapters import HTTPAdapter

from designate.backend import base
from designate import exceptions
from designate import utils

LOG = logging.getLogger(__name__)
CONF = cfg.CONF
CFG_GROUP_NAME = 'backend:dynect'


[docs]class DynClientError(exceptions.Backend): """The base exception class for all HTTP exceptions. """ def __init__(self, data=None, job_id=None, msgs=None, http_status=None, url=None, method=None, details=None): self.data = data self.job_id = job_id self.msgs = msgs self.http_status = http_status self.url = url self.method = method self.details = details formatted_string = "%s (HTTP %s to %s - %s) - %s" % (self.msgs, self.method, self.url, self.http_status, self.details) if job_id: formatted_string += " (Job-ID: %s)" % job_id super(DynClientError, self).__init__(formatted_string)
[docs] @staticmethod def from_response(response, details=None): data = response.json() exc_kwargs = dict( data=data['data'], job_id=data['job_id'], msgs=data['msgs'], http_status=response.status_code, url=response.url, method=response.request.method, details=details) for msg in data.get('msgs', []): if msg['INFO'].startswith('login:'): raise DynClientAuthError(**exc_kwargs) elif 'Operation blocked' in msg['INFO']: raise DynClientOperationBlocked(**exc_kwargs) return DynClientError(**exc_kwargs)
[docs]class DynClientAuthError(DynClientError): pass
[docs]class DynTimeoutError(exceptions.Backend): """ A job timedout. """ error_code = 408 error_type = 'dyn_timeout'
[docs]class DynClientOperationBlocked(exceptions.BadRequest, DynClientError): error_type = 'operation_blocked'
[docs]class DynClient(object): """ DynECT service client. https://help.dynect.net/rest/ """ def __init__(self, customer_name, user_name, password, endpoint="https://api.dynect.net:443", api_version='3.5.6', headers=None, verify=True, retries=1, timeout=10, timings=False, pool_maxsize=10, pool_connections=10): self.customer_name = customer_name self.user_name = user_name self.password = password self.endpoint = endpoint self.api_version = api_version self.times = [] # [("item", starttime, endtime), ...] self.timings = timings self.timeout = timeout self.authing = False self.token = None session = requests.Session() session.verify = verify session.headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', 'API-Version': api_version, 'User-Agent': 'DynECTClient'} if headers is not None: session.headers.update(headers) adapter = HTTPAdapter(max_retries=int(retries), pool_maxsize=int(pool_maxsize), pool_connections=int(pool_connections), pool_block=True) session.mount(endpoint, adapter) self.http = session def _http_log_req(self, method, url, kwargs): string_parts = [ "curl -i", "-X '%s'" % method, "'%s'" % url, ] for element in kwargs['headers']: header = "-H '%s: %s'" % (element, kwargs['headers'][element]) string_parts.append(header) LOG.debug("REQ: %s", " ".join(string_parts)) if 'data' in kwargs: LOG.debug("REQ BODY: %s\n" % (kwargs['data'])) def _http_log_resp(self, resp): LOG.debug( "RESP: [%s] %s\n" % (resp.status_code, resp.headers)) if resp._content_consumed: LOG.debug( "RESP BODY: %s\n" % resp.text)
[docs] def get_timings(self): return self.times
[docs] def reset_timings(self): self.times = []
def _request(self, method, url, **kwargs): """ Low level request helper that actually executes the request towards a wanted URL. This does NOT do any authentication. """ # NOTE: Allow passing the url as just the path or a full url if not url.startswith('http'): if not url.startswith('/REST'): url = '/REST' + url url = self.endpoint + url kwargs.setdefault("headers", kwargs.get("headers", {})) kwargs['proxies'] = utils.get_proxies() if self.token is not None: kwargs['headers']['Auth-Token'] = self.token if self.timeout is not None: kwargs.setdefault("timeout", self.timeout) data = kwargs.get('data') if data is not None: kwargs['data'] = data.copy() # NOTE: We don't want to log the credentials (password) that are # used in a auth request. if 'password' in kwargs['data']: kwargs['data']['password'] = '**SECRET**' self._http_log_req(method, url, kwargs) # NOTE: Set it back to the original data and serialize it. kwargs['data'] = jsonutils.dump_as_bytes(data) else: self._http_log_req(method, url, kwargs) if self.timings: start_time = time.time() resp = self.http.request(method, url, **kwargs) if self.timings: self.times.append(("%s %s" % (method, url), start_time, time.time())) self._http_log_resp(resp) if resp.status_code >= 400: LOG.debug( "Request returned failure status: %s", resp.status_code) raise DynClientError.from_response(resp) return resp
[docs] 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
[docs] def request(self, method, url, retries=2, **kwargs): if self.token is None and not self.authing: self.login() try: response = self._request(method, url, **kwargs) except DynClientAuthError: if retries > 0: self.token = None retries = retries - 1 return self.request(method, url, retries, **kwargs) else: raise if response.status_code == 307: response = self.poll_response(response) return response.json()
[docs] def login(self): self.authing = True data = { 'customer_name': self.customer_name, 'user_name': self.user_name, 'password': self.password } response = self.post('/Session', data=data) self.token = response['data']['token'] self.authing = False
[docs] def logout(self): self.delete('/Session') self.token = None
[docs] def post(self, *args, **kwargs): response = self.request('POST', *args, **kwargs) return response
[docs] def get(self, *args, **kwargs): response = self.request('GET', *args, **kwargs) return response
[docs] def put(self, *args, **kwargs): response = self.request('PUT', *args, **kwargs) return response
[docs] def patch(self, *args, **kwargs): response = self.request('PATCH', *args, **kwargs) return response
[docs] def delete(self, *args, **kwargs): response = self.request('DELETE', *args, **kwargs) return response
[docs]class DynECTBackend(base.Backend): """ Support for DynECT as a secondary DNS. """ __plugin_name__ = 'dynect' __backend_status__ = 'untested' def __init__(self, target): super(DynECTBackend, self).__init__(target) self.customer_name = self.options.get('customer_name') self.username = self.options.get('username') self.password = self.options.get('password') self.contact_nickname = self.options.get('contact_nickname', None) self.tsig_key_name = self.options.get('tsig_key_name', None) for m in self.masters: if m.port != 53: raise exceptions.ConfigurationError( "DynECT only supports mDNS instances on port 53")
[docs] def get_client(self): return DynClient( customer_name=self.customer_name, user_name=self.username, password=self.password, timeout=CONF[CFG_GROUP_NAME].timeout, timings=CONF[CFG_GROUP_NAME].timings)
[docs] def create_zone(self, context, zone): LOG.info('Creating zone %(d_id)s / %(d_name)s', {'d_id': zone['id'], 'd_name': zone['name']}) url = '/Secondary/%s' % zone['name'].rstrip('.') data = { 'masters': [m.host for m in self.masters] } if self.contact_nickname is not None: data['contact_nickname'] = self.contact_nickname if self.tsig_key_name is not None: data['tsig_key_name'] = self.tsig_key_name client = self.get_client() try: client.post(url, data=data) except DynClientError as e: for emsg in e.msgs: if emsg['ERR_CD'] == 'TARGET_EXISTS': LOG.info("Zone already exists, updating existing " "zone instead %s", zone['name']) client.put(url, data=data) break else: raise client.put(url, data={'activate': True}) client.logout()
[docs] def delete_zone(self, context, zone): LOG.info('Deleting zone %(d_id)s / %(d_name)s', {'d_id': zone['id'], 'd_name': zone['name']}) url = '/Zone/%s' % zone['name'].rstrip('.') client = self.get_client() try: client.delete(url) except DynClientError as e: if e.http_status == 404: LOG.warning("Attempt to delete %(d_id)s / %(d_name)s " "caused 404, ignoring.", {'d_id': zone['id'], 'd_name': zone['name']}) pass else: raise client.logout()