Merge lp:~mpontillo/maas/dns-api-ui-consistency--bug-1686864 into lp:maas/trunk

Proposed by Mike Pontillo on 2017-04-28
Status: Rejected
Rejected by: MAAS Lander on 2017-06-22
Proposed branch: lp:~mpontillo/maas/dns-api-ui-consistency--bug-1686864
Merge into: lp:maas/trunk
Prerequisite: lp:~mpontillo/maas/dns-dhcp-fixes--bug-1686234
Diff against target: 256 lines (+117/-16)
4 files modified
src/maasserver/api/dnsresources.py (+64/-1)
src/maasserver/models/domain.py (+29/-13)
src/maasserver/models/tests/test_domain.py (+23/-1)
src/maasserver/testing/factory.py (+1/-1)
To merge this branch: bzr merge lp:~mpontillo/maas/dns-api-ui-consistency--bug-1686864
Reviewer Review Type Date Requested Status
MAAS Maintainers 2017-04-28 Pending
Review via email: mp+323361@code.launchpad.net

Commit message

Optionally allow implicit DNS records to be shown using the API.

To post a comment you must log in.
MAAS Lander (maas-lander) wrote :

Transitioned to Git.

lp:maas has now moved from Bzr to Git.
Please propose your branches with Launchpad using Git.

git clone https://git.launchpad.net/maas

Unmerged revisions

6026. By Mike Pontillo on 2017-04-28

Don't include redundant DNS data in the extra records shown in the API.

6025. By Mike Pontillo on 2017-04-28

Add a way to get implicit DNS records via the API.

6024. By Mike Pontillo on 2017-04-27

Add a way to see implicit DNS records.

6023. By Mike Pontillo on 2017-04-27

Merge trunk.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api/dnsresources.py'
2--- src/maasserver/api/dnsresources.py 2017-03-29 22:30:31 +0000
3+++ src/maasserver/api/dnsresources.py 2017-04-28 00:50:14 +0000
4@@ -2,11 +2,15 @@
5 # GNU Affero General Public License version 3 (see the file LICENSE).
6
7 """API handlers: `DNSResource`."""
8+from collections import OrderedDict
9
10+from django.db.models.query import QuerySet
11+from formencode.validators import StringBool
12 from maasserver.api.support import (
13 admin_method,
14 OperationsHandler,
15 )
16+from maasserver.api.utils import get_optional_param
17 from maasserver.enum import NODE_PERMISSION
18 from maasserver.exceptions import MAASAPIValidationError
19 from maasserver.forms.dnsresource import DNSResourceForm
20@@ -27,6 +31,40 @@
21 )
22
23
24+class DNSResourcesQuerySet(QuerySet):
25+
26+ def __iter__(self):
27+ """Custom iterator which also includes implicit DNS records."""
28+ previous_domain = None
29+ for record in super().__iter__():
30+ # This works because the query always either contains a single
31+ # domain, or is sorted by domain.
32+ if record.domain != previous_domain:
33+ previous_domain = record.domain
34+ rrdata = record.domain.render_json_for_related_rrdata(
35+ include_dnsdata=False, as_dict=True)
36+ for name, value in rrdata.items():
37+ name = name.split('.')[0]
38+ # Remove redundant info (this is provided in the top
39+ # level 'fqdn' field).
40+ if (self._name_filter is not None and
41+ name != self._name_filter):
42+ continue
43+ items = []
44+ for rr in value:
45+ if (self._rrtype_filter is not None and
46+ rr['rrtype'] != self._rrtype_filter):
47+ continue
48+ del rr['name']
49+ items.append(rr)
50+ if len(items) > 0:
51+ resource = DNSResource(
52+ id=-1, name=name, domain=record.domain)
53+ resource._rrdata = items
54+ yield resource
55+ yield record
56+
57+
58 class DNSResourcesHandler(OperationsHandler):
59 """Manage dnsresources."""
60 api_doc_section_name = "DNSResources"
61@@ -44,12 +82,18 @@
62 :param name: restrict the listing to entries of the given name.
63 :param rrtype: restrict the listing to entries which have
64 records of the given rrtype.
65+ :param all: if True, also include implicit DNS records created for
66+ nodes registered in MAAS.
67 """
68 data = request.GET
69 fqdn = data.get('fqdn', None)
70 name = data.get('name', None)
71 domainname = data.get('domain', None)
72 rrtype = data.get('rrtype', None)
73+ if rrtype is not None:
74+ rrtype = rrtype.upper()
75+ _all = get_optional_param(
76+ request.GET, 'all', default=False, validator=StringBool)
77 if domainname is None and name is None and fqdn is not None:
78 # We need a type for resource separation. If the user didn't give
79 # us a rrtype, then assume it's an address of some sort.
80@@ -66,10 +110,22 @@
81 query = domain.dnsresource_set.all().order_by('name')
82 else:
83 query = DNSResource.objects.all().order_by('domain_id', 'name')
84+ rrtype_filter = None
85+ name_filter = None
86 if name is not None:
87 query = query.filter(name=name)
88+ name_filter = name
89 if rrtype is not None:
90 query = query.filter(dnsdata__rrtype=rrtype)
91+ rrtype_filter = rrtype
92+ query = query.prefetch_related('ip_addresses', 'dnsdata_set')
93+ query = query.select_related('domain')
94+ # Note: This must be done last, otherwise our hacks to show additional
95+ # records won't work.
96+ if _all is True:
97+ query.__class__ = DNSResourcesQuerySet
98+ query._name_filter = name_filter
99+ query._rrtype_filter = rrtype_filter
100 return query
101
102 @admin_method
103@@ -137,12 +193,19 @@
104 @classmethod
105 def ip_addresses(cls, dnsresource):
106 """Return IPAddresses within the specified dnsresource."""
107+ rrdata = getattr(dnsresource, '_rrdata', None)
108+ if rrdata is not None:
109+ return None
110 return dnsresource.ip_addresses.all()
111
112 @classmethod
113 def resource_records(cls, dnsresource):
114 """Other data for this dnsresource."""
115- return dnsresource.dnsdata_set.all().order_by('rrtype')
116+ rrdata = getattr(dnsresource, '_rrdata', None)
117+ if rrdata is not None:
118+ return rrdata
119+ else:
120+ return dnsresource.dnsdata_set.all().order_by('rrtype')
121
122 def read(self, request, id):
123 """Read dnsresource.
124
125=== modified file 'src/maasserver/models/domain.py'
126--- src/maasserver/models/domain.py 2017-04-17 14:11:28 +0000
127+++ src/maasserver/models/domain.py 2017-04-28 00:50:14 +0000
128@@ -12,6 +12,7 @@
129 "validate_domain_name",
130 ]
131
132+from collections import OrderedDict, defaultdict
133 import datetime
134 import re
135
136@@ -351,7 +352,8 @@
137 super(Domain, self).clean(*args, **kwargs)
138 self.clean_name()
139
140- def render_json_for_related_rrdata(self, for_list=False):
141+ def render_json_for_related_rrdata(
142+ self, for_list=False, include_dnsdata=True, as_dict=False):
143 """Render a representation of this domain's related non-IP data,
144 suitable for converting to JSON.
145
146@@ -360,8 +362,13 @@
147 DNSData,
148 StaticIPAddress,
149 )
150- rr_mapping = DNSData.objects.get_hostname_dnsdata_mapping(
151- self, raw_ttl=True)
152+ if include_dnsdata is True:
153+ rr_mapping = DNSData.objects.get_hostname_dnsdata_mapping(
154+ self, raw_ttl=True)
155+ else:
156+ # Circular imports.
157+ from maasserver.models.dnsdata import HostnameRRsetMapping
158+ rr_mapping = defaultdict(HostnameRRsetMapping)
159 # Smash the IP Addresses in the rrset mapping, so that the far end
160 # only needs to worry about one thing.
161 ip_mapping = StaticIPAddress.objects.get_hostname_ip_mapping(
162@@ -376,16 +383,25 @@
163 rr_mapping[hostname].rrset.add((info.ttl, 'AAAA', ip))
164 else:
165 rr_mapping[hostname].rrset.add((info.ttl, 'A', ip))
166- data = []
167+ if as_dict is True:
168+ result = OrderedDict()
169+ else:
170+ result = []
171 for hostname, info in rr_mapping.items():
172- data += [{
173- 'name': hostname,
174- 'system_id': info.system_id,
175- 'node_type': info.node_type,
176- 'ttl': ttl,
177- 'rrtype': rrtype,
178- 'rrdata': rrdata
179+ data = [{
180+ 'name': hostname,
181+ 'system_id': info.system_id,
182+ 'node_type': info.node_type,
183+ 'ttl': ttl,
184+ 'rrtype': rrtype,
185+ 'rrdata': rrdata
186 }
187 for ttl, rrtype, rrdata in info.rrset
188- ]
189- return data
190+ ]
191+ if as_dict is True:
192+ existing = result.get(hostname, [])
193+ existing.extend(data)
194+ result[hostname] = existing
195+ else:
196+ result.extend(data)
197+ return result
198
199=== modified file 'src/maasserver/models/tests/test_domain.py'
200--- src/maasserver/models/tests/test_domain.py 2017-04-05 19:57:09 +0000
201+++ src/maasserver/models/tests/test_domain.py 2017-04-28 00:50:14 +0000
202@@ -32,7 +32,11 @@
203 from maasserver.testing.factory import factory
204 from maasserver.testing.testcase import MAASServerTestCase
205 from netaddr import IPAddress
206-from testtools.matchers import MatchesStructure
207+from testtools.matchers import (
208+ Equals,
209+ HasLength,
210+ MatchesStructure,
211+)
212 from testtools.testcase import ExpectedException
213
214
215@@ -372,6 +376,9 @@
216 dnsresource__domain_id=domain.id)
217 self.assertEqual("0 0 1688 %s." % target, srvrr.rrdata)
218
219+
220+class TestRenderRRData(MAASServerTestCase):
221+
222 def render_rrdata(self, domain, for_list=False):
223 rr_map = DNSData.objects.get_hostname_dnsdata_mapping(
224 domain, raw_ttl=True)
225@@ -414,3 +421,18 @@
226 expected = self.render_rrdata(domain, for_list=False)
227 actual = domain.render_json_for_related_rrdata(for_list=False)
228 self.assertItemsEqual(expected, actual)
229+
230+ def test_renders_as_dictionary(self):
231+ domain = factory.make_Domain()
232+ name1 = factory.make_name(prefix='a')
233+ name2 = factory.make_name(prefix='b')
234+ factory.make_DNSData(name=name1, domain=domain, rrtype='MX')
235+ rrdata_list = domain.render_json_for_related_rrdata(as_dict=False)
236+ rrdata_dict = domain.render_json_for_related_rrdata(as_dict=True)
237+ self.assertThat(rrdata_dict[name1], Equals([rrdata_list[0]]))
238+ self.assertThat(rrdata_dict[name1], HasLength(1))
239+ factory.make_DNSData(name=name1, domain=domain, rrtype='MX')
240+ factory.make_DNSData(name=name2, domain=domain, rrtype='NS')
241+ rrdata_dict = domain.render_json_for_related_rrdata(as_dict=True)
242+ self.assertThat(rrdata_dict[name1], HasLength(2))
243+ self.assertThat(rrdata_dict[name2], HasLength(1))
244
245=== modified file 'src/maasserver/testing/factory.py'
246--- src/maasserver/testing/factory.py 2017-04-28 00:50:13 +0000
247+++ src/maasserver/testing/factory.py 2017-04-28 00:50:14 +0000
248@@ -655,7 +655,7 @@
249 else ip
250 for ip in ip_addresses
251 ]
252- dnsrr = DNSResource(
253+ dnsrr, _ = DNSResource.objects.get_or_create(
254 name=name, address_ttl=address_ttl,
255 domain=domain)
256 dnsrr.save()