Skip to content

Fix DNS driver API inconsistencies - #2066

Open
aaron-sierra wants to merge 52 commits into
apache:trunkfrom
aaron-sierra:dns-consistent-api
Open

aaron-sierra wants to merge 52 commits into
apache:trunkfrom
aaron-sierra:dns-consistent-api

Conversation

@aaron-sierra

@aaron-sierra aaron-sierra commented Apr 3, 2025 •

Copy link
Copy Markdown

Fix DNS driver API inconsistencies

Description

Implement #2065

Summary

In general, commit messages provide necessary background regarding the changes made and why.

  • Drop Zerigo DNS
  • Deprecate outdated DNSimple DNS driver
  • Deprecate outdated RcodeZero DNS driver
  • Ignore AuroraDNS (see Remove unavailable cloud vendors: auroracompute; InternetSolutions; dimension data #2057)
  • Add unittest API request checking to remaining drivers
  • Add and leverage common code for domain/host name manipulation
  • Add and leverage common code for <type>[:<name>]-style record IDs
  • Change Record.id format for NS1 and Zonomi providers
  • Change Record.id interpretation for World Wide DNS

Status

  • Ready for review

Checklist (tick everything that applies)

  • Code linting (required, can be done after the PR checks)
  • Documentation
  • Tests
  • ICLA (required for bigger changes)

Aaron Sierra added 3 commits March 26, 2025 18:31
The Zerigo DNS service was permanently shut down on April 30, 2017.
The RcodeZero v1 API has been shutdown:
    https://my.rcodezero.at/openapi/
@aaron-sierra
aaron-sierra force-pushed the dns-consistent-api branch 2 times, most recently from 0fe8ec0 to 46c0e4f Compare April 3, 2025 19:34
@aaron-sierra

Copy link
Copy Markdown
Author

The latest batch of updates includes changes from black and isort.

Aaron Sierra added 24 commits April 3, 2025 15:04
Enable unit tests to verify HTTP requests they submit.
* Fix test_create_zone_success
* Fix test_get_record_success
* Fix test_update_zone
Leverage the existing get_record.json for this test.
Previously, test_get_record_success() succeeded when receiving a record
completely unrelated to the originating .get_record() request. Update
the request to align with the expected response.
Previously, test_list_records_success() succeeded when receiving records
completely unrelated to the originating .list_records() request. Update
the request to align with the expected response.
Make NSOneResponse.errors and NSOneResponse.objects instance variables,
rather than class variables.

This was found due to errors in NSOneDNSDriver unit testing that only
appeared when tests were run together, not when tests were run
individually.
Add .hostname(), .fqdn(), and .prefix() helpers to the Zone class. Each
of these functions accepts a host prefix, complete name (unrooted
hostname), or FQDN (rooted hostname) associated with the Zone object and
returns the format implied by the function name.
Add .to_default_id() and .from_default_id() functions to the DNSDriver
class. These functions support the <type>[:<name>] (e.g. "A:www") record
ID format used by many of the drivers.
Previously, our NsOneDNSDriver.get_record() function performed lookups
using a `record_id` format that differed from the ID embedded within the
returned Record instance, so the driver leveraged two independent
record ID concepts simultaneously.

The NS1 API returns an ID when records are created, but that ID has no
local relevance. For example, it cannot be used to request the record
from the NS1 API.

Make the Record class .id value consistent between create_record() and
get_record(z.id, r.id).

	 |  zone_id |   z.id   |     r.id      |   record_id   |
---------|----------|----------|---------------|---------------|
previous | z.domain |  random* |    random*    |     r.type    |
    next | z.domain | z.domain | r.type:r.name | r.type:r.name |

* no meaning, effectively random
* Fix TTL in test_create_record_success() request
* Align record update with parent zone in test_update_record_success()
* Fix record_id in test_get_record_success() request
DNS AAAA records are associated with IPv6 addresses.
* Fix "ttle" typo in test_update_record()
Aaron Sierra added 11 commits April 3, 2025 15:04
Previously, the Zonomi DNS driver could only return the first record
for a given host, because it used the name of the host as Record.id.

Now, use the new default, <type>[:<name>], format to allow various
records to be differentiated for a given host.
DNS drivers share the need to manipulate host/domain names, so let's add
utility functions to the common Record and Zone classes to allow
redundant code scattered throughout the various drivers to be
consolidated into common and consistent implementations.

  * Record.name - no longer driver-dependent, now the host prefix or ""
  * Record.hostname - the complete host name, including domain part
  * Record.fqdn - the fully qualified domain name, including root dot

  * Zone.domain - no longer driver-dependent, now the unrooted domain
  * Zone.rooted() - format a (un)rooted domain as a rooted domain
  * Zone.rooted() - format a (un)rooted domain as an unrooted domain
Update testing inputs to conform to spec'ed usage.
Leverage common code from DNSDriver, Record, and Zone classes for host
name and record ID handling.
Drop RackspaceDNSDriver._to_partial_record_name() and
RackspaceDNSDriver._to_full_record_name() in favor of the common
support provided by the Record and Zone classes.
@aaron-sierra

Copy link
Copy Markdown
Author

The latest changes have been through black, isort, lint, and pylint.

@aaron-sierra
aaron-sierra marked this pull request as ready for review April 3, 2025 20:07

@micafer micafer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the extensive work on making the DNS driver APIs more consistent. The additional HTTP request assertions are particularly valuable.

I found an issue in the new Zone.prefix() implementation that should be addressed before merging.

The expression:

subname.rstrip(".").removesuffix(self.domain).rstrip(".")

removes the zone domain even when it is not preceded by a DNS label separator.

For example, with self.domain == "example.com", calling prefix("notexample.com") returns "not", even though notexample.com is not a hostname within the example.com zone.

Since Zone.hostname() and Zone.fqdn() depend on this method, the issue can silently produce incorrect DNS names.

I suggest checking for an exact domain match or the suffix "." + self.domain before stripping it, and adding regression tests for hostnames that merely share a textual suffix with the zone domain.

Additionally, this PR changes public-facing Record.id, Record.name, and Zone.domain semantics across multiple providers. As we are going to merge this to the new major version these compatibility changes will be explicitly documented and it is not majot issue.

@aaron-sierra

Copy link
Copy Markdown
Author

Hi Miguel,

Thanks for your review. In the example that you described regarding Zone.prefix() for a "sub-domain" that isn't actually related to the Zone instance, what would you suggest as reasonable output? A few options come to mind:

  1. Return None to indicate "no relationship", since the empty string is used to indicate "related, but no prefix"
    • Both None and "" are falsy, which could make it a source of error.
  2. Raise a ValueError exception (to aggressively highlight bugs?)
  3. Something else

Presumably all three of the Zone helpers that operate on sub-domains should behave similarly when offered an unrelated "sub-domain" (i.e. Zone.prefix(), Zone.hostname(), and Zone.fqdn()). Does that sound right to you?

@micafer

micafer commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

I probably prefer to return None when the domain is not related, as in the example notexample.com.
"" should only be returned when using the same domain as a prefix.
What do you think?

@micafer micafer mentioned this pull request Nov 10, 2026
2 of 7 tasks
@micafer
micafer requested a review from tisonkun November 10, 2026 09:51
@micafer

micafer commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

I probably prefer to return None when the domain is not related, as in the example notexample.com. "" should only be returned when using the same domain as a prefix. What do you think?

I thought a bit on this and probably returning the full DNS name can be the best solution: as in the example notexample.com will return notexample.com.

@micafer

micafer commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

I'm still checking this and I found two issues:

Google Cloud DNS — get_record() uses a relative name in the API query.

After parsing the normalized record ID, parts.name contains a relative name such as foo. However, the Cloud DNS API expects a fully qualified name such as foo.example.com. in the name query parameter.

I believe this should use zone.fqdn(parts.name) instead. The corresponding test currently checks the endpoint URL but does not assert the query parameters.

World Wide DNS — update_record() fails when extra=None.

The new condition if "entry" in extra raises TypeError when the optional extra argument is omitted.

Please guard against None and add a regression test covering an update without extra.

@aaron-sierra

Copy link
Copy Markdown
Author

Google Cloud DNS — get_record() uses a relative name in the API query.

After parsing the normalized record ID, parts.name contains a relative name such as foo. However, the Cloud DNS API expects a fully qualified name such as foo.example.com. in the name query parameter.

I believe this should use zone.fqdn(parts.name) instead. The corresponding test currently checks the endpoint URL but does not assert the query parameters.

You're right! I've added API parameter checking to test_get_record(), so this doesn't slip through again:

        self.assertEqual(len(sent.query["type"]), 1)
        self.assertEqual(sent.query["type"].pop(), "A")
        self.assertEqual(len(sent.query["name"]), 1)
        self.assertEqual(sent.query["name"].pop(), "foo.example.com.")

@aaron-sierra

Copy link
Copy Markdown
Author

I probably prefer to return None when the domain is not related, as in the example notexample.com. "" should only be returned when using the same domain as a prefix. What do you think?

I thought a bit on this and probably returning the full DNS name can be the best solution: as in the example notexample.com will return notexample.com.

I think we need to clarify what should happen when a hostname (unrooted) and an FQDN (rooted) are passed into these functions. In the following examples, zone.domain == "example.com".

  1. In the case of an unrelated hostname ("notexample.com") , these functions can't really do much more than treat the input as a prefix and output garbage:
    • zone.prefix("notexample.com") == "notexample.com"
    • zone.hostname("notexample.com") == "notexample.com.example.com"
    • zone.fqdn("notexample.com") == "notexample.com.example.com."
  2. In the case of an unrelated FQDN ("notexample.com.") , these functions know that the input is garbage and can return None (or raise an exception):
    • zone.prefix("notexample.com.") == None
    • zone.hostname("notexample.com.") == None
    • zone.fqdn("notexample.com.") == None

I think that I prefer raising an exception in the unrelated FQDN case, considering how #2065 notes inconsistent usage of None and the empty string by various DNS driver implementations.

@micafer

micafer commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

I think that I prefer raising an exception in the unrelated FQDN case, considering how #2065 notes inconsistent usage of None and the empty string by various DNS driver implementations.

Ok, so let's go in that direction. But we can create an specific exception (something like ZoneNameMismatchError) instead of using the generic ValueError.

@micafer

micafer commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

So please make this changes and merge the trunk branch.

@Hisernberg

Copy link
Copy Markdown
Contributor

FYI from #2216 (micafer's review thread): your NsOneResponse hunk and mine are semantically identical (class-level errors/objects → per-instance lists, set before super().__init__() since it calls success()). Whichever PR merges first, the other rebases trivially; the regression tests in #2216 (test_nsone.py) pin the shared behavior either way. No action needed from you now — just flagging so the eventual rebase is a no-op decision-wise.

@aaron-sierra

Copy link
Copy Markdown
Author

I think that I prefer raising an exception in the unrelated FQDN case, considering how #2065 notes inconsistent usage of None and the empty string by various DNS driver implementations.

Ok, so let's go in that direction. But we can create an specific exception (something like ZoneNameMismatchError) instead of using the generic ValueError.

How's ZoneNotRelatedError for clarity and specificity?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants