Example #1
0
 def process_value_to_user(self, record_type, value):
     """
     Process a record value (string) for sending to the user.
     """
     record = DNSRecord()
     record.type = record_type
     record.target = value
     self.process_to_user(record)
     return record.target
Example #2
0
 def process_value_from_user(self, record_type, value):
     """
     Process a record value (string) after receiving from the user.
     """
     record = DNSRecord()
     record.type = record_type
     record.target = value
     self.process_from_user(record)
     return record.target
Example #3
0
def test_unknown_records():
    data = {
        "id": 17,
        "type": "unknown",
        "name": "",
        "ttl": 3600,
        "comment": "my first record",
    }
    with pytest.raises(DNSAPIError) as exc:
        _create_record_from_json(data)
    assert exc.value.args[0] == 'Cannot parse unknown record type: unknown'

    record = DNSRecord()
    record.type = 'unknown'
    with pytest.raises(DNSAPIError) as exc:
        _record_to_json(record)
    assert exc.value.args[0] == 'Cannot serialize unknown record type: unknown'
Example #4
0
def _create_record_from_encoding(source, type=None):
    source = dict(source)
    result = DNSRecord()
    result.id = source.pop('id')
    result.type = source.pop('type', type)
    result.prefix = source.pop('prefix', None)
    ttl = source.pop('ttl')
    result.ttl = int(ttl) if ttl is not None else None
    priority = source.pop('priority')
    target = source.pop('target')
    if result.type in ('PTR', 'MX'):
        result.target = '{0} {1}'.format(priority, target)
    else:
        result.target = target
    source.pop('zone', None)
    result.extra['comment'] = source.pop('comment') or ''
    result.extra.update(source)
    return result
Example #5
0
def _create_record_from_json(source, type=None, has_id=True):
    source = dict(source)
    result = DNSRecord()
    if has_id:
        result.id = source.pop('id')
    result.type = source.pop('type', type)
    result.ttl = source.pop('ttl', None)
    name = source.pop('name', None)
    if name == '@':
        name = None
    result.prefix = name
    result.target = source.pop('value')
    source.pop('zone_id', None)
    result.extra.update(source)
    return result
Example #6
0
def test_update_id_delete():
    api = HetznerAPI(MagicMock(), '123')
    with pytest.raises(DNSAPIError) as exc:
        api.delete_record(1, DNSRecord())
    assert exc.value.args[0] == 'Need record ID to delete record!'
def run_module(module, create_api, provider_information):
    option_provider = ModuleOptionProvider(module)
    record_converter = RecordConverter(provider_information, option_provider)

    record_in = normalize_dns_name(module.params.get('record'))
    prefix_in = module.params.get('prefix')
    type_in = module.params.get('type')
    try:
        # Create API
        api = create_api()

        # Get zone information
        if module.params.get('zone_name') is not None:
            zone_in = normalize_dns_name(module.params.get('zone_name'))
            record_in, prefix = get_prefix(
                normalized_zone=zone_in,
                normalized_record=record_in,
                prefix=prefix_in,
                provider_information=provider_information)
            zone = api.get_zone_with_records_by_name(zone_in,
                                                     prefix=prefix,
                                                     record_type=type_in)
            if zone is None:
                module.fail_json(msg='Zone not found')
            zone_id = zone.zone.id
            records = zone.records
        elif record_in is not None:
            zone = api.get_zone_with_records_by_id(
                module.params.get('zone_id'),
                record_type=type_in,
                prefix=provider_information.normalize_prefix(prefix_in)
                if prefix_in is not None else NOT_PROVIDED,
            )
            if zone is None:
                module.fail_json(msg='Zone not found')
            zone_in = normalize_dns_name(zone.zone.name)
            record_in, prefix = get_prefix(
                normalized_zone=zone_in,
                normalized_record=record_in,
                prefix=prefix_in,
                provider_information=provider_information)
            zone_id = zone.zone.id
            records = zone.records
        else:
            zone_id = module.params.get('zone_id')
            prefix = provider_information.normalize_prefix(prefix_in)
            records = api.get_zone_records(
                zone_id,
                record_type=type_in,
                prefix=prefix,
            )
            if records is None:
                module.fail_json(msg='Zone not found')
            zone_in = None
            record_in = None

        # Find matching records
        records = filter_records(records, prefix=prefix)
        record_converter.process_multiple_from_api(records)

        # Parse records
        value_in = module.params.get('value')
        value_in = record_converter.process_value_from_user(type_in, value_in)

        # Compare records
        existing_record = None
        exact_match = False
        ttl_in = module.params.get('ttl')
        for record in records:
            if record.target == value_in:
                existing_record = record
                exact_match = record.ttl == ttl_in
                break

        before = existing_record.clone() if existing_record else None
        after = before
        changed = False

        if module.params.get('state') == 'present':
            if existing_record is None:
                # Create record
                record = DNSRecord()
                record.prefix = prefix
                record.type = type_in
                record.ttl = ttl_in
                record.target = value_in
                api_record = record_converter.clone_to_api(record)
                if not module.check_mode:
                    new_api_record = api.add_record(zone_id, api_record)
                    record = record_converter.clone_from_api(new_api_record)
                after = record
                changed = True
            elif not exact_match:
                # Update record
                record = existing_record
                record.ttl = ttl_in
                api_record = record_converter.clone_to_api(record)
                if not module.check_mode:
                    new_api_record = api.update_record(zone_id, api_record)
                    record = record_converter.clone_from_api(new_api_record)
                after = record
                changed = True
        else:
            if existing_record is not None:
                # Delete record
                api_record = record_converter.clone_to_api(record)
                if not module.check_mode:
                    api.delete_record(zone_id, api_record)
                after = None
                changed = True

        # Compose result
        result = dict(
            changed=changed,
            zone_id=zone_id,
        )
        if module._diff:
            result['diff'] = dict(
                before=format_record_for_output(
                    before,
                    record_in,
                    prefix,
                    record_converter=record_converter) if before else {},
                after=format_record_for_output(
                    after,
                    record_in,
                    prefix,
                    record_converter=record_converter) if after else {},
            )

        module.exit_json(**result)
    except DNSConversionError as e:
        module.fail_json(msg='Error while converting DNS values: {0}'.format(
            e.error_message),
                         error=e.error_message,
                         exception=traceback.format_exc())
    except DNSAPIAuthenticationError as e:
        module.fail_json(msg='Cannot authenticate: {0}'.format(e),
                         error=to_text(e),
                         exception=traceback.format_exc())
    except DNSAPIError as e:
        module.fail_json(msg='Error: {0}'.format(e),
                         error=to_text(e),
                         exception=traceback.format_exc())
Example #8
0
def test_format_records_for_output():
    A1 = DNSRecord()
    A1.type = 'A'
    A1.ttl = 300
    A1.target = '1.2.3.4'
    A1.extra['foo'] = 'bar'
    A2 = DNSRecord()
    A2.type = 'A'
    A2.ttl = 300
    A2.target = '1.2.3.5'
    A3 = DNSRecord()
    A3.type = 'A'
    A3.ttl = 3600
    A3.target = '1.2.3.6'
    AAAA = DNSRecord()
    AAAA.type = 'AAAA'
    AAAA.ttl = 600
    AAAA.target = '::1'
    AAAA2 = DNSRecord()
    AAAA2.type = 'AAAA'
    AAAA2.ttl = None
    AAAA2.target = '::2'
    assert format_records_for_output([], 'foo', '') == {
        'record': 'foo',
        'prefix': '',
        'type': None,
        'ttl': None,
        'value': [],
    }
    assert format_records_for_output([A1, A2], 'foo', 'bar') == {
        'record': 'foo',
        'prefix': 'bar',
        'type': 'A',
        'ttl': 300,
        'value': ['1.2.3.4', '1.2.3.5'],
    }
    assert format_records_for_output([A3, A1], 'foo', None) == {
        'record': 'foo',
        'prefix': '',
        'type': 'A',
        'ttl': 300,
        'ttls': [300, 3600],
        'value': ['1.2.3.6', '1.2.3.4'],
    }
    assert format_records_for_output([A3], 'foo', None) == {
        'record': 'foo',
        'prefix': '',
        'type': 'A',
        'ttl': 3600,
        'value': ['1.2.3.6'],
    }
    assert format_records_for_output([AAAA], 'foo', None) == {
        'record': 'foo',
        'prefix': '',
        'type': 'AAAA',
        'ttl': 600,
        'value': ['::1'],
    }
    assert format_records_for_output([A3, AAAA], 'foo', None) == {
        'record': 'foo',
        'prefix': '',
        'type': 'A',
        'ttl': 600,
        'ttls': [600, 3600],
        'value': ['1.2.3.6', '::1'],
    }
    assert format_records_for_output([AAAA, A3], 'foo', None) == {
        'record': 'foo',
        'prefix': '',
        'type': 'A',
        'ttl': 600,
        'ttls': [600, 3600],
        'value': ['::1', '1.2.3.6'],
    }
    assert format_records_for_output([AAAA2], 'foo', None) == {
        'record': 'foo',
        'prefix': '',
        'type': 'AAAA',
        'ttl': None,
        'value': ['::2'],
    }
    print(format_records_for_output([AAAA2, AAAA], 'foo', None))
    assert format_records_for_output([AAAA2, AAAA], 'foo', None) == {
        'record': 'foo',
        'prefix': '',
        'type': 'AAAA',
        'ttl': None,
        'ttls': [None, 600],
        'value': ['::2', '::1'],
    }
Example #9
0
def test_record_str_repr():
    A1 = DNSRecord()
    A1.prefix = None
    A1.type = 'A'
    A1.ttl = 300
    A1.target = '1.2.3.4'
    assert str(A1) == 'DNSRecord(type: A, prefix: (none), target: "1.2.3.4", ttl: 5m)'
    assert repr(A1) == 'DNSRecord(type: A, prefix: (none), target: "1.2.3.4", ttl: 5m)'
    A2 = DNSRecord()
    A2.id = 23
    A2.prefix = 'bar'
    A2.type = 'A'
    A2.ttl = 1
    A2.target = ''
    A2.extra['foo'] = 'bar'
    assert str(A2) == 'DNSRecord(id: 23, type: A, prefix: "bar", target: "", ttl: 1s, extra: {\'foo\': \'bar\'})'
    assert repr(A2) == 'DNSRecord(id: 23, type: A, prefix: "bar", target: "", ttl: 1s, extra: {\'foo\': \'bar\'})'
Example #10
0
def run_module(module, create_api, provider_information):
    option_provider = ModuleOptionProvider(module)
    record_converter = RecordConverter(provider_information, option_provider)

    record_in = normalize_dns_name(module.params.get('record'))
    prefix_in = module.params.get('prefix')
    type_in = module.params.get('type')
    try:
        # Create API
        api = create_api()

        # Get zone information
        if module.params.get('zone_name') is not None:
            zone_in = normalize_dns_name(module.params.get('zone_name'))
            record_in, prefix = get_prefix(
                normalized_zone=zone_in,
                normalized_record=record_in,
                prefix=prefix_in,
                provider_information=provider_information)
            zone = api.get_zone_with_records_by_name(zone_in,
                                                     prefix=prefix,
                                                     record_type=type_in)
            if zone is None:
                module.fail_json(msg='Zone not found')
            zone_id = zone.zone.id
            records = zone.records
        elif record_in is not None:
            zone = api.get_zone_with_records_by_id(
                module.params.get('zone_id'),
                record_type=type_in,
                prefix=provider_information.normalize_prefix(prefix_in)
                if prefix_in is not None else NOT_PROVIDED,
            )
            if zone is None:
                module.fail_json(msg='Zone not found')
            zone_in = normalize_dns_name(zone.zone.name)
            record_in, prefix = get_prefix(
                normalized_zone=zone_in,
                normalized_record=record_in,
                prefix=prefix_in,
                provider_information=provider_information)
            zone_id = zone.zone.id
            records = zone.records
        else:
            zone_id = module.params.get('zone_id')
            prefix = provider_information.normalize_prefix(prefix_in)
            records = api.get_zone_records(
                zone_id,
                record_type=type_in,
                prefix=prefix,
            )
            if records is None:
                module.fail_json(msg='Zone not found')
            zone_in = None
            record_in = None

        # Find matching records
        records = filter_records(records, prefix=prefix)
        record_converter.process_multiple_from_api(records)

        # Parse records
        values = []
        value_in = module.params.get('value') or []
        value_in = record_converter.process_values_from_user(type_in, value_in)
        values = value_in[:]

        # Compare records
        ttl_in = module.params.get('ttl')
        mismatch = False
        mismatch_records = []
        keep_records = []
        for record in records:
            if record.ttl != ttl_in:
                mismatch = True
                mismatch_records.append(record)
                continue
            val = record.target
            if val in values:
                values.remove(val)
                keep_records.append(record)
            else:
                mismatch = True
                mismatch_records.append(record)
                continue
        if values:
            mismatch = True

        before = [record.clone() for record in records]
        after = keep_records[:]

        # Determine what to do
        to_create = []
        to_delete = []
        to_change = []
        on_existing = module.params.get('on_existing')
        no_mod = False
        if module.params.get('state') == 'present':
            if records and mismatch:
                # Mismatch: user wants to overwrite?
                if on_existing == 'replace':
                    to_delete.extend(mismatch_records)
                elif on_existing == 'keep_and_fail':
                    module.fail_json(
                        msg=
                        "Record already exists with different value. Set on_existing=replace to replace it"
                    )
                elif on_existing == 'keep_and_warn':
                    module.warn(
                        "Record already exists with different value. Set on_existing=replace to replace it"
                    )
                    no_mod = True
                else:  # on_existing == 'keep'
                    no_mod = True
            if no_mod:
                after = before[:]
            else:
                for target in values:
                    if to_delete:
                        # If there's a record to delete, change it to new record
                        record = to_delete.pop()
                        to_change.append(record)
                    else:
                        # Otherwise create new record
                        record = DNSRecord()
                        to_create.append(record)
                    record.prefix = prefix
                    record.type = type_in
                    record.ttl = ttl_in
                    record.target = target
                    after.append(record)
        if module.params.get('state') == 'absent':
            if mismatch:
                # Mismatch: user wants to overwrite?
                if on_existing == 'replace':
                    no_mod = False
                elif on_existing == 'keep_and_fail':
                    module.fail_json(
                        msg=
                        "Record already exists with different value. Set on_existing=replace to remove it"
                    )
                elif on_existing == 'keep_and_warn':
                    module.warn(
                        "Record already exists with different value. Set on_existing=replace to remove it"
                    )
                    no_mod = True
                else:  # on_existing == 'keep'
                    no_mod = True
            if no_mod:
                after = before[:]
            else:
                to_delete.extend(records)
                after = []

        # Compose result
        result = dict(
            changed=False,
            zone_id=zone_id,
        )

        # Determine whether there's something to do
        if to_create or to_delete or to_change:
            # Actually do something
            records_to_delete = record_converter.clone_multiple_to_api(
                to_delete)
            records_to_change = record_converter.clone_multiple_to_api(
                to_change)
            records_to_create = record_converter.clone_multiple_to_api(
                to_create)
            result['changed'] = True
            if not module.check_mode:
                dummy, errors, success = bulk_apply_changes(
                    api,
                    zone_id=zone_id,
                    records_to_delete=records_to_delete,
                    records_to_change=records_to_change,
                    records_to_create=records_to_create,
                    provider_information=provider_information,
                    options=option_provider,
                )
                if errors:
                    if len(errors) == 1:
                        raise errors[0]
                    module.fail_json(
                        msg='Errors: {0}'.format('; '.join(
                            [str(e) for e in errors])),
                        errors=[str(e) for e in errors],
                    )

        # Include diff information
        if module._diff:
            result['diff'] = dict(
                before=(format_records_for_output(
                    sorted(before, key=lambda record: record.target),
                    record_in,
                    prefix,
                    record_converter=record_converter) if before else dict()),
                after=(format_records_for_output(
                    sorted(after, key=lambda record: record.target),
                    record_in,
                    prefix,
                    record_converter=record_converter) if after else dict()),
            )

        module.exit_json(**result)
    except DNSConversionError as e:
        module.fail_json(msg='Error while converting DNS values: {0}'.format(
            e.error_message),
                         error=e.error_message,
                         exception=traceback.format_exc())
    except DNSAPIAuthenticationError as e:
        module.fail_json(msg='Cannot authenticate: {0}'.format(e),
                         error=to_text(e),
                         exception=traceback.format_exc())
    except DNSAPIError as e:
        module.fail_json(msg='Error: {0}'.format(e),
                         error=to_text(e),
                         exception=traceback.format_exc())
Example #11
0
def test_update_id_missing():
    api = HostTechJSONAPI(MagicMock(), '123')
    with pytest.raises(DNSAPIError) as exc:
        api.update_record(1, DNSRecord())
    assert exc.value.args[0] == 'Need record ID to update record!'
Example #12
0
def test_zone_with_records_str_repr():
    Z1 = DNSZone('foo')
    Z2 = DNSZone('foo')
    Z2.id = 42
    A1 = DNSRecord()
    A1.prefix = None
    A1.type = 'A'
    A1.ttl = 300
    A1.target = '1.2.3.4'
    A2 = DNSRecord()
    A2.id = 23
    A2.prefix = 'bar'
    A2.type = 'A'
    A2.ttl = 1
    A2.target = ''
    A2.extra['foo'] = 23
    ZZ1 = DNSZoneWithRecords(Z1, [A1])
    ZZ2 = DNSZoneWithRecords(Z2, [A1, A2])
    assert str(
        ZZ1
    ) == '(DNSZone(name: foo, info: {}), [DNSRecord(type: A, prefix: (none), target: "1.2.3.4", ttl: 5m)])'
    assert repr(
        ZZ1
    ) == 'DNSZoneWithRecords(DNSZone(name: foo, info: {}), [DNSRecord(type: A, prefix: (none), target: "1.2.3.4", ttl: 5m)])'
    assert str(ZZ2) == (
        '(DNSZone(id: 42, name: foo, info: {}), [DNSRecord(type: A, prefix: (none), target: "1.2.3.4", ttl: 5m),'
        ' DNSRecord(id: 23, type: A, prefix: "bar", target: "", ttl: 1s, extra: {\'foo\': 23})])'
    )
    assert repr(ZZ2) == (
        'DNSZoneWithRecords(DNSZone(id: 42, name: foo, info: {}), [DNSRecord(type: A, prefix: (none), target: "1.2.3.4", ttl: 5m),'
        ' DNSRecord(id: 23, type: A, prefix: "bar", target: "", ttl: 1s, extra: {\'foo\': 23})])'
    )