Ejemplo n.º 1
0
    def update_record(self, zone_id, record):
        """
        Update a record.

        @param zone_id: The zone ID
        @param record: The DNS record (DNSRecord)
        @return The DNS record (DNSRecord)
        """
        if record.id is None:
            raise DNSAPIError('Need record ID to update record!')
        self._announce('update record')
        command = self._prepare()
        command.add_simple_command('updateRecord',
                                   recordId=record.id,
                                   recorddata=_encode_record(record,
                                                             include_id=False))
        try:
            return _create_record_from_encoding(
                self._execute(command, 'updateRecordResponse', dict))
        except WSDLError as exc:
            raise_from(
                DNSAPIError('Error while updating record: {0}'.format(
                    to_native(exc))), exc)
        except WSDLNetworkError as exc:
            raise_from(
                DNSAPIError('Network error while updating record: {0}'.format(
                    to_native(exc))), exc)
Ejemplo n.º 2
0
    def add_record(self, zone_id, record):
        """
        Adds a new record to an existing zone.

        @param zone_id: The zone ID
        @param record: The DNS record (DNSRecord)
        @return The created DNS record (DNSRecord)
        """
        self._announce('add record')
        command = self._prepare()
        command.add_simple_command('addRecord',
                                   search=str(zone_id),
                                   recorddata=_encode_record(record,
                                                             include_id=False))
        try:
            return _create_record_from_encoding(
                self._execute(command, 'addRecordResponse', dict))
        except WSDLError as exc:
            raise_from(
                DNSAPIError('Error while adding record: {0}'.format(
                    to_native(exc))), exc)
        except WSDLNetworkError as exc:
            raise_from(
                DNSAPIError('Network error while adding record: {0}'.format(
                    to_native(exc))), exc)
Ejemplo n.º 3
0
    def get_zone_with_records_by_name(self,
                                      name,
                                      prefix=NOT_PROVIDED,
                                      record_type=NOT_PROVIDED):
        """
        Given a zone name, return the zone contents with records if found.

        @param name: The zone name (string)
        @param prefix: The prefix to filter for, if provided. Since None is a valid value,
                       the special constant NOT_PROVIDED indicates that we are not filtering.
        @param record_type: The record type to filter for, if provided
        @return The zone information with records (DNSZoneWithRecords), or None if not found
        """
        self._announce('get zone')
        command = self._prepare()
        command.add_simple_command('getZone', sZoneName=name)
        try:
            return _create_zone_from_encoding(self._execute(
                command, 'getZoneResponse', dict),
                                              prefix=prefix,
                                              record_type=record_type)
        except WSDLError as exc:
            if exc.error_origin == 'server' and exc.error_message == 'zone not found':
                return None
            raise_from(
                DNSAPIError('Error while getting zone: {0}'.format(
                    to_native(exc))), exc)
        except WSDLNetworkError as exc:
            raise_from(
                DNSAPIError('Network error while getting zone: {0}'.format(
                    to_native(exc))), exc)
Ejemplo n.º 4
0
def create_hosttech_api(option_provider, http_helper):
    username = option_provider.get_option('hosttech_username')
    password = option_provider.get_option('hosttech_password')
    if username is not None and password is not None:
        if not HAS_LXML_ETREE:
            raise DNSAPIError('Needs lxml Python module (pip install lxml)')

        return HostTechWSDLAPI(http_helper, username, password, debug=False)

    token = option_provider.get_option('hosttech_token')
    if token is not None:
        return HostTechJSONAPI(http_helper, token)

    raise DNSAPIError('One of hosttech_token or both hosttech_username and hosttech_password must be provided!')
Ejemplo n.º 5
0
 def _validate(self, result=None, info=None, expected=None, method='GET'):
     if info is None:
         raise DNSAPIError('Internal error: info needs to be provided')
     status = info['status']
     url = info['url']
     # Check expected status
     error_code = ERROR_CODES.get(status, UNKNOWN_ERROR)
     if expected is not None:
         if status not in expected:
             more = self._extract_error_message(result)
             raise DNSAPIError(
                 'Expected HTTP status {0} for {1} {2}, but got HTTP status {3} ({4}){5}'
                 .format(', '.join(['{0}'.format(e) for e in expected]),
                         method, url, status, error_code, more))
     else:
         if status < 200 or status >= 300:
             more = self._extract_error_message(result)
             raise DNSAPIError(
                 'Expected successful HTTP status for {0} {1}, but got HTTP status {2} ({3}){4}'
                 .format(method, url, status, error_code, more))
Ejemplo n.º 6
0
    def delete_record(self, zone_id, record):
        """
        Delete a record.

        @param zone_id: The zone ID
        @param record: The DNS record (DNSRecord)
        @return True in case of success (boolean)
        """
        if record.id is None:
            raise DNSAPIError('Need record ID to delete record!')
        dummy, info = self._delete('v1/records/{id}'.format(id=record.id), must_have_content=False, expected=[200, 404])
        return info['status'] == 200
Ejemplo n.º 7
0
    def update_record(self, zone_id, record):
        """
        Update a record.

        @param zone_id: The zone ID
        @param record: The DNS record (DNSRecord)
        @return The DNS record (DNSRecord)
        """
        if record.id is None:
            raise DNSAPIError('Need record ID to update record!')
        data = _record_to_json(record, zone_id=zone_id)
        result, info = self._put('v1/records/{id}'.format(id=record.id), data=data, expected=[200, 422])
        if info['status'] == 422:
            raise DNSAPIError(
                'The updated {type} record with value "{target}" and TTL {ttl} has not been accepted by the server{message}'.format(
                    type=record.type,
                    target=record.target,
                    ttl=record.ttl,
                    message=self._extract_only_error_message(result),
                )
            )
        return _create_record_from_json(result['record'])
Ejemplo n.º 8
0
    def delete_record(self, zone_id, record):
        """
        Delete a record.

        @param zone_id: The zone ID
        @param record: The DNS record (DNSRecord)
        @return True in case of success (boolean)
        """
        if record.id is None:
            raise DNSAPIError('Need record ID to delete record!')
        self._announce('delete record')
        command = self._prepare()
        command.add_simple_command('deleteRecord', recordId=record.id)
        try:
            return self._execute(command, 'deleteRecordResponse', bool)
        except WSDLError as exc:
            raise_from(
                DNSAPIError('Error while deleting record: {0}'.format(
                    to_native(exc))), exc)
        except WSDLNetworkError as exc:
            raise_from(
                DNSAPIError('Network error while deleting record: {0}'.format(
                    to_native(exc))), exc)
Ejemplo n.º 9
0
 def _validate(self, result=None, info=None, expected=None, method='GET'):
     super(HetznerAPI, self)._validate(result=result, info=info, expected=expected, method=method)
     if isinstance(result, dict):
         error = result.get('error')
         if isinstance(error, dict):
             status = error.get('code')
             if status is None:
                 return
             url = info['url']
             if expected is not None and status in expected:
                 return
             error_code = ERROR_CODES.get(status, UNKNOWN_ERROR)
             more = self._extract_error_message(result)
             raise DNSAPIError(
                 '{0} {1} resulted in API error {2} ({3}){4}'.format(method, url, status, error_code, more))
Ejemplo n.º 10
0
    def add_records(self, records_per_zone_id, stop_early_on_errors=True):
        """
        Add new records to an existing zone.

        @param records_per_zone_id: Maps a zone ID to a list of DNS records (DNSRecord)
        @param stop_early_on_errors: If set to ``True``, try to stop changes after the first error happens.
                                     This might only work on some APIs.
        @return A dictionary mapping zone IDs to lists of tuples ``(record, created, failed)``.
                Here ``created`` indicates whether the record was created (``True``) or not (``False``).
                If it was created, ``record`` contains the record ID and ``failed`` is ``None``.
                If it was not created, ``failed`` should be a ``DNSAPIError`` instance indicating why
                it was not created. It is possible that the API only creates records if all succeed,
                in that case ``failed`` can be ``None`` even though ``created`` is ``False``.
        """
        json_records = []
        for zone_id, records in records_per_zone_id.items():
            for record in records:
                json_records.append(_record_to_json(record, zone_id=zone_id))
        data = {'records': json_records}
        # Error 422 means that at least one of the records was not valid
        result, info = self._post('v1/records/bulk', data=data, expected=[200, 422])
        results_per_zone_id = {}
        # This is the list of invalid records that was detected before accepting the whole set
        for json_record in result.get('invalid_records') or []:
            record = _create_record_from_json(json_record, has_id=False)
            zone_id = json_record['zone_id']
            self._append(results_per_zone_id, zone_id, (record, False, DNSAPIError(
                'Creating {type} record "{target}" with TTL {ttl} for zone {zoneID} failed with unknown reason'.format(
                    type=record.type,
                    target=record.target,
                    ttl=record.ttl,
                    zoneID=zone_id))))
        # This is the list of valid records that were not processed
        for json_record in result.get('valid_records') or []:
            record = _create_record_from_json(json_record, has_id=False)
            zone_id = json_record['zone_id']
            self._append(results_per_zone_id, zone_id, (record, False, None))
        # This is the list of correctly processed records
        for json_record in result.get('records') or []:
            record = _create_record_from_json(json_record)
            zone_id = json_record['zone_id']
            self._append(results_per_zone_id, zone_id, (record, True, None))
        return results_per_zone_id
Ejemplo n.º 11
0
    def update_records(self, records_per_zone_id, stop_early_on_errors=True):
        """
        Update multiple records.

        @param records_per_zone_id: Maps a zone ID to a list of DNS records (DNSRecord)
        @param stop_early_on_errors: If set to ``True``, try to stop changes after the first error happens.
                                     This might only work on some APIs.
        @return A dictionary mapping zone IDs to lists of tuples ``(record, updated, failed)``.
                Here ``updated`` indicates whether the record was updated (``True``) or not (``False``).
                If it was not updated, ``failed`` should be a ``DNSAPIError`` instance. If it was
                updated, ``failed`` should be ``None``.  It is possible that the API only updates
                records if all succeed, in that case ``failed`` can be ``None`` even though
                ``updated`` is ``False``.
        """
        # Currently Hetzner's bulk update API seems to be broken, it always returns the error message
        # "An invalid response was received from the upstream server". That's why for now, we always
        # fall back to the default implementation.
        if True:  # pylint: disable=using-constant-test
            return super(HetznerAPI, self).update_records(records_per_zone_id, stop_early_on_errors=stop_early_on_errors)

        json_records = []
        for zone_id, records in records_per_zone_id.items():
            for record in records:
                json_records.append(_record_to_json(record, zone_id=zone_id))
        data = {'records': json_records}
        result, dummy = self._put('v1/records/bulk', data=data, expected=[200])
        results_per_zone_id = {}
        for json_record in result.get('failed_records') or []:
            record = _create_record_from_json(json_record)
            zone_id = json_record['zone_id']
            self._append(results_per_zone_id, zone_id, (record, False, DNSAPIError(
                'Updating {type} record #{id} "{target}" with TTL {ttl} for zone {zoneID} failed with unknown reason'.format(
                    type=record.type,
                    id=record.id,
                    target=record.target,
                    ttl=record.ttl,
                    zoneID=zone_id))))
        for json_record in result.get('records') or []:
            record = _create_record_from_json(json_record)
            zone_id = json_record['zone_id']
            self._append(results_per_zone_id, zone_id, (record, True, None))
        return results_per_zone_id
Ejemplo n.º 12
0
def get_prefix(normalized_zone,
               provider_information,
               normalized_record=None,
               prefix=None):
    # If normalized_record is not specified, use prefix
    if normalized_record is None:
        if prefix is not None:
            prefix = provider_information.normalize_prefix(
                normalize_dns_name(prefix))
        return (prefix + '.' +
                normalized_zone) if prefix else normalized_zone, prefix
    # Convert record to prefix
    if not normalized_record.endswith(
            '.' + normalized_zone) and normalized_record != normalized_zone:
        raise DNSAPIError('Record must be in zone')
    if normalized_record == normalized_zone:
        return normalized_record, None
    else:
        return normalized_record, normalized_record[:len(normalized_record) -
                                                    len(normalized_zone) - 1]
Ejemplo n.º 13
0
    def add_record(self, zone_id, record):
        """
        Adds a new record to an existing zone.

        @param zone_id: The zone ID
        @param record: The DNS record (DNSRecord)
        @return The created DNS record (DNSRecord)
        """
        data = _record_to_json(record, zone_id=zone_id)
        result, info = self._post('v1/records', data=data, expected=[200, 422])
        if info['status'] == 422:
            raise DNSAPIError(
                'The new {type} record with value "{target}" and TTL {ttl} has not been accepted by the server{message}'.format(
                    type=record.type,
                    target=record.target,
                    ttl=record.ttl,
                    message=self._extract_only_error_message(result),
                )
            )
        return _create_record_from_json(result['record'])
Ejemplo n.º 14
0
def _encode_record(record, include_id=False):
    result = {
        'type': record.type,
        'prefix': record.prefix,
        'target': record.target,
        'ttl': record.ttl,
    }
    if record.type in ('PTR', 'MX'):
        try:
            priority, target = record.target.split(' ', 1)
            result['priority'] = int(priority)
            result['target'] = target
        except Exception as e:
            raise DNSAPIError(
                'Cannot split {0} record "{1}" into integer priority and target: {2}'
                .format(record.type, record.target, e))
    else:
        result['priority'] = None
    if include_id:
        result['id'] = record.id
    return result
Ejemplo n.º 15
0
 def _request(self, url, **kwargs):
     """Execute a HTTP request and handle common things like rate limiting."""
     number_retries = 10
     countdown = number_retries + 1
     while True:
         content, info = self._http_helper.fetch_url(url, **kwargs)
         countdown -= 1
         if info['status'] == 429:
             if countdown <= 0:
                 break
             try:
                 retry_after = max(
                     min(float(_get_header_value(info, 'retry-after')), 60),
                     1)
             except (ValueError, TypeError):
                 retry_after = 10
             time.sleep(retry_after)
             continue
         return content, info
     raise DNSAPIError(
         'Stopping after {0} failed retries with 429 Too Many Attempts'.
         format(number_retries))
Ejemplo n.º 16
0
 def _execute(self, command, result_name, acceptable_types):
     if self._debug:
         pass
         # q.q('Request: {0}'.format(command))
     try:
         result = command.execute(debug=self._debug)
     except WSDLError as e:
         if e.error_code == '998':
             raise DNSAPIAuthenticationError(
                 'Error on authentication ({0})'.format(e.error_message))
         raise
     res = result.get_result(result_name)
     if isinstance(res, acceptable_types):
         if self._debug:
             pass
             # q.q('Extracted result: {0} (type {1})'.format(res, type(res)))
         return res
     if self._debug:
         pass
         # q.q('Result: {0}; extracted type {1}'.format(result, type(res)))
     raise DNSAPIError(
         'Result has unexpected type {0} (expecting {1})!'.format(
             type(res), acceptable_types))
Ejemplo n.º 17
0
 def _process_json_result(self,
                          content,
                          info,
                          must_have_content=True,
                          method='GET',
                          expected=None):
     if isinstance(must_have_content, (list, tuple)):
         must_have_content = info['status'] in must_have_content
     # Check for unauthenticated
     if info['status'] == 401:
         message = 'Unauthorized: the authentication parameters are incorrect (HTTP status 401)'
         try:
             body = json.loads(content.decode('utf8'))
             if body['message']:
                 message = '{0}: {1}'.format(message, body['message'])
         except Exception:
             pass
         raise DNSAPIAuthenticationError(message)
     if info['status'] == 403:
         message = 'Forbidden: you do not have access to this resource (HTTP status 403)'
         try:
             body = json.loads(content.decode('utf8'))
             if body['message']:
                 message = '{0}: {1}'.format(message, body['message'])
         except Exception:
             pass
         raise DNSAPIAuthenticationError(message)
     # Check Content-Type header
     content_type = _get_header_value(info, 'content-type')
     if content_type != 'application/json' and (
             content_type is None
             or not content_type.startswith('application/json;')):
         if must_have_content:
             raise DNSAPIError(
                 '{0} {1} did not yield JSON data, but HTTP status code {2} with Content-Type "{3}" and data: {4}'
                 .format(method, info['url'], info['status'], content_type,
                         to_native(content)))
         self._validate(result=content,
                        info=info,
                        expected=expected,
                        method=method)
         return None, info
     # Decode content as JSON
     try:
         result = json.loads(content.decode('utf8'))
     except Exception:
         if must_have_content:
             raise DNSAPIError(
                 '{0} {1} did not yield JSON data, but HTTP status code {2} with data: {3}'
                 .format(method, info['url'], info['status'],
                         to_native(content)))
         self._validate(result=content,
                        info=info,
                        expected=expected,
                        method=method)
         return None, info
     self._validate(result=result,
                    info=info,
                    expected=expected,
                    method=method)
     return result, info