Skip to content

Commit c0df4fd

Browse files
author
Aaron Sierra
committed
dns: Add common code for domain/host naming
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
1 parent 7e48145 commit c0df4fd

18 files changed

+123
-69
lines changed

libcloud/dns/base.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def __init__(
6060
:type extra: ``dict``
6161
"""
6262
self.id = str(id) if id else None
63-
self.domain = domain
63+
self.domain = self.unrooted(domain)
6464
self.type = type
6565
self.ttl = ttl or None
6666
self.driver = driver
@@ -104,6 +104,16 @@ def __repr__(self):
104104
self.driver.name,
105105
)
106106

107+
@staticmethod
108+
def unrooted(domain):
109+
"""Return the provided domain as an unrooted domain"""
110+
return domain.rstrip(".")
111+
112+
@staticmethod
113+
def rooted(domain):
114+
"""Return the provided domain as an unrooted domain"""
115+
return domain if domain.endswith(".") else f"{domain}."
116+
107117
def prefix(self, subname):
108118
"""
109119
Accept subordinate (or identity) names in multiple convenience formats.
@@ -211,7 +221,17 @@ def __init__(
211221
:type extra: ``dict``
212222
"""
213223
self.id = str(id) if id else None
214-
self.name = name
224+
225+
# Support callers that have:
226+
# 1. used None, rather than the empty string to indicate apex/naked
227+
# records, while consistently setting the empty string.
228+
# 2. used the full hostname (i.e. including domain), rather than the
229+
# bare prefix (i.e. excluding domain)
230+
# 3. used the fully qualified domain name (i.e. including domain and
231+
# trailing dot), rather than the bare prefix.
232+
#
233+
self.name = "" if name is None else zone.prefix(name)
234+
215235
self.type = type
216236
self.data = data
217237
self.zone = zone
@@ -252,6 +272,19 @@ def _get_numeric_id(self):
252272

253273
return record_id
254274

275+
@property
276+
def hostname(self):
277+
"""Return the complete hostname, including domain, for this record."""
278+
return self.zone.hostname(self.name)
279+
280+
@property
281+
def fqdn(self):
282+
"""
283+
Return the traditional fully qualified domain name, including full-stop
284+
trailing dot, for this record.
285+
"""
286+
return self.zone.fqdn(self.name)
287+
255288
def __repr__(self):
256289
# type: () -> str
257290
zone = self.zone.domain if self.zone.domain else self.zone.id

libcloud/dns/drivers/durabledns.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ def create_zone(self, domain, type="master", ttl=None, extra=None):
293293
params = {
294294
"apiuser": self.key,
295295
"apikey": self.secret,
296-
"zonename": domain,
296+
"zonename": Zone.rooted(domain),
297297
"ttl": ttl or DEFAULT_TTL,
298298
}
299299
params.update(extra)
@@ -312,7 +312,7 @@ def create_zone(self, domain, type="master", ttl=None, extra=None):
312312
req_data = skel % (
313313
self.key,
314314
self.secret,
315-
domain,
315+
Zone.rooted(domain),
316316
extra.get("ns"),
317317
extra.get("mbox"),
318318
extra.get("refresh"),
@@ -366,6 +366,7 @@ def create_record(self, name, zone, type, data, extra=None):
366366
367367
:rtype: :class:`Record`
368368
"""
369+
name = zone.prefix(name)
369370
if extra is None:
370371
extra = RECORD_EXTRA_PARAMS_DEFAULT_VALUES
371372
else:
@@ -476,7 +477,7 @@ def update_zone(self, zone, domain, type="master", ttl=None, extra=None):
476477
params = {
477478
"apiuser": self.key,
478479
"apikey": self.secret,
479-
"zonename": domain,
480+
"zonename": Zone.rooted(domain),
480481
"ttl": ttl,
481482
}
482483
params.update(extra)
@@ -495,7 +496,7 @@ def update_zone(self, zone, domain, type="master", ttl=None, extra=None):
495496
req_data = skel % (
496497
self.key,
497498
self.secret,
498-
domain,
499+
Zone.rooted(domain),
499500
extra["ns"],
500501
extra["mbox"],
501502
extra["refresh"],
@@ -545,6 +546,7 @@ def update_record(self, record, name, type, data, extra=None):
545546
:rtype: :class:`Record`
546547
"""
547548
zone = record.zone
549+
name = zone.prefix(name)
548550
if extra is None:
549551
extra = record.extra
550552
else:

libcloud/dns/drivers/google.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ def create_zone(self, domain, type="master", ttl=None, extra=None):
185185
name = self._cleanup_domain(domain)
186186

187187
data = {
188-
"dnsName": domain,
188+
"dnsName": Zone.rooted(domain),
189189
"name": name,
190190
"description": description,
191191
}
@@ -216,6 +216,7 @@ def create_record(self, name, zone, type, data, extra=None):
216216
217217
:rtype: :class:`Record`
218218
"""
219+
name = zone.fqdn(name)
219220
ttl = data.get("ttl", 0)
220221
rrdatas = data.get("rrdatas", [])
221222

@@ -253,7 +254,7 @@ def delete_record(self, record):
253254
data = {
254255
"deletions": [
255256
{
256-
"name": record.name,
257+
"name": record.fqdn,
257258
"type": record.type,
258259
"rrdatas": record.data["rrdatas"],
259260
"ttl": record.data["ttl"],

libcloud/dns/drivers/luadns.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ def create_record(self, name, zone, type, data, extra=None):
233233
:rtype: :class:`Record`
234234
"""
235235
action = "/v1/zones/%s/records" % zone.id
236-
to_post = {"name": name, "content": data, "type": type, "zone_id": int(zone.id)}
236+
to_post = {"name": zone.fqdn(name), "content": data, "type": type, "zone_id": int(zone.id)}
237237
# ttl is required to create a record for luadns
238238
# pass it through extra like this: extra={'ttl':ttl}
239239
if extra is not None:

libcloud/dns/drivers/nsone.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ def delete_record(self, record):
190190
191191
:return: Boolean
192192
"""
193-
action = "/v1/zones/{}/{}/{}".format(record.zone.domain, record.name, record.type)
193+
action = "/v1/zones/{}/{}/{}".format(record.zone.domain, record.hostname, record.type)
194194
try:
195195
response = self.connection.request(action=action, method="DELETE")
196196
except NsOneException as e:

libcloud/dns/drivers/pointdns.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ def create_record(self, name, zone, type, data, extra=None):
280280
281281
:rtype: :class:`Record`
282282
"""
283-
r_json = {"name": name, "data": data, "record_type": type}
283+
r_json = {"name": zone.hostname(name), "data": data, "record_type": type}
284284
if extra is not None:
285285
r_json.update(extra)
286286
r_data = json.dumps({"zone_record": r_json})
@@ -354,7 +354,7 @@ def update_record(self, record, name, type, data, extra=None):
354354
:rtype: :class:`Record`
355355
"""
356356
zone = record.zone
357-
r_json = {"name": name, "data": data, "record_type": type}
357+
r_json = {"name": zone.hostname(name), "data": data, "record_type": type}
358358
if extra is not None:
359359
r_json.update(extra)
360360
r_data = json.dumps({"zone_record": r_json})

libcloud/dns/drivers/powerdns.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -185,18 +185,19 @@ def create_record(self, name, zone, type, data, extra=None):
185185
if extra is None or extra.get("ttl", None) is None:
186186
raise ValueError("PowerDNS requires a ttl value for every record")
187187

188+
hostname = zone.hostname(name)
188189
if self._pdns_version() == 3:
189190
record = {
190191
"content": data,
191192
"disabled": False,
192-
"name": name,
193+
"name": hostname,
193194
"ttl": extra["ttl"],
194195
"type": type,
195196
}
196197
payload = {
197198
"rrsets": [
198199
{
199-
"name": name,
200+
"name": hostname,
200201
"type": type,
201202
"changetype": "REPLACE",
202203
"records": [record],
@@ -212,7 +213,7 @@ def create_record(self, name, zone, type, data, extra=None):
212213
payload = {
213214
"rrsets": [
214215
{
215-
"name": name,
216+
"name": hostname,
216217
"type": type,
217218
"changetype": "REPLACE",
218219
"ttl": extra["ttl"],
@@ -426,19 +427,20 @@ def update_record(self, record, name, type, data, extra=None):
426427
if extra is None or extra.get("ttl", None) is None:
427428
raise ValueError("PowerDNS requires a ttl value for every record")
428429

430+
hostname = record.zone.hostname(name)
429431
if self._pdns_version() == 3:
430432
updated_record = {
431433
"content": data,
432434
"disabled": False,
433-
"name": name,
435+
"name": hostname,
434436
"ttl": extra["ttl"],
435437
"type": type,
436438
}
437439
payload = {
438440
"rrsets": [
439-
{"name": record.name, "type": record.type, "changetype": "DELETE"},
441+
{"name": record.hostname, "type": record.type, "changetype": "DELETE"},
440442
{
441-
"name": name,
443+
"name": hostname,
442444
"type": type,
443445
"changetype": "REPLACE",
444446
"records": [updated_record],
@@ -457,7 +459,7 @@ def update_record(self, record, name, type, data, extra=None):
457459
payload = {
458460
"rrsets": [
459461
{
460-
"name": name,
462+
"name": hostname,
461463
"type": type,
462464
"changetype": "REPLACE",
463465
"ttl": extra["ttl"],

libcloud/dns/drivers/zonomi.py

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,7 @@ def create_record(self, name, zone, type, data, extra=None):
176176
:rtype: :class:`Record`
177177
"""
178178
action = "/app/dns/dyndns.jsp?"
179-
if name:
180-
record_name = name + "." + zone.domain
181-
else:
182-
record_name = zone.domain
179+
record_name = zone.hostname(name)
183180
params = {"action": "SET", "name": record_name, "value": data, "type": type}
184181

185182
if type == "MX" and extra is not None:
@@ -237,7 +234,7 @@ def delete_record(self, record):
237234
:rtype: Bool
238235
"""
239236
action = "/app/dns/dyndns.jsp?"
240-
params = {"action": "DELETE", "name": record.name, "type": record.type}
237+
params = {"action": "DELETE", "name": record.hostname, "type": record.type}
241238
try:
242239
response = self.connection.request(action=action, params=params)
243240
except ZonomiException as e:
@@ -315,16 +312,11 @@ def _to_record(self, item, zone):
315312
else:
316313
ttl = None
317314
extra = {"ttl": ttl, "prio": item.get("prio")}
318-
if len(item["name"]) > len(zone.domain):
319-
full_domain = item["name"]
320-
index = full_domain.index("." + zone.domain)
321-
record_name = full_domain[:index]
322-
else:
323-
record_name = zone.domain
315+
rname = item["name"]
324316
rtype = item["type"]
325317
record = Record(
326-
id=self.to_default_id(zone, item["name"], rtype),
327-
name=record_name,
318+
id=self.to_default_id(zone, rname, rtype),
319+
name=rname,
328320
data=item["content"],
329321
type=rtype,
330322
zone=zone,

libcloud/test/dns/test_base.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,30 @@ def test_zone_helpers(self):
8282
self.assertEqual(zone.hostname(sub), "sub.example.com")
8383
self.assertEqual(zone.fqdn(sub), "sub.example.com.")
8484

85+
def test_record_init(self):
86+
common = {
87+
"id": None,
88+
"name": None,
89+
"type": RecordType.A,
90+
"data": "0.0.0.0",
91+
"zone": self.master_zone,
92+
"driver": self.master_zone,
93+
}
94+
95+
for apex in (None, "", "example.com", "example.com."):
96+
common["name"] = apex
97+
r1 = Record(**common)
98+
self.assertEqual(r1.name, "")
99+
self.assertEqual(r1.hostname, "example.com")
100+
self.assertEqual(r1.fqdn, "example.com.")
101+
102+
for sub in ("sub", "sub.example.com", "sub.example.com."):
103+
common["name"] = sub
104+
r2 = Record(**common)
105+
self.assertEqual(r2.name, "sub")
106+
self.assertEqual(r2.hostname, "sub.example.com")
107+
self.assertEqual(r2.fqdn, "sub.example.com.")
108+
85109
def test_export_zone_to_bind_format_slave_should_throw(self):
86110
zone = Zone(id=1, domain="example.com", type="slave", ttl=900, driver=self.driver)
87111
self.assertRaises(ValueError, zone.export_to_bind_format)
@@ -157,7 +181,7 @@ def test_export_zone_to_bind_format_success(self):
157181
def test_get_numeric_id(self):
158182
values = MOCK_RECORDS_VALUES[0].copy()
159183
values["driver"] = self.driver
160-
values["zone"] = None
184+
values["zone"] = self.master_zone
161185
record = Record(**values)
162186

163187
record.id = "abcd"

libcloud/test/dns/test_cloudflare.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def test_get_record(self):
8080
self.assertEqual(sent.url, '/client/v4/zones/1234/dns_records/364797364')
8181

8282
self.assertEqual(record.id, "364797364")
83-
self.assertIsNone(record.name)
83+
self.assertEqual(record.name, "")
8484
self.assertEqual(record.type, "A")
8585
self.assertEqual(record.data, "192.30.252.153")
8686

@@ -104,7 +104,7 @@ def test_list_records(self):
104104

105105
record = records[0]
106106
self.assertEqual(record.id, "364797364")
107-
self.assertIsNone(record.name)
107+
self.assertEqual(record.name, "")
108108
self.assertEqual(record.type, "A")
109109
self.assertEqual(record.data, "192.30.252.153")
110110
self.assertEqual(record.extra["priority"], None)
@@ -123,7 +123,7 @@ def test_list_records(self):
123123

124124
record = [r for r in records if r.type == "MX"][0]
125125
self.assertEqual(record.id, "78526")
126-
self.assertIsNone(record.name)
126+
self.assertEqual(record.name, "")
127127
self.assertEqual(record.type, "MX")
128128
self.assertEqual(record.data, "aspmx3.googlemail.com")
129129
self.assertEqual(record.extra["priority"], 30)

0 commit comments

Comments
 (0)