Merge lp:~lamont/maas/domain-detail into lp:~maas-committers/maas/trunk
- domain-detail
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | LaMont Jones |
Approved revision: | 4668 |
Merged at revision: | 4670 |
Proposed branch: | lp:~lamont/maas/domain-detail |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
1965 lines (+914/-228) 31 files modified
services/rackd/run (+1/-1) src/maasserver/api/dnsresourcerecords.py (+4/-0) src/maasserver/api/ip_addresses.py (+9/-4) src/maasserver/api/tests/test_dnsresourcerecords.py (+24/-0) src/maasserver/api/tests/test_ipaddresses.py (+59/-0) src/maasserver/dns/config.py (+13/-2) src/maasserver/dns/tests/test_config.py (+64/-4) src/maasserver/dns/tests/test_zonegenerator.py (+30/-22) src/maasserver/dns/zonegenerator.py (+17/-11) src/maasserver/models/dnsdata.py (+18/-8) src/maasserver/models/domain.py (+33/-35) src/maasserver/models/staticipaddress.py (+28/-12) src/maasserver/models/tests/test_dnsdata.py (+23/-2) src/maasserver/models/tests/test_domain.py (+31/-52) src/maasserver/models/tests/test_node.py (+2/-1) src/maasserver/models/tests/test_staticipaddress.py (+118/-28) src/maasserver/static/js/angular/controllers/domain_details.js (+61/-0) src/maasserver/static/js/angular/controllers/domains_list.js (+1/-1) src/maasserver/static/js/angular/controllers/tests/test_domain_details.js (+158/-0) src/maasserver/static/js/angular/controllers/tests/test_domains_list.js (+95/-0) src/maasserver/static/js/angular/controllers/tests/test_networks_list.js (+2/-2) src/maasserver/static/js/angular/controllers/tests/test_subnet_details.js (+2/-2) src/maasserver/static/js/angular/maas.js (+8/-0) src/maasserver/static/partials/domain-details.html (+56/-0) src/maasserver/static/partials/domains-list.html (+10/-12) src/maasserver/static/partials/subnet-details.html (+7/-2) src/maasserver/testing/factory.py (+9/-3) src/maasserver/views/combo.py (+1/-0) src/maasserver/websockets/handlers/domain.py (+4/-4) src/maasserver/websockets/handlers/tests/test_domain.py (+22/-18) src/provisioningserver/dns/tests/test_zoneconfig.py (+4/-2) |
To merge this branch: | bzr merge lp:~lamont/maas/domain-detail |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Mike Pontillo (community) | Approve | ||
Blake Rouse (community) | Needs Fixing | ||
Review via email: mp+285794@code.launchpad.net |
Commit message
Add domain-details page, read only.
Description of the change
Add domain-details page, read only.
LaMont Jones (lamont) wrote : | # |
Addressed the various.
Blake Rouse (blake-rouse) wrote : | # |
Looks almost there. Couple of issues to fix.
Blake Rouse (blake-rouse) wrote : | # |
Sorry that was wrong.
Mike Pontillo (mpontillo) wrote : | # |
Thanks for adding the tests.
My only (minor) nit was that when I read the test case, it wasn't immediately obvious why you expected it to fail. (because the Domain object didn't exist, I think) Adding a comment might help future readers.
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~lamont/maas/domain-detail into lp:maas failed. Below is the output from the failed tests.
Hit:1 http://
Get:2 http://
Hit:3 http://
Hit:4 http://
Fetched 95.8 kB in 0s (216 kB/s)
Reading package lists...
sudo DEBIAN_
--no-
Reading package lists...
Building dependency tree...
Reading state information...
apache2 is already the newest version (2.4.18-1ubuntu1).
archdetect-deb is already the newest version (1.114ubuntu1).
authbind is already the newest version (2.1.1+nmu1).
bind9 is already the newest version (1:9.9.
bind9utils is already the newest version (1:9.9.
build-essential is already the newest version (12.1ubuntu2).
curl is already the newest version (7.47.0-1ubuntu1).
debhelper is already the newest version (9.20160115ubun
dh-apport is already the newest version (2.20-0ubuntu3).
dh-systemd is already the newest version (1.28ubuntu2).
distro-info is already the newest version (0.14build1).
dnsutils is already the newest version (1:9.9.
firefox is already the newest version (44.0.2+
freeipmi-tools is already the newest version (1.4.11-1ubuntu1).
git is already the newest version (1:2.7.0-1).
isc-dhcp-common is already the newest version (4.3.3-5ubuntu4).
libjs-angularjs is already the newest version (1.2.28-1ubuntu2).
libjs-jquery is already the newest version (1.11.3+dfsg-4).
libjs-jquery-
libjs-yui3-full is already the n...
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~lamont/maas/domain-detail into lp:maas failed. Below is the output from the failed tests.
Hit:1 http://
Get:2 http://
Hit:3 http://
Hit:4 http://
Get:5 http://
Get:6 http://
Get:7 http://
Get:8 http://
Get:9 http://
Get:10 http://
Get:11 http://
Get:12 http://
Get:13 http://
Fetched 23.8 MB in 8s (2,963 kB/s)
Reading package lists...
sudo DEBIAN_
--no-
Reading package lists...
Building dependency tree...
Reading state information...
apache2 is already the newest version (2.4.18-1ubuntu1).
archdetect-deb is already the newest version (1.114ubuntu1).
authbind is already the newest version (2.1.1+nmu1).
bind9 is already the newest version (1:9.9.
bind9utils is already the newest version (1:9.9.5.df...
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~lamont/maas/domain-detail into lp:maas failed. Below is the output from the failed tests.
Hit:1 http://
Get:2 http://
Hit:3 http://
Hit:4 http://
Get:5 http://
Get:6 http://
Get:7 http://
Get:8 http://
Get:9 http://
Get:10 http://
Fetched 23.4 MB in 6s (3,495 kB/s)
Reading package lists...
sudo DEBIAN_
--no-
Reading package lists...
Building dependency tree...
Reading state information...
apache2 is already the newest version (2.4.18-1ubuntu1).
archdetect-deb is already the newest version (1.114ubuntu1).
authbind is already the newest version (2.1.1+nmu1).
bind9 is already the newest version (1:9.9.
bind9utils is already the newest version (1:9.9.
build-essential is already the newest version (12.1ubuntu2).
curl is already the newest version (7.47.0-1ubuntu1).
debhelper is already the newest version (9.20160115ubun
dh-apport is already the newest version (2.20-0ubuntu3).
dh-systemd is already the newest version (1.28ubuntu2).
distr...
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~lamont/maas/domain-detail into lp:maas failed. Below is the output from the failed tests.
Hit:1 http://
Get:2 http://
Hit:3 http://
Hit:4 http://
Get:5 http://
Get:6 http://
Fetched 15.1 MB in 5s (2,756 kB/s)
Reading package lists...
sudo DEBIAN_
--no-
Reading package lists...
Building dependency tree...
Reading state information...
apache2 is already the newest version (2.4.18-1ubuntu1).
archdetect-deb is already the newest version (1.114ubuntu1).
authbind is already the newest version (2.1.1+nmu1).
bind9 is already the newest version (1:9.9.
bind9utils is already the newest version (1:9.9.
build-essential is already the newest version (12.1ubuntu2).
curl is already the newest version (7.47.0-1ubuntu1).
debhelper is already the newest version (9.20160115ubun
dh-apport is already the newest version (2.20-0ubuntu3).
dh-systemd is already the newest version (1.28ubuntu2).
distro-info is already the newest version (0.14build1).
dnsutils is already the newest version (1:9.9.
firefox is already the newest version (44.0.2+
freeipmi-tools is already the newest version (1.4.11-1ubuntu1).
git is already the newest version (1:2.7.0-1).
isc-dhcp-common is already the newest version (4.3.3-5ubuntu5).
libjs-angularjs is already the newest versi...
MAAS Lander (maas-lander) wrote : | # |
There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~lamont/maas/domain-detail into lp:maas failed. Below is the output from the failed tests.
Hit:1 http://
Get:2 http://
Hit:3 http://
Hit:4 http://
Get:5 http://
Get:6 http://
Get:7 http://
Get:8 http://
Get:9 http://
Get:10 http://
Fetched 23.4 MB in 7s (3,059 kB/s)
Reading package lists...
sudo DEBIAN_
--no-
Reading package lists...
Building dependency tree...
Reading state information...
apache2 is already the newest version (2.4.18-1ubuntu1).
archdetect-deb is already the newest version (1.114ubuntu1).
authbind is already the newest version (2.1.1+nmu1).
bind9 is already the newest version (1:9.9.
bind9utils is already the newest version (1:9.9.
build-essential is already the newest version (12.1ubuntu2).
curl is already the newest version (7.47.0-1ubuntu1).
debhelper is already the newest version (9.20160115ubun
dh-apport is already the newest version (2.20-0ubuntu3).
dh-systemd is already the newest version (1.28ubuntu2).
distr...
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~lamont/maas/domain-detail into lp:maas failed. Below is the output from the failed tests.
Hit:1 http://
Get:2 http://
Hit:3 http://
Hit:4 http://
Get:5 http://
Get:6 http://
Get:7 http://
Get:8 http://
Fetched 21.4 MB in 6s (3,285 kB/s)
Reading package lists...
sudo DEBIAN_
--no-
Reading package lists...
Building dependency tree...
Reading state information...
apache2 is already the newest version (2.4.18-1ubuntu1).
archdetect-deb is already the newest version (1.114ubuntu1).
authbind is already the newest version (2.1.1+nmu1).
bind9 is already the newest version (1:9.9.
bind9utils is already the newest version (1:9.9.
build-essential is already the newest version (12.1ubuntu2).
curl is already the newest version (7.47.0-1ubuntu1).
debhelper is already the newest version (9.20160115ubun
dh-apport is already the newest version (2.20-0ubuntu3).
dh-systemd is already the newest version (1.28ubuntu2).
distro-info is already the newest version (0.14build1).
dnsutils is already the newest version (1:9.9.
firefox is already the newest version (44.0.2+
freeipmi-too...
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~lamont/maas/domain-detail into lp:maas failed. Below is the output from the failed tests.
Hit:1 http://
Hit:2 http://
Hit:3 http://
Hit:4 http://
Reading package lists...
sudo DEBIAN_
--no-
Reading package lists...
Building dependency tree...
Reading state information...
apache2 is already the newest version (2.4.18-1ubuntu1).
archdetect-deb is already the newest version (1.114ubuntu1).
authbind is already the newest version (2.1.1+nmu1).
bind9 is already the newest version (1:9.9.
bind9utils is already the newest version (1:9.9.
build-essential is already the newest version (12.1ubuntu2).
curl is already the newest version (7.47.0-1ubuntu1).
debhelper is already the newest version (9.20160115ubun
dh-apport is already the newest version (2.20-0ubuntu3).
dh-systemd is already the newest version (1.28ubuntu2).
distro-info is already the newest version (0.14build1).
dnsutils is already the newest version (1:9.9.
firefox is already the newest version (44.0.2+
freeipmi-tools is already the newest version (1.4.11-1ubuntu1).
git is already the newest version (1:2.7.0-1).
isc-dhcp-common is already the newest version (4.3.3-5ubuntu5).
libjs-angularjs is already the newest version (1.2.28-1ubuntu2).
libjs-jquery is already the newest version (1.11.3+dfsg-4).
libjs-jquery-
libjs-yui3-full is already the newest version (3.5.1-1ubuntu3).
libjs-yui3-...
- 4668. By LaMont Jones
-
Use a bigger random space for the test, so that we collide and fail less often.
Preview Diff
1 | === modified file 'services/rackd/run' |
2 | --- services/rackd/run 2016-02-05 07:32:49 +0000 |
3 | +++ services/rackd/run 2016-02-18 16:28:50 +0000 |
4 | @@ -17,7 +17,7 @@ |
5 | # because there are race issues when restarting. |
6 | [ -z "${logdir:-}" ] || exec &>> "${logdir}/current" |
7 | |
8 | -# Configure the cluster's UUID to match sampledata, and also use a high |
9 | +# Configure the rack's UUID to match sampledata, and also use a high |
10 | # port for TFTP to match this branch's etc/services. |
11 | bin/maas-provision config \ |
12 | --uuid adfd3977-f251-4f2c-8d61-745dbd690bf2 \ |
13 | |
14 | === modified file 'src/maasserver/api/dnsresourcerecords.py' |
15 | --- src/maasserver/api/dnsresourcerecords.py 2016-02-05 07:53:45 +0000 |
16 | +++ src/maasserver/api/dnsresourcerecords.py 2016-02-18 16:28:50 +0000 |
17 | @@ -94,6 +94,7 @@ |
18 | resource type.) |
19 | """ |
20 | data = request.data |
21 | + domain = None |
22 | fqdn = data.get('fqdn', None) |
23 | name = data.get('name', None) |
24 | domainname = data.get('domain', None) |
25 | @@ -122,6 +123,9 @@ |
26 | "name:%s" % domainname, user=request.user, |
27 | perm=NODE_PERMISSION.VIEW) |
28 | data['domain'] = domain.id |
29 | + if domain is None or name is None: |
30 | + raise MAASAPIValidationError( |
31 | + "Either name and domain (or fqdn) must be specified") |
32 | # Do we already have a DNSResource for this fqdn? |
33 | dnsrr = DNSResource.objects.filter(name=name, domain__id=domain.id) |
34 | if not dnsrr.exists(): |
35 | |
36 | === modified file 'src/maasserver/api/ip_addresses.py' |
37 | --- src/maasserver/api/ip_addresses.py 2016-02-10 20:32:34 +0000 |
38 | +++ src/maasserver/api/ip_addresses.py 2016-02-18 16:28:50 +0000 |
39 | @@ -20,6 +20,7 @@ |
40 | INTERFACE_LINK_TYPE, |
41 | INTERFACE_TYPE, |
42 | IPADDRESS_TYPE, |
43 | + NODE_PERMISSION, |
44 | ) |
45 | from maasserver.exceptions import ( |
46 | MAASAPIBadRequest, |
47 | @@ -56,8 +57,7 @@ |
48 | |
49 | @transactional |
50 | def _claim_ip( |
51 | - self, user, subnet, ip_address, mac=None, |
52 | - hostname=None, domain=None): |
53 | + self, user, subnet, ip_address, mac=None, hostname=None): |
54 | """Attempt to get a USER_RESERVED StaticIPAddress for `user`. |
55 | |
56 | :param subnet: Subnet to use use for claiming the IP. |
57 | @@ -72,7 +72,11 @@ |
58 | :type domain: Domain |
59 | :raises StaticIPAddressExhaustion: If no IPs available. |
60 | """ |
61 | - if domain is None: |
62 | + if hostname is not None and hostname.find('.') > 0: |
63 | + hostname, domain = hostname.split('.', 1) |
64 | + domain = Domain.objects.get_domain_or_404( |
65 | + "name:%s" % domain, user, NODE_PERMISSION.VIEW) |
66 | + else: |
67 | domain = Domain.objects.get_default_domain() |
68 | if mac is None: |
69 | sip = StaticIPAddress.objects.allocate_new( |
70 | @@ -141,7 +145,8 @@ |
71 | reservation is required. e.g. 10.1.2.0/24 |
72 | :param ip_address: The IP address, which must be within |
73 | a known subnet. |
74 | - :param hostname: The hostname to use for the specified IP address |
75 | + :param hostname: The hostname to use for the specified IP address. If |
76 | + no domain component is given, the default domain will be used. |
77 | :param mac: The MAC address that should be linked to this reservation. |
78 | |
79 | Returns 400 if there is no subnet in MAAS matching the provided one, |
80 | |
81 | === modified file 'src/maasserver/api/tests/test_dnsresourcerecords.py' |
82 | --- src/maasserver/api/tests/test_dnsresourcerecords.py 2016-02-02 14:20:45 +0000 |
83 | +++ src/maasserver/api/tests/test_dnsresourcerecords.py 2016-02-18 16:28:50 +0000 |
84 | @@ -196,6 +196,30 @@ |
85 | response.content.decode( |
86 | settings.DEFAULT_CHARSET))['rrdata']) |
87 | |
88 | + def test_create_fails_with_no_name(self): |
89 | + self.become_admin() |
90 | + domain = factory.make_Domain() |
91 | + uri = get_dnsresourcerecords_uri() |
92 | + response = self.client.post(uri, { |
93 | + "domain": domain.name, |
94 | + "rrtype": "TXT", |
95 | + "rrdata": "Sample Text.", |
96 | + }) |
97 | + self.assertEqual( |
98 | + http.client.BAD_REQUEST, response.status_code, response.content) |
99 | + |
100 | + def test_create_fails_with_no_domain(self): |
101 | + self.become_admin() |
102 | + dnsresource_name = factory.make_name("dnsresource") |
103 | + uri = get_dnsresourcerecords_uri() |
104 | + response = self.client.post(uri, { |
105 | + "name": dnsresource_name, |
106 | + "rrtype": "TXT", |
107 | + "rrdata": "Sample Text.", |
108 | + }) |
109 | + self.assertEqual( |
110 | + http.client.BAD_REQUEST, response.status_code, response.content) |
111 | + |
112 | def test_create_admin_only(self): |
113 | dnsresource_name = factory.make_name("dnsresource") |
114 | uri = get_dnsresourcerecords_uri() |
115 | |
116 | === modified file 'src/maasserver/api/tests/test_ipaddresses.py' |
117 | --- src/maasserver/api/tests/test_ipaddresses.py 2016-02-02 14:20:45 +0000 |
118 | +++ src/maasserver/api/tests/test_ipaddresses.py 2016-02-18 16:28:50 +0000 |
119 | @@ -177,6 +177,20 @@ |
120 | # We expect 1 call from the Subnet creation. |
121 | self.expectThat(dns_update_subnets.call_count, Equals(1)) |
122 | |
123 | + def test_POST_reserve_with_bad_fqdn_fails(self): |
124 | + from maasserver.dns import config as dns_config_module |
125 | + dns_update_subnets = self.patch( |
126 | + dns_config_module, 'dns_update_subnets') |
127 | + subnet = factory.make_Subnet() |
128 | + hostname = factory.make_hostname() |
129 | + domainname = factory.make_name('domain') |
130 | + fqdn = "%s.%s" % (hostname, domainname) |
131 | + response = self.post_reservation_request( |
132 | + subnet=subnet, hostname=fqdn) |
133 | + self.assertEqual(http.client.NOT_FOUND, response.status_code) |
134 | + # We expect no calls |
135 | + self.expectThat(dns_update_subnets.call_count, Equals(0)) |
136 | + |
137 | def test_POST_reserve_with_hostname_creates_ip_with_hostname(self): |
138 | from maasserver.dns import config as dns_config_module |
139 | dns_update_subnets = self.patch( |
140 | @@ -215,6 +229,51 @@ |
141 | # We expect one from the Subnet. |
142 | self.expectThat(dns_update_subnets.call_count, Equals(1)) |
143 | |
144 | + def test_POST_reserve_with_fqdn_creates_ip_with_hostname(self): |
145 | + from maasserver.dns import config as dns_config_module |
146 | + dns_update_subnets = self.patch( |
147 | + dns_config_module, 'dns_update_subnets') |
148 | + subnet = factory.make_Subnet() |
149 | + hostname = factory.make_hostname() |
150 | + domainname = factory.make_Domain().name |
151 | + fqdn = "%s.%s" % (hostname, domainname) |
152 | + response = self.post_reservation_request( |
153 | + subnet=subnet, hostname="%s.%s" % (hostname, domainname)) |
154 | + self.assertEqual(http.client.OK, response.status_code) |
155 | + [staticipaddress] = StaticIPAddress.objects.all() |
156 | + self.expectThat( |
157 | + staticipaddress.dnsresource_set.first().name, Equals(hostname)) |
158 | + self.expectThat( |
159 | + staticipaddress.dnsresource_set.first().fqdn, Equals(fqdn)) |
160 | + # We expect one from the Subnet. |
161 | + self.expectThat(dns_update_subnets.call_count, Equals(1)) |
162 | + |
163 | + def test_POST_reserve_with_fqdn_and_ip_creates_ip_with_hostname(self): |
164 | + from maasserver.dns import config as dns_config_module |
165 | + dns_update_subnets = self.patch( |
166 | + dns_config_module, 'dns_update_subnets') |
167 | + subnet = factory.make_Subnet() |
168 | + hostname = factory.make_hostname() |
169 | + domainname = factory.make_Domain().name |
170 | + fqdn = "%s.%s" % (hostname, domainname) |
171 | + ip_in_network = factory.pick_ip_in_Subnet(subnet) |
172 | + response = self.post_reservation_request( |
173 | + subnet=subnet, ip_address=ip_in_network, |
174 | + hostname="%s.%s" % (hostname, domainname)) |
175 | + self.assertEqual( |
176 | + http.client.OK, response.status_code, response.content) |
177 | + returned_address = json_load_bytes(response.content) |
178 | + [staticipaddress] = StaticIPAddress.objects.all() |
179 | + self.expectThat( |
180 | + returned_address["alloc_type"], |
181 | + Equals(IPADDRESS_TYPE.USER_RESERVED)) |
182 | + self.expectThat(returned_address["ip"], Equals(ip_in_network)) |
183 | + self.expectThat(staticipaddress.ip, Equals(ip_in_network)) |
184 | + self.expectThat( |
185 | + staticipaddress.dnsresource_set.first().fqdn, Equals(fqdn)) |
186 | + # We expect one from the Subnet. |
187 | + self.expectThat(dns_update_subnets.call_count, Equals(1)) |
188 | + |
189 | def test_POST_reserve_with_no_parameters_fails_with_bad_request(self): |
190 | response = self.post_reservation_request() |
191 | self.assertEqual( |
192 | |
193 | === modified file 'src/maasserver/dns/config.py' |
194 | --- src/maasserver/dns/config.py 2016-02-02 14:20:45 +0000 |
195 | +++ src/maasserver/dns/config.py 2016-02-18 16:28:50 +0000 |
196 | @@ -21,6 +21,7 @@ |
197 | sequence, |
198 | ZoneGenerator, |
199 | ) |
200 | +from maasserver.enum import RDNS_MODE |
201 | from maasserver.models.config import Config |
202 | from maasserver.models.domain import Domain |
203 | from maasserver.models.subnet import Subnet |
204 | @@ -109,6 +110,10 @@ |
205 | :return: The post-commit `Deferred`. |
206 | """ |
207 | if is_dns_enabled(): |
208 | + subnets = [ |
209 | + net |
210 | + for net in subnets |
211 | + if net.rdns_mode != RDNS_MODE.DISABLED] |
212 | if DNS_DEFER_UPDATES: |
213 | return consolidator.add_subnets(subnets) |
214 | else: |
215 | @@ -160,6 +165,10 @@ |
216 | :return: The post-commit `Deferred`. |
217 | """ |
218 | if is_dns_enabled(): |
219 | + subnets = [ |
220 | + net |
221 | + for net in subnets |
222 | + if net.rdns_mode != RDNS_MODE.DISABLED] |
223 | if DNS_DEFER_UPDATES: |
224 | return consolidator.update_subnets(subnets) |
225 | else: |
226 | @@ -193,7 +202,9 @@ |
227 | if node.domain.authoritative is True: |
228 | auth_domains.append(node.domain) |
229 | # What subnets may be affected by this node being updated? |
230 | - subnets = Subnet.objects.filter(staticipaddress__interface__node=node) |
231 | + subnets = Subnet.objects.filter( |
232 | + staticipaddress__interface__node=node).exclude( |
233 | + rdns_mode=RDNS_MODE.DISABLED) |
234 | if DNS_DEFER_UPDATES: |
235 | return consolidator.update_zones(auth_domains, subnets) |
236 | else: |
237 | @@ -219,7 +230,7 @@ |
238 | return |
239 | |
240 | domains = Domain.objects.filter(authoritative=True) |
241 | - subnets = Subnet.objects.all() |
242 | + subnets = Subnet.objects.exclude(rdns_mode=RDNS_MODE.DISABLED) |
243 | default_ttl = Config.objects.get_config('default_dns_ttl') |
244 | zones = ZoneGenerator( |
245 | domains, subnets, default_ttl, |
246 | |
247 | === modified file 'src/maasserver/dns/tests/test_config.py' |
248 | --- src/maasserver/dns/tests/test_config.py 2016-02-03 14:25:59 +0000 |
249 | +++ src/maasserver/dns/tests/test_config.py 2016-02-18 16:28:50 +0000 |
250 | @@ -34,6 +34,8 @@ |
251 | from maasserver.enum import ( |
252 | IPADDRESS_TYPE, |
253 | NODE_STATUS, |
254 | + RDNS_MODE, |
255 | + RDNS_MODE_CHOICES, |
256 | ) |
257 | from maasserver.models import ( |
258 | Config, |
259 | @@ -111,6 +113,18 @@ |
260 | [next_zone_serial() for _ in range(initial, initial + 10)]) |
261 | |
262 | |
263 | +class ReverseThing: |
264 | + def __init__(self, id, rdns_mode): |
265 | + self.id = id |
266 | + self.rdns_mode = rdns_mode |
267 | + |
268 | + def __eq__(self, other): |
269 | + return self.id == other.id |
270 | + |
271 | + def __hash__(self): |
272 | + return hash(self.id) |
273 | + |
274 | + |
275 | class Thing: |
276 | def __init__(self, id, authoritative): |
277 | self.id = id |
278 | @@ -140,8 +154,8 @@ |
279 | ("dns_add_subnets", { |
280 | "now_function": dns_add_zones_now, |
281 | "calling_function": dns_add_subnets, |
282 | - "args": [[Thing(555, True)]], |
283 | - "now_args": [[], [Thing(555, True)]], |
284 | + "args": [[ReverseThing(555, True)]], |
285 | + "now_args": [[], [ReverseThing(555, True)]], |
286 | "kwargs": {}, |
287 | }), |
288 | ("dns_update_domains", { |
289 | @@ -154,8 +168,8 @@ |
290 | ("dns_update_subnets", { |
291 | "now_function": dns_update_zones_now, |
292 | "calling_function": dns_update_subnets, |
293 | - "args": [[Thing(555, True)]], |
294 | - "now_args": [[], [Thing(555, True)]], |
295 | + "args": [[ReverseThing(555, True)]], |
296 | + "now_args": [[], [ReverseThing(555, True)]], |
297 | "kwargs": {}, |
298 | }), |
299 | ("dns_update_all_zones", { |
300 | @@ -560,6 +574,52 @@ |
301 | compose_config_path(DNSConfig.target_file_name), |
302 | FileContains(matcher=Contains(trusted_network))) |
303 | |
304 | + def test_subnets_correctly_added_to_config(self): |
305 | + # We choose 3 sizes of subnets (big, /24, and small), and all 3 |
306 | + # RDNS_MODE values, for a total of 9 subnets that we will check. |
307 | + subnets = [ |
308 | + factory.make_Subnet( |
309 | + cidr='%d.%d.12.64/%d' % ( |
310 | + random.randint(1, 223), random.randint(0, 255), prefix), |
311 | + rdns_mode=choice[0]) |
312 | + for prefix in [random.randint(17, 23), 24, random.randint(25, 29)] |
313 | + for choice in RDNS_MODE_CHOICES |
314 | + ] |
315 | + for subnet in subnets: |
316 | + node, static = self.create_node_with_static_ip( |
317 | + subnet=subnet) |
318 | + self.patch(settings, 'DNS_CONNECT', True) |
319 | + dns_add_zones_now([node.domain], subnets) |
320 | + for subnet in subnets: |
321 | + net = IPNetwork(subnet.cidr) |
322 | + # Generate the reverse zone name for the /24. |
323 | + last, rname = IPAddress(net).reverse_dns[:-1].split('.', 1) |
324 | + # RFC2317 zones look different. |
325 | + if net.prefixlen > 24: |
326 | + rname = '%s-%d.%s' % (last, net.prefixlen, rname) |
327 | + # If we're supposed to generate reverse DNS, make sure there is a |
328 | + # zone declaration in the written config. |
329 | + if subnet.rdns_mode != RDNS_MODE.DISABLED: |
330 | + matcher = Contains('zone "%s"' % rname) |
331 | + else: |
332 | + matcher = Not(Contains('zone "%s"' % rname)) |
333 | + self.assertThat( |
334 | + compose_config_path(DNSConfig.target_file_name), |
335 | + FileContains( |
336 | + matcher=matcher)) |
337 | + # If this is an RFC2317 zone, make sure that the glue is (or is |
338 | + # not) present, dpeending on the configuration setting for this |
339 | + # subnet. |
340 | + if net.prefixlen > 24: |
341 | + _, rname = rname.split('.', 1) |
342 | + if subnet.rdns_mode != RDNS_MODE.RFC2317: |
343 | + matcher = Not(Contains('zone "%s"' % rname)) |
344 | + else: |
345 | + matcher = Contains('zone "%s"' % rname) |
346 | + self.assertThat( |
347 | + compose_config_path(DNSConfig.target_file_name), |
348 | + FileContains(matcher=matcher)) |
349 | + |
350 | def test_dns_update_zones_now_changes_dns_zone(self): |
351 | node, static = self.create_node_with_static_ip() |
352 | self.patch(settings, 'DNS_CONNECT', True) |
353 | |
354 | === modified file 'src/maasserver/dns/tests/test_zonegenerator.py' |
355 | --- src/maasserver/dns/tests/test_zonegenerator.py 2016-02-03 09:11:25 +0000 |
356 | +++ src/maasserver/dns/tests/test_zonegenerator.py 2016-02-18 16:28:50 +0000 |
357 | @@ -222,9 +222,9 @@ |
358 | Config.objects.set_config('default_dns_ttl', ttl) |
359 | expected_mapping = { |
360 | "%s.maas" % node1.hostname: HostnameIPMapping( |
361 | - node1.system_id, ttl, {static_ip.ip}), |
362 | + node1.system_id, ttl, {static_ip.ip}, node1.node_type), |
363 | "%s.maas" % node2.hostname: HostnameIPMapping( |
364 | - node2.system_id, ttl, {dynamic_ip.ip}), |
365 | + node2.system_id, ttl, {dynamic_ip.ip}, node2.node_type), |
366 | } |
367 | actual = get_hostname_ip_mapping(Domain.objects.get_default_domain()) |
368 | self.assertItemsEqual( |
369 | @@ -240,9 +240,10 @@ |
370 | Config.objects.set_config('default_dns_ttl', ttl) |
371 | expected_mapping = { |
372 | dnsdata1.dnsresource.name: HostnameRRsetMapping( |
373 | - node.system_id, {(ttl, dnsdata1.rrtype, dnsdata1.rrdata)}), |
374 | + node.system_id, {(ttl, dnsdata1.rrtype, dnsdata1.rrdata)}, |
375 | + node.node_type), |
376 | dnsdata2.dnsresource.name: HostnameRRsetMapping( |
377 | - None, {(ttl, dnsdata2.rrtype, dnsdata2.rrdata)}), |
378 | + None, {(ttl, dnsdata2.rrtype, dnsdata2.rrdata)}, None), |
379 | } |
380 | actual = get_hostname_dnsdata_mapping(node.domain) |
381 | self.assertItemsEqual( |
382 | @@ -355,7 +356,7 @@ |
383 | self.assertEqual( |
384 | {node.hostname: HostnameIPMapping( |
385 | node.system_id, default_ttl, |
386 | - {'%s' % boot_ip.ip})}, zones[0]._mapping) |
387 | + {'%s' % boot_ip.ip}, node.node_type)}, zones[0]._mapping) |
388 | self.assertEqual( |
389 | {dnsdata.dnsresource.name: HostnameRRsetMapping( |
390 | None, |
391 | @@ -363,11 +364,11 @@ |
392 | zones[0]._other_mapping.items()) |
393 | self.assertItemsEqual({ |
394 | node.fqdn: HostnameIPMapping( |
395 | - node.system_id, 30, {'%s' % boot_ip.ip}), |
396 | + node.system_id, 30, {'%s' % boot_ip.ip}, node.node_type), |
397 | '%s.%s' % (interfaces[0].name, node.fqdn): HostnameIPMapping( |
398 | - None, default_ttl, {'%s' % sip.ip}), |
399 | + None, default_ttl, {'%s' % sip.ip}, None), |
400 | '%s.%s' % (boot_iface.name, node.fqdn): HostnameIPMapping( |
401 | - None, default_ttl, {'%s' % boot_ip.ip})}, |
402 | + None, default_ttl, {'%s' % boot_ip.ip}, None)}, |
403 | zones[1]._mapping) |
404 | |
405 | def rfc2317_network(self, network): |
406 | @@ -464,12 +465,12 @@ |
407 | [boot_ip] = boot_iface.claim_auto_ips() |
408 | expected_forward = { |
409 | node.hostname: HostnameIPMapping( |
410 | - node.system_id, domain.ttl, {boot_ip.ip})} |
411 | + node.system_id, domain.ttl, {boot_ip.ip}, node.node_type)} |
412 | expected_reverse = { |
413 | node.fqdn: HostnameIPMapping( |
414 | - node.system_id, domain.ttl, {boot_ip.ip}), |
415 | + node.system_id, domain.ttl, {boot_ip.ip}, node.node_type), |
416 | "%s.%s" % (boot_iface.name, node.fqdn): HostnameIPMapping( |
417 | - node.system_id, domain.ttl, {boot_ip.ip})} |
418 | + node.system_id, domain.ttl, {boot_ip.ip}, node.node_type)} |
419 | zones = ZoneGenerator( |
420 | domain, subnet, default_ttl=global_ttl, |
421 | serial_generator=Mock()).as_list() |
422 | @@ -490,12 +491,16 @@ |
423 | [boot_ip] = boot_iface.claim_auto_ips() |
424 | expected_forward = { |
425 | node.hostname: HostnameIPMapping( |
426 | - node.system_id, node.address_ttl, {boot_ip.ip})} |
427 | + node.system_id, node.address_ttl, {boot_ip.ip}, |
428 | + node.node_type)} |
429 | expected_reverse = { |
430 | node.fqdn: HostnameIPMapping( |
431 | - node.system_id, node.address_ttl, {boot_ip.ip}), |
432 | + node.system_id, node.address_ttl, {boot_ip.ip}, |
433 | + node.node_type), |
434 | "%s.%s" % (boot_iface.name, node.fqdn): |
435 | - HostnameIPMapping(node.system_id, node.address_ttl, {boot_ip.ip})} |
436 | + HostnameIPMapping( |
437 | + node.system_id, node.address_ttl, {boot_ip.ip}, |
438 | + node.node_type)} |
439 | zones = ZoneGenerator( |
440 | domain, subnet, default_ttl=global_ttl, |
441 | serial_generator=Mock()).as_list() |
442 | @@ -524,13 +529,14 @@ |
443 | ip.ip for ip in dnsrr.ip_addresses.all() if ip is not None} |
444 | ips.add(boot_ip.ip) |
445 | expected_forward = {node.hostname: HostnameIPMapping( |
446 | - node.system_id, node.address_ttl, ips)} |
447 | + node.system_id, node.address_ttl, ips, node.node_type)} |
448 | expected_reverse = { |
449 | node.fqdn: HostnameIPMapping( |
450 | - node.system_id, node.address_ttl, ips), |
451 | + node.system_id, node.address_ttl, ips, node.node_type), |
452 | "%s.%s" % (boot_iface.name, node.fqdn): |
453 | HostnameIPMapping( |
454 | - node.system_id, node.address_ttl, {boot_ip.ip})} |
455 | + node.system_id, node.address_ttl, {boot_ip.ip}, |
456 | + node.node_type)} |
457 | zones = ZoneGenerator( |
458 | domain, subnet, default_ttl=global_ttl, |
459 | serial_generator=Mock()).as_list() |
460 | @@ -558,17 +564,19 @@ |
461 | ip.ip for ip in dnsrr.ip_addresses.all() if ip is not None} |
462 | expected_forward = { |
463 | node.hostname: HostnameIPMapping( |
464 | - node.system_id, node.address_ttl, node_ips), |
465 | + node.system_id, node.address_ttl, node_ips, node.node_type), |
466 | dnsrr.name: HostnameIPMapping( |
467 | - None, dnsrr.address_ttl, dnsrr_ips), |
468 | + None, dnsrr.address_ttl, dnsrr_ips, None), |
469 | } |
470 | expected_reverse = { |
471 | node.fqdn: HostnameIPMapping( |
472 | - node.system_id, node.address_ttl, node_ips), |
473 | + node.system_id, node.address_ttl, node_ips, node.node_type), |
474 | dnsrr.fqdn: HostnameIPMapping( |
475 | - None, dnsrr.address_ttl, dnsrr_ips), |
476 | + None, dnsrr.address_ttl, dnsrr_ips, None), |
477 | "%s.%s" % (boot_iface.name, node.fqdn): |
478 | - HostnameIPMapping(node.system_id, node.address_ttl, {boot_ip.ip})} |
479 | + HostnameIPMapping( |
480 | + node.system_id, node.address_ttl, {boot_ip.ip}, |
481 | + node.node_type)} |
482 | zones = ZoneGenerator( |
483 | domain, subnet, default_ttl=global_ttl, |
484 | serial_generator=Mock()).as_list() |
485 | |
486 | === modified file 'src/maasserver/dns/zonegenerator.py' |
487 | --- src/maasserver/dns/zonegenerator.py 2016-02-04 11:35:31 +0000 |
488 | +++ src/maasserver/dns/zonegenerator.py 2016-02-18 16:28:50 +0000 |
489 | @@ -239,10 +239,11 @@ |
490 | rfc2317_glue = {} |
491 | for subnet in subnets: |
492 | network = IPNetwork(subnet.cidr) |
493 | - # If this is a small subnet and we are doing RFC2317 glue for it, |
494 | - # then we need to combine that with any other such subnets |
495 | - # We need to know this before we start creating reverse DNS zones. |
496 | if subnet.rdns_mode == RDNS_MODE.RFC2317: |
497 | + # If this is a small subnet and we are doing RFC2317 glue for |
498 | + # it, then we need to combine that with any other such subnets |
499 | + # We need to know this before we start creating reverse DNS |
500 | + # zones. |
501 | if network.version == 4 and network.prefixlen > 24: |
502 | # Turn 192.168.99.32/29 into 192.168.99.0/24 |
503 | basenet = IPNetwork( |
504 | @@ -254,6 +255,12 @@ |
505 | "%s/124" % |
506 | IPNetwork("%s/124" % network.network).network) |
507 | rfc2317_glue.setdefault(basenet, set()).add(network) |
508 | + elif subnet.rdns_mode == RDNS_MODE.DISABLED: |
509 | + # If we are not doing reverse dns for this subnet, then just |
510 | + # skip to the next subnet. |
511 | + logger.debug( |
512 | + "%s disabled subnet in DNS config list" % subnet.cidr) |
513 | + continue |
514 | |
515 | # 1. Figure out the dynamic ranges. |
516 | dynamic_ranges = [ |
517 | @@ -283,14 +290,13 @@ |
518 | else: |
519 | ttl = default_ttl |
520 | for iface in node.interface_set.all(): |
521 | - iface_map = HostnameIPMapping( |
522 | - node.system_id, ttl, { |
523 | - ip.ip |
524 | - for ip in iface.ip_addresses.all() |
525 | - if ( |
526 | - ip.ip is not None and |
527 | - ip.subnet_id == subnet.id)}) |
528 | - if len(iface_map.ips) > 0: |
529 | + ips_in_subnet = { |
530 | + ip.ip |
531 | + for ip in iface.ip_addresses.all() |
532 | + if (ip.ip is not None and ip.subnet_id == subnet.id)} |
533 | + if len(ips_in_subnet) > 0: |
534 | + iface_map = HostnameIPMapping( |
535 | + node.system_id, ttl, ips_in_subnet, node.node_type) |
536 | mapping.update({ |
537 | "%s.%s" % (iface.name, iface.node.fqdn): iface_map |
538 | }) |
539 | |
540 | === modified file 'src/maasserver/models/dnsdata.py' |
541 | --- src/maasserver/models/dnsdata.py 2016-02-03 10:13:27 +0000 |
542 | +++ src/maasserver/models/dnsdata.py 2016-02-18 16:28:50 +0000 |
543 | @@ -73,12 +73,14 @@ |
544 | """This is used to return non-address information for a hostname in a way |
545 | that keeps life simple for the allers. Rrset is a set of (ttl, rrtype, |
546 | rrdata) tuples.""" |
547 | - def __init__(self, system_id=None, rrset=set()): |
548 | + def __init__(self, system_id=None, rrset=set(), node_type=None): |
549 | self.system_id = system_id |
550 | + self.node_type = node_type |
551 | self.rrset = rrset.copy() |
552 | |
553 | def __repr__(self): |
554 | - return "%s:%s" % (self.system_id, self.rrset) |
555 | + return "HostnameRRSetMapping(%r, %r, %r)" % ( |
556 | + self.system_id, self.rrset, self.node_type) |
557 | |
558 | def __eq__(self, other): |
559 | return self.__dict__ == other.__dict__ |
560 | @@ -134,18 +136,24 @@ |
561 | else: |
562 | raise PermissionDenied() |
563 | |
564 | - def get_hostname_dnsdata_mapping(self, domain): |
565 | + def get_hostname_dnsdata_mapping(self, domain, raw_ttl=False): |
566 | """Return hostname to RRset mapping for this domain.""" |
567 | cursor = connection.cursor() |
568 | default_ttl = "%d" % Config.objects.get_config('default_dns_ttl') |
569 | + if raw_ttl: |
570 | + ttl_clause = """dnsdata.ttl""" |
571 | + else: |
572 | + ttl_clause = """ |
573 | + COALESCE( |
574 | + dnsdata.ttl, |
575 | + domain.ttl, |
576 | + %s)""" % default_ttl |
577 | sql_query = """ |
578 | SELECT |
579 | dnsresource.name, |
580 | node.system_id, |
581 | - COALESCE( |
582 | - dnsdata.ttl, |
583 | - domain.ttl, |
584 | - """ + default_ttl + """) AS ttl, |
585 | + node.node_type, |
586 | + """ + ttl_clause + """ AS ttl, |
587 | dnsdata.rrtype, |
588 | dnsdata.rrdata |
589 | FROM maasserver_dnsdata AS dnsdata |
590 | @@ -168,7 +176,9 @@ |
591 | # not spill CNAME and other data. |
592 | mapping = defaultdict(HostnameRRsetMapping) |
593 | cursor.execute(sql_query, (domain.id,)) |
594 | - for (name, system_id, ttl, rrtype, rrdata) in cursor.fetchall(): |
595 | + for (name, system_id, node_type, |
596 | + ttl, rrtype, rrdata) in cursor.fetchall(): |
597 | + mapping[name].node_type = node_type |
598 | mapping[name].system_id = system_id |
599 | mapping[name].rrset.add((ttl, rrtype, rrdata)) |
600 | return mapping |
601 | |
602 | === modified file 'src/maasserver/models/domain.py' |
603 | --- src/maasserver/models/domain.py 2016-02-05 08:00:32 +0000 |
604 | +++ src/maasserver/models/domain.py 2016-02-18 16:28:50 +0000 |
605 | @@ -42,6 +42,7 @@ |
606 | from maasserver.models.config import Config |
607 | from maasserver.models.timestampedmodel import TimestampedModel |
608 | from maasserver.utils.orm import MAASQueriesMixin |
609 | +from netaddr import IPAddress |
610 | |
611 | # Labels are at most 63 octets long, and a name can be many of them. |
612 | LABEL = r'[a-zA-Z0-9]([-a-zA-Z0-9]{0,62}[a-zA-Z0-9]){0,1}' |
613 | @@ -272,44 +273,41 @@ |
614 | super(Domain, self).clean(*args, **kwargs) |
615 | self.clean_name() |
616 | |
617 | - def render_json_for_related_ips(self, for_list=False): |
618 | - """Render a representation of this domain's related IP addresses, |
619 | - suitable for converting to JSON. |
620 | - |
621 | - :return: (data, address_count)""" |
622 | - from maasserver.models import StaticIPAddress |
623 | - # Get all of the address mappings. |
624 | - ip_mapping = StaticIPAddress.objects.get_hostname_ip_mapping(self) |
625 | - domainname_len = len(self.name) |
626 | - data = [] |
627 | - count = 0 |
628 | - for hostname, info in ip_mapping.items(): |
629 | - if not for_list: |
630 | - data.append({ |
631 | - # strip off the domain name. |
632 | - 'hostname': hostname[:-domainname_len - 1], |
633 | - 'system_id': info.system_id, |
634 | - 'ttl': info.ttl, |
635 | - 'ips': info.ips, |
636 | - }) |
637 | - count += len(info.ips) |
638 | - return (data, count) |
639 | - |
640 | def render_json_for_related_rrdata(self, for_list=False): |
641 | """Render a representation of this domain's related non-IP data, |
642 | suitable for converting to JSON. |
643 | |
644 | - :return: (data, record_count)""" |
645 | - from maasserver.models import DNSData |
646 | - rr_mapping = DNSData.objects.get_hostname_dnsdata_mapping(self) |
647 | + :return: data""" |
648 | + from maasserver.models import ( |
649 | + DNSData, |
650 | + StaticIPAddress, |
651 | + ) |
652 | + rr_mapping = DNSData.objects.get_hostname_dnsdata_mapping( |
653 | + self, raw_ttl=True) |
654 | + # Smash the IP Addresses in the rrset mapping, so that the far end |
655 | + # only needs to worry about one thing. |
656 | + ip_mapping = StaticIPAddress.objects.get_hostname_ip_mapping( |
657 | + self, raw_ttl=True) |
658 | + for hostname, info in ip_mapping.items(): |
659 | + hostname = hostname[:-len(self.name) - 1] |
660 | + if info.system_id is not None: |
661 | + rr_mapping[hostname].system_id = info.system_id |
662 | + rr_mapping[hostname].node_type = info.node_type |
663 | + for ip in info.ips: |
664 | + if IPAddress(ip).version == 6: |
665 | + rr_mapping[hostname].rrset.add((info.ttl, 'AAAA', ip)) |
666 | + else: |
667 | + rr_mapping[hostname].rrset.add((info.ttl, 'A', ip)) |
668 | data = [] |
669 | - count = 0 |
670 | for hostname, info in rr_mapping.items(): |
671 | - if not for_list: |
672 | - data.append({ |
673 | - 'hostname': hostname, |
674 | - 'system_id': info.system_id, |
675 | - 'rrsets': info.rrset, |
676 | - }) |
677 | - count += len(info.rrset) |
678 | - return (data, count) |
679 | + data += [{ |
680 | + 'name': hostname, |
681 | + 'system_id': info.system_id, |
682 | + 'node_type': info.node_type, |
683 | + 'ttl': ttl, |
684 | + 'rrtype': rrtype, |
685 | + 'rrdata': rrdata |
686 | + } |
687 | + for ttl, rrtype, rrdata in info.rrset |
688 | + ] |
689 | + return data |
690 | |
691 | === modified file 'src/maasserver/models/staticipaddress.py' |
692 | --- src/maasserver/models/staticipaddress.py 2016-02-10 23:42:49 +0000 |
693 | +++ src/maasserver/models/staticipaddress.py 2016-02-18 16:28:50 +0000 |
694 | @@ -70,13 +70,15 @@ |
695 | class HostnameIPMapping: |
696 | """This is used to return address information for a host in a way that |
697 | keeps life simple for the callers.""" |
698 | - def __init__(self, system_id=None, ttl=None, ips=set()): |
699 | + def __init__(self, system_id=None, ttl=None, ips=set(), node_type=None): |
700 | self.system_id = system_id |
701 | + self.node_type = node_type |
702 | self.ttl = ttl |
703 | self.ips = ips.copy() |
704 | |
705 | def __repr__(self): |
706 | - return "%s:%s:%s" % (self.system_id, self.ttl, self.ips) |
707 | + return "HostnameIPMapping(%r, %r, %r, %r)" % ( |
708 | + self.system_id, self.ttl, self.ips, self.node_type) |
709 | |
710 | def __eq__(self, other): |
711 | return self.__dict__ == other.__dict__ |
712 | @@ -217,13 +219,18 @@ |
713 | requested_address, alloc_type, |
714 | user=user, subnet=subnet) |
715 | |
716 | - def _get_user_reserved_mappings(self): |
717 | + def _get_user_reserved_mappings(self, domain_or_subnet, raw_ttl=False): |
718 | # A poorly named routine these days, since it actually returns |
719 | # addresses for anything with any DNSResource records as well. |
720 | default_ttl = Config.objects.get_config('default_dns_ttl') |
721 | qs = self.filter( |
722 | Q(alloc_type=IPADDRESS_TYPE.USER_RESERVED) | |
723 | Q(dnsresource__isnull=False)) |
724 | + if isinstance(domain_or_subnet, Subnet): |
725 | + qs = qs.filter(subnet_id=domain_or_subnet.id) |
726 | + elif isinstance(domain_or_subnet, Domain): |
727 | + qs = qs.filter(dnsresource__domain_id=domain_or_subnet.id) |
728 | + qs = qs.prefetch_related("dnsresource_set") |
729 | mappings = defaultdict(HostnameIPMapping) |
730 | for instance in qs: |
731 | ip = instance.ip |
732 | @@ -241,7 +248,7 @@ |
733 | Domain.objects.get_default_domain().name) |
734 | else: |
735 | hostname = '%s.%s' % (dnsrr.name, dnsrr.domain.name) |
736 | - if dnsrr.address_ttl is not None: |
737 | + if raw_ttl or dnsrr.address_ttl is not None: |
738 | ttl = dnsrr.address_ttl |
739 | elif dnsrr.domain.ttl is not None: |
740 | ttl = dnsrr.domain.ttl |
741 | @@ -253,7 +260,7 @@ |
742 | # No DNSResource, but it's USER_RESERVED. |
743 | domain = Domain.objects.get_default_domain() |
744 | hostname = "%s.%s" % (get_ip_based_hostname(ip), domain.name) |
745 | - if domain.ttl is not None: |
746 | + if raw_ttl or domain.ttl is not None: |
747 | ttl = domain.ttl |
748 | else: |
749 | ttl = default_ttl |
750 | @@ -281,7 +288,7 @@ |
751 | "No more IPs available in subnet: %s" % subnet.cidr) |
752 | return str(IPAddress(free_ranges[0].first)) |
753 | |
754 | - def get_hostname_ip_mapping(self, domain_or_subnet): |
755 | + def get_hostname_ip_mapping(self, domain_or_subnet, raw_ttl=False): |
756 | """Return hostname mappings for `StaticIPAddress` entries. |
757 | |
758 | Returns a mapping `{hostnames -> (ttl, [ips])}` corresponding to |
759 | @@ -308,14 +315,20 @@ |
760 | raise ValueError('bad object passed to get_hostname_ip_mapping') |
761 | |
762 | default_ttl = "%d" % Config.objects.get_config('default_dns_ttl') |
763 | + if raw_ttl: |
764 | + ttl_clause = """node.address_ttl""" |
765 | + else: |
766 | + ttl_clause = """ |
767 | + COALESCE( |
768 | + node.address_ttl, |
769 | + domain.ttl, |
770 | + %s)""" % default_ttl |
771 | sql_query = """ |
772 | SELECT DISTINCT ON (node.hostname, family(staticip.ip)) |
773 | node.hostname, |
774 | node.system_id, |
775 | - COALESCE( |
776 | - node.address_ttl, |
777 | - domain.ttl, |
778 | - """ + default_ttl + """) AS ttl, |
779 | + node.node_type, |
780 | + """ + ttl_clause + """ AS ttl, |
781 | domain.name, staticip.ip |
782 | FROM |
783 | maasserver_interface AS interface |
784 | @@ -375,10 +388,12 @@ |
785 | """ |
786 | # We get user reserved et al mappings first, so that we can overwrite |
787 | # TTL as we process the return from the SQL horror above. |
788 | - mapping = self._get_user_reserved_mappings() |
789 | + mapping = self._get_user_reserved_mappings(domain_or_subnet) |
790 | cursor.execute(sql_query, (domain_or_subnet.id,)) |
791 | - for (node_name, system_id, ttl, domain_name, ip) in cursor.fetchall(): |
792 | + for (node_name, system_id, node_type, |
793 | + ttl, domain_name, ip) in cursor.fetchall(): |
794 | hostname = "%s.%s" % (strip_domain(node_name), domain_name) |
795 | + mapping[hostname].node_type = node_type |
796 | mapping[hostname].system_id = system_id |
797 | mapping[hostname].ttl = ttl |
798 | mapping[hostname].ips.add(ip) |
799 | @@ -621,6 +636,7 @@ |
800 | data["node_summary"] = { |
801 | "system_id": node.system_id, |
802 | "node_type": node.node_type, |
803 | + "fqdn": node.fqdn, |
804 | } |
805 | if (with_username and |
806 | self.alloc_type != IPADDRESS_TYPE.DISCOVERED): |
807 | |
808 | === modified file 'src/maasserver/models/tests/test_dnsdata.py' |
809 | --- src/maasserver/models/tests/test_dnsdata.py 2016-02-03 09:11:25 +0000 |
810 | +++ src/maasserver/models/tests/test_dnsdata.py 2016-02-18 16:28:50 +0000 |
811 | @@ -226,7 +226,7 @@ |
812 | class TestDNSDataMapping(MAASServerTestCase): |
813 | """Tests for get_hostname_dnsdata_mapping().""" |
814 | |
815 | - def make_mapping(self, dnsresource): |
816 | + def make_mapping(self, dnsresource, raw_ttl=False): |
817 | nodes = Node.objects.filter( |
818 | hostname=dnsresource.name, domain=dnsresource.domain) |
819 | if nodes.count() > 0: |
820 | @@ -235,7 +235,7 @@ |
821 | system_id = None |
822 | mapping = HostnameRRsetMapping(system_id) |
823 | for data in dnsresource.dnsdata_set.all(): |
824 | - if data.ttl is not None: |
825 | + if raw_ttl or data.ttl is not None: |
826 | ttl = data.ttl |
827 | elif dnsresource.domain.ttl is not None: |
828 | ttl = dnsresource.domain.ttl |
829 | @@ -279,3 +279,24 @@ |
830 | expected_mapping.update(self.make_mapping(dnsrr)) |
831 | actual = DNSData.objects.get_hostname_dnsdata_mapping(dom) |
832 | self.assertItemsEqual(expected_mapping, actual) |
833 | + |
834 | + def test_get_hostname_dnsdata_mapping_returns_raw_ttl(self): |
835 | + # We create 2 domains, one with a ttl, one withoout. |
836 | + # Within each domain, create an RRset with and without ttl. |
837 | + # We then query with raw_ttl=True, and confirm that nothing is |
838 | + # inherited. |
839 | + global_ttl = random.randint(1, 99) |
840 | + Config.objects.set_config('default_dns_ttl', global_ttl) |
841 | + domains = [ |
842 | + factory.make_Domain(), |
843 | + factory.make_Domain(ttl=random.randint(100, 199))] |
844 | + for dom in domains: |
845 | + factory.make_DNSData(domain=dom) |
846 | + factory.make_DNSData(domain=dom, ttl=random.randint(200, 299)) |
847 | + expected_mapping = {} |
848 | + for dnsrr in dom.dnsresource_set.all(): |
849 | + expected_mapping.update(self.make_mapping( |
850 | + dnsrr, raw_ttl=True)) |
851 | + actual = DNSData.objects.get_hostname_dnsdata_mapping( |
852 | + dom, raw_ttl=True) |
853 | + self.assertItemsEqual(expected_mapping, actual) |
854 | |
855 | === modified file 'src/maasserver/models/tests/test_domain.py' |
856 | --- src/maasserver/models/tests/test_domain.py 2016-02-05 07:23:05 +0000 |
857 | +++ src/maasserver/models/tests/test_domain.py 2016-02-18 16:28:50 +0000 |
858 | @@ -23,6 +23,7 @@ |
859 | from maasserver.models.staticipaddress import StaticIPAddress |
860 | from maasserver.testing.factory import factory |
861 | from maasserver.testing.testcase import MAASServerTestCase |
862 | +from netaddr import IPAddress |
863 | from testtools.matchers import MatchesStructure |
864 | from testtools.testcase import ExpectedException |
865 | |
866 | @@ -212,67 +213,45 @@ |
867 | dnsresource__domain_id=domain.id) |
868 | self.assertEqual("0 0 1688 %s." % target, srvrr.rrdata) |
869 | |
870 | - def render_ipaddresses(self, domain, for_list=False): |
871 | - ip_map = StaticIPAddress.objects.get_hostname_ip_mapping(domain) |
872 | - ip_addresses = [ |
873 | - { |
874 | - # strip off the domain name. |
875 | - 'hostname': hostname[:-len(domain.name) - 1], |
876 | - 'system_id': info.system_id, |
877 | - 'ttl': info.ttl, |
878 | - 'ips': info.ips} |
879 | - for hostname, info in ip_map.items() |
880 | - ] |
881 | - count = 0 |
882 | - for record in ip_addresses: |
883 | - count += len(record['ips']) |
884 | - if for_list: |
885 | - ip_addresses = [] |
886 | - return (ip_addresses, count) |
887 | - |
888 | def render_rrdata(self, domain, for_list=False): |
889 | - rr_map = DNSData.objects.get_hostname_dnsdata_mapping(domain) |
890 | + rr_map = DNSData.objects.get_hostname_dnsdata_mapping( |
891 | + domain, raw_ttl=True) |
892 | + ip_map = StaticIPAddress.objects.get_hostname_ip_mapping( |
893 | + domain, raw_ttl=True) |
894 | + for hostname, info in ip_map.items(): |
895 | + hostname = hostname[:-len(domain.name) - 1] |
896 | + if info.system_id is not None: |
897 | + rr_map[hostname].system_id = info.system_id |
898 | + for ip in info.ips: |
899 | + if IPAddress(ip).version == 4: |
900 | + rr_map[hostname].rrset.add((info.ttl, 'A', ip)) |
901 | + else: |
902 | + rr_map[hostname].rrset.add((info.ttl, 'AAAA', ip)) |
903 | rrsets = [ |
904 | { |
905 | - 'hostname': hostname, |
906 | + 'name': name, |
907 | 'system_id': info.system_id, |
908 | - 'rrsets': info.rrset, |
909 | + 'node_type': info.node_type, |
910 | + 'ttl': ttl, |
911 | + 'rrtype': rrtype, |
912 | + 'rrdata': rrdata, |
913 | } |
914 | - for hostname, info in rr_map.items() |
915 | + for name, info in rr_map.items() |
916 | + for ttl, rrtype, rrdata in info.rrset |
917 | ] |
918 | - count = 0 |
919 | - for record in rrsets: |
920 | - count += len(record['rrsets']) |
921 | - if for_list: |
922 | - rrsets = [] |
923 | - return (rrsets, count) |
924 | - |
925 | - def test_render_json_for_related_ips_returns_correct_values(self): |
926 | - domain = factory.make_Domain() |
927 | - factory.make_DNSData(domain=domain) |
928 | - dnsdata = factory.make_DNSData(domain=domain, rrtype='TXT') |
929 | - factory.make_DNSData(dnsresource=dnsdata.dnsresource, rrtype='TXT') |
930 | - factory.make_DNSResource(domain=domain) |
931 | - node = factory.make_Node_with_Interface_on_Subnet(domain=domain) |
932 | - factory.make_DNSResource(name=node.hostname, domain=domain) |
933 | - self.assertItemsEqual( |
934 | - self.render_ipaddresses(domain, for_list=True), |
935 | - domain.render_json_for_related_ips(for_list=True)) |
936 | - self.assertItemsEqual( |
937 | - self.render_ipaddresses(domain, for_list=False), |
938 | - domain.render_json_for_related_ips(for_list=False)) |
939 | + return (rrsets) |
940 | |
941 | def test_render_json_for_related_rrdata_returns_correct_values(self): |
942 | domain = factory.make_Domain() |
943 | - factory.make_DNSData(domain=domain) |
944 | - dnsdata = factory.make_DNSData(domain=domain, rrtype='TXT') |
945 | - factory.make_DNSData(dnsresource=dnsdata.dnsresource, rrtype='TXT') |
946 | + factory.make_DNSData(domain=domain, rrtype='NS') |
947 | + dnsdata = factory.make_DNSData(domain=domain, rrtype='MX') |
948 | + factory.make_DNSData(dnsresource=dnsdata.dnsresource, rrtype='MX') |
949 | factory.make_DNSResource(domain=domain) |
950 | node = factory.make_Node_with_Interface_on_Subnet(domain=domain) |
951 | factory.make_DNSResource(name=node.hostname, domain=domain) |
952 | - self.assertItemsEqual( |
953 | - self.render_rrdata(domain, for_list=True), |
954 | - domain.render_json_for_related_rrdata(for_list=True)) |
955 | - self.assertItemsEqual( |
956 | - self.render_rrdata(domain, for_list=False), |
957 | - domain.render_json_for_related_rrdata(for_list=False)) |
958 | + expected = self.render_rrdata(domain, for_list=True) |
959 | + actual = domain.render_json_for_related_rrdata(for_list=True) |
960 | + self.assertItemsEqual(expected, actual) |
961 | + expected = self.render_rrdata(domain, for_list=False) |
962 | + actual = domain.render_json_for_related_rrdata(for_list=False) |
963 | + self.assertItemsEqual(expected, actual) |
964 | |
965 | === modified file 'src/maasserver/models/tests/test_node.py' |
966 | --- src/maasserver/models/tests/test_node.py 2016-02-02 14:20:45 +0000 |
967 | +++ src/maasserver/models/tests/test_node.py 2016-02-18 16:28:50 +0000 |
968 | @@ -3102,7 +3102,8 @@ |
969 | INTERFACE_TYPE.PHYSICAL, node=node, vlan=vlan) |
970 | for _ in range(3) |
971 | ] |
972 | - subnet = factory.make_Subnet(vlan=vlan) |
973 | + subnet = factory.make_Subnet( |
974 | + vlan=vlan, host_bits=random.randint(4, 12)) |
975 | for interface in interfaces: |
976 | for _ in range(2): |
977 | factory.make_StaticIPAddress( |
978 | |
979 | === modified file 'src/maasserver/models/tests/test_staticipaddress.py' |
980 | --- src/maasserver/models/tests/test_staticipaddress.py 2016-02-11 00:20:13 +0000 |
981 | +++ src/maasserver/models/tests/test_staticipaddress.py 2016-02-18 16:28:50 +0000 |
982 | @@ -23,6 +23,7 @@ |
983 | StaticIPAddressOutOfRange, |
984 | StaticIPAddressUnavailable, |
985 | ) |
986 | +from maasserver.models.config import Config |
987 | from maasserver.models.domain import Domain |
988 | from maasserver.models.staticipaddress import ( |
989 | HostnameIPMapping, |
990 | @@ -348,7 +349,7 @@ |
991 | subnet=subnet, interface=boot_interface) |
992 | full_hostname = "%s.%s" % (node.hostname, domain.name) |
993 | expected_mapping[full_hostname] = HostnameIPMapping( |
994 | - node.system_id, 30, {staticip.ip}) |
995 | + node.system_id, 30, {staticip.ip}, node.node_type) |
996 | mapping = StaticIPAddress.objects.get_hostname_ip_mapping(domain) |
997 | self.assertEqual(expected_mapping, mapping) |
998 | |
999 | @@ -368,7 +369,78 @@ |
1000 | subnet=subnet, interface=boot_interface) |
1001 | mapping = StaticIPAddress.objects.get_hostname_ip_mapping(subnet) |
1002 | self.assertEqual({full_hostname: HostnameIPMapping( |
1003 | - node.system_id, 30, {staticip.ip})}, mapping) |
1004 | + node.system_id, 30, {staticip.ip}, node.node_type)}, mapping) |
1005 | + |
1006 | + def make_mapping(self, node, raw_ttl=False): |
1007 | + if raw_ttl or node.address_ttl is not None: |
1008 | + ttl = node.address_ttl |
1009 | + elif node.domain.ttl is not None: |
1010 | + ttl = node.domain.ttl |
1011 | + else: |
1012 | + ttl = Config.objects.get_config('default_dns_ttl') |
1013 | + mapping = HostnameIPMapping( |
1014 | + system_id=node.system_id, node_type=node.node_type, ttl=ttl) |
1015 | + for ip in node.boot_interface.ip_addresses.all(): |
1016 | + mapping.ips.add(str(ip.ip)) |
1017 | + return {node.fqdn: mapping} |
1018 | + |
1019 | + def test_get_hostname_ip_mapping_inherits_ttl(self): |
1020 | + # We create 2 domains, one with a ttl, one withoout. |
1021 | + # Within each domain, create a node with an address_ttl, and one |
1022 | + # without. |
1023 | + global_ttl = randint(1, 99) |
1024 | + Config.objects.set_config('default_dns_ttl', global_ttl) |
1025 | + domains = [ |
1026 | + factory.make_Domain(), |
1027 | + factory.make_Domain(ttl=randint(100, 199))] |
1028 | + subnet = factory.make_Subnet(host_bits=randint(4, 15)) |
1029 | + for dom in domains: |
1030 | + for ttl in (None, randint(200, 299)): |
1031 | + node = factory.make_Node_with_Interface_on_Subnet( |
1032 | + interface=True, hostname="%s.%s" % ( |
1033 | + factory.make_name('hostname'), dom.name), |
1034 | + subnet=subnet, disable_ipv4=False, address_ttl=ttl) |
1035 | + boot_interface = node.get_boot_interface() |
1036 | + factory.make_StaticIPAddress( |
1037 | + alloc_type=IPADDRESS_TYPE.STICKY, |
1038 | + ip=factory.pick_ip_in_Subnet(subnet), |
1039 | + subnet=subnet, interface=boot_interface) |
1040 | + expected_mapping = {} |
1041 | + for node in dom.node_set.all(): |
1042 | + expected_mapping.update(self.make_mapping(node)) |
1043 | + actual = StaticIPAddress.objects.get_hostname_ip_mapping(dom) |
1044 | + self.assertItemsEqual(expected_mapping, actual) |
1045 | + |
1046 | + def test_get_hostname_ip_mapping_returns_raw_ttl(self): |
1047 | + # We create 2 domains, one with a ttl, one withoout. |
1048 | + # Within each domain, create a node with an address_ttl, and one |
1049 | + # without. |
1050 | + # We then query with raw_ttl=True, and confirm that nothing is |
1051 | + # inherited. |
1052 | + global_ttl = randint(1, 99) |
1053 | + Config.objects.set_config('default_dns_ttl', global_ttl) |
1054 | + domains = [ |
1055 | + factory.make_Domain(), |
1056 | + factory.make_Domain(ttl=randint(100, 199))] |
1057 | + subnet = factory.make_Subnet() |
1058 | + for dom in domains: |
1059 | + for ttl in (None, randint(200, 299)): |
1060 | + node = factory.make_Node_with_Interface_on_Subnet( |
1061 | + interface=True, hostname="%s.%s" % ( |
1062 | + factory.make_name('hostname'), dom.name), |
1063 | + subnet=subnet, disable_ipv4=False, address_ttl=ttl) |
1064 | + boot_interface = node.get_boot_interface() |
1065 | + factory.make_StaticIPAddress( |
1066 | + alloc_type=IPADDRESS_TYPE.STICKY, |
1067 | + ip=factory.pick_ip_in_Subnet(subnet), |
1068 | + subnet=subnet, interface=boot_interface) |
1069 | + expected_mapping = {} |
1070 | + for node in dom.node_set.all(): |
1071 | + expected_mapping.update(self.make_mapping( |
1072 | + node, raw_ttl=True)) |
1073 | + actual = StaticIPAddress.objects.get_hostname_ip_mapping( |
1074 | + dom, raw_ttl=True) |
1075 | + self.assertItemsEqual(expected_mapping, actual) |
1076 | |
1077 | def test_get_hostname_ip_mapping_picks_mac_with_static_address(self): |
1078 | node = factory.make_Node_with_Interface_on_Subnet( |
1079 | @@ -383,7 +455,7 @@ |
1080 | mapping = StaticIPAddress.objects.get_hostname_ip_mapping( |
1081 | node.domain) |
1082 | self.assertEqual({node.fqdn: HostnameIPMapping( |
1083 | - node.system_id, 30, {staticip.ip})}, mapping) |
1084 | + node.system_id, 30, {staticip.ip}, node.node_type)}, mapping) |
1085 | |
1086 | def test_get_hostname_ip_mapping_considers_given_domain(self): |
1087 | domain = factory.make_Domain() |
1088 | @@ -409,7 +481,7 @@ |
1089 | mapping = StaticIPAddress.objects.get_hostname_ip_mapping( |
1090 | node.domain) |
1091 | self.assertEqual({node.fqdn: HostnameIPMapping( |
1092 | - node.system_id, 30, {staticip.ip})}, mapping) |
1093 | + node.system_id, 30, {staticip.ip}, node.node_type)}, mapping) |
1094 | |
1095 | def test_get_hostname_ip_mapping_picks_sticky_over_auto(self): |
1096 | subnet = factory.make_Subnet( |
1097 | @@ -428,7 +500,7 @@ |
1098 | mapping = StaticIPAddress.objects.get_hostname_ip_mapping( |
1099 | node.domain) |
1100 | self.assertEqual({node.fqdn: HostnameIPMapping( |
1101 | - node.system_id, 30, {staticip.ip})}, mapping) |
1102 | + node.system_id, 30, {staticip.ip}, node.node_type)}, mapping) |
1103 | |
1104 | def test_get_hostname_ip_mapping_combines_IPv4_and_IPv6_addresses(self): |
1105 | node = factory.make_Node(interface=True, disable_ipv4=False) |
1106 | @@ -447,7 +519,8 @@ |
1107 | node.domain) |
1108 | self.assertItemsEqual( |
1109 | {node.fqdn: HostnameIPMapping( |
1110 | - node.system_id, 30, {ipv4_address.ip, ipv6_address.ip})}, |
1111 | + node.system_id, 30, {ipv4_address.ip, ipv6_address.ip}, |
1112 | + node.node_type)}, |
1113 | mapping) |
1114 | |
1115 | def test_get_hostname_ip_mapping_combines_MACs_for_same_node(self): |
1116 | @@ -473,7 +546,7 @@ |
1117 | self.assertItemsEqual( |
1118 | {node.fqdn: HostnameIPMapping( |
1119 | node.system_id, 30, |
1120 | - {ipv4_address.ip, ipv6_address.ip})}, |
1121 | + {ipv4_address.ip, ipv6_address.ip}, node.node_type)}, |
1122 | mapping) |
1123 | |
1124 | def test_get_hostname_ip_mapping_skips_ipv4_if_disable_ipv4_set(self): |
1125 | @@ -494,7 +567,7 @@ |
1126 | mapping = StaticIPAddress.objects.get_hostname_ip_mapping( |
1127 | node.domain) |
1128 | self.assertEqual({node.fqdn: HostnameIPMapping( |
1129 | - node.system_id, 30, {ipv6_address.ip})}, mapping) |
1130 | + node.system_id, 30, {ipv6_address.ip}, node.node_type)}, mapping) |
1131 | |
1132 | def test_get_hostname_ip_mapping_prefers_non_discovered_addresses(self): |
1133 | subnet = factory.make_Subnet( |
1134 | @@ -512,7 +585,7 @@ |
1135 | mapping = StaticIPAddress.objects.get_hostname_ip_mapping( |
1136 | node.domain) |
1137 | self.assertEqual({node.fqdn: HostnameIPMapping( |
1138 | - node.system_id, 30, {staticip.ip})}, mapping) |
1139 | + node.system_id, 30, {staticip.ip}, node.node_type)}, mapping) |
1140 | |
1141 | def test_get_hostname_ip_mapping_prefers_bond_with_no_boot_interface(self): |
1142 | subnet = factory.make_Subnet( |
1143 | @@ -542,7 +615,7 @@ |
1144 | mapping = StaticIPAddress.objects.get_hostname_ip_mapping( |
1145 | node.domain) |
1146 | self.assertEqual({node.fqdn: HostnameIPMapping( |
1147 | - node.system_id, 30, {bond_staticip.ip})}, mapping) |
1148 | + node.system_id, 30, {bond_staticip.ip}, node.node_type)}, mapping) |
1149 | |
1150 | def test_get_hostname_ip_mapping_prefers_bond_with_boot_interface(self): |
1151 | subnet = factory.make_Subnet( |
1152 | @@ -566,7 +639,7 @@ |
1153 | mapping = StaticIPAddress.objects.get_hostname_ip_mapping( |
1154 | node.domain) |
1155 | self.assertEqual({node.fqdn: HostnameIPMapping( |
1156 | - node.system_id, 30, {bond_staticip.ip})}, mapping) |
1157 | + node.system_id, 30, {bond_staticip.ip}, node.node_type)}, mapping) |
1158 | |
1159 | def test_get_hostname_ip_mapping_ignores_bond_without_boot_interface(self): |
1160 | subnet = factory.make_Subnet( |
1161 | @@ -594,7 +667,7 @@ |
1162 | mapping = StaticIPAddress.objects.get_hostname_ip_mapping( |
1163 | node.domain) |
1164 | self.assertEqual({node.fqdn: HostnameIPMapping( |
1165 | - node.system_id, 30, {boot_staticip.ip})}, mapping) |
1166 | + node.system_id, 30, {boot_staticip.ip}, node.node_type)}, mapping) |
1167 | |
1168 | def test_get_hostname_ip_mapping_prefers_boot_interface(self): |
1169 | subnet = factory.make_Subnet( |
1170 | @@ -617,7 +690,7 @@ |
1171 | mapping = StaticIPAddress.objects.get_hostname_ip_mapping( |
1172 | node.domain) |
1173 | self.assertEqual({node.fqdn: HostnameIPMapping( |
1174 | - node.system_id, 30, {boot_sip.ip})}, mapping) |
1175 | + node.system_id, 30, {boot_sip.ip}, node.node_type)}, mapping) |
1176 | |
1177 | def test_get_hostname_ip_mapping_prefers_boot_interface_to_alias(self): |
1178 | subnet = factory.make_Subnet( |
1179 | @@ -640,7 +713,7 @@ |
1180 | mapping = StaticIPAddress.objects.get_hostname_ip_mapping( |
1181 | node.domain) |
1182 | self.assertEqual({node.fqdn: HostnameIPMapping( |
1183 | - node.system_id, 30, {boot_sip.ip})}, mapping) |
1184 | + node.system_id, 30, {boot_sip.ip}, node.node_type)}, mapping) |
1185 | |
1186 | def test_get_hostname_ip_mapping_prefers_physical_interfaces_to_vlan(self): |
1187 | subnet = factory.make_Subnet( |
1188 | @@ -660,7 +733,7 @@ |
1189 | mapping = StaticIPAddress.objects.get_hostname_ip_mapping( |
1190 | node.domain) |
1191 | self.assertEqual({node.fqdn: HostnameIPMapping( |
1192 | - node.system_id, 30, {phy_staticip.ip})}, mapping) |
1193 | + node.system_id, 30, {phy_staticip.ip}, node.node_type)}, mapping) |
1194 | |
1195 | |
1196 | class TestStaticIPAddress(MAASServerTestCase): |
1197 | @@ -727,39 +800,56 @@ |
1198 | class TestUserReservedStaticIPAddress(MAASServerTestCase): |
1199 | |
1200 | def test_user_reserved_addresses_have_default_hostnames(self): |
1201 | + subnet = factory.make_Subnet() |
1202 | num_ips = randint(3, 5) |
1203 | ips = [ |
1204 | factory.make_StaticIPAddress( |
1205 | - alloc_type=IPADDRESS_TYPE.USER_RESERVED) |
1206 | + subnet=subnet, alloc_type=IPADDRESS_TYPE.USER_RESERVED) |
1207 | for _ in range(num_ips) |
1208 | ] |
1209 | - mappings = StaticIPAddress.objects._get_user_reserved_mappings() |
1210 | + mappings = StaticIPAddress.objects._get_user_reserved_mappings( |
1211 | + subnet) |
1212 | self.expectThat(mappings, HasLength(len(ips))) |
1213 | |
1214 | def test_user_reserved_addresses_included_in_get_hostname_ip_mapping(self): |
1215 | num_ips = randint(3, 5) |
1216 | - domain = factory.make_Domain() |
1217 | + domain0 = Domain.objects.get_default_domain() |
1218 | + domain1 = factory.make_Domain() |
1219 | ips = [ |
1220 | factory.make_StaticIPAddress( |
1221 | + hostname="%s.%s" % (factory.make_name('host'), domain0.name), |
1222 | alloc_type=IPADDRESS_TYPE.USER_RESERVED) |
1223 | for _ in range(num_ips) |
1224 | ] |
1225 | - mappings = StaticIPAddress.objects.get_hostname_ip_mapping(domain) |
1226 | + mappings = StaticIPAddress.objects.get_hostname_ip_mapping(domain0) |
1227 | self.expectThat(mappings, HasLength(len(ips))) |
1228 | + mappings = StaticIPAddress.objects.get_hostname_ip_mapping(domain1) |
1229 | + self.expectThat(mappings, HasLength(0)) |
1230 | |
1231 | - def test_user_reserved_addresses_included_in_all_nodegroups(self): |
1232 | - num_ips = randint(3, 5) |
1233 | + def test_user_reserved_addresses_included_in_correct_domains(self): |
1234 | + domain0 = Domain.objects.get_default_domain() |
1235 | domain1 = factory.make_Domain() |
1236 | domain2 = factory.make_Domain() |
1237 | - ips = [ |
1238 | - factory.make_StaticIPAddress( |
1239 | - alloc_type=IPADDRESS_TYPE.USER_RESERVED) |
1240 | - for _ in range(num_ips) |
1241 | - ] |
1242 | + ips1 = [ |
1243 | + factory.make_StaticIPAddress( |
1244 | + hostname="%s.%s" % ( |
1245 | + factory.make_name('host'), domain1.name), |
1246 | + alloc_type=IPADDRESS_TYPE.USER_RESERVED) |
1247 | + for _ in range(randint(3, 5)) |
1248 | + ] |
1249 | + ips2 = [ |
1250 | + factory.make_StaticIPAddress( |
1251 | + hostname="%s.%s" % ( |
1252 | + factory.make_name('host'), domain2.name), |
1253 | + alloc_type=IPADDRESS_TYPE.USER_RESERVED) |
1254 | + for _ in range(randint(1, 2)) |
1255 | + ] |
1256 | + mappings = StaticIPAddress.objects.get_hostname_ip_mapping(domain0) |
1257 | + self.expectThat(mappings, HasLength(0)) |
1258 | mappings = StaticIPAddress.objects.get_hostname_ip_mapping(domain1) |
1259 | - self.expectThat(mappings, HasLength(len(ips))) |
1260 | + self.expectThat(mappings, HasLength(len(ips1))) |
1261 | mappings = StaticIPAddress.objects.get_hostname_ip_mapping(domain2) |
1262 | - self.expectThat(mappings, HasLength(len(ips))) |
1263 | + self.expectThat(mappings, HasLength(len(ips2))) |
1264 | |
1265 | |
1266 | class TestRenderJSON(MAASServerTestCase): |
1267 | |
1268 | === added file 'src/maasserver/static/js/angular/controllers/domain_details.js' |
1269 | --- src/maasserver/static/js/angular/controllers/domain_details.js 1970-01-01 00:00:00 +0000 |
1270 | +++ src/maasserver/static/js/angular/controllers/domain_details.js 2016-02-18 16:28:50 +0000 |
1271 | @@ -0,0 +1,61 @@ |
1272 | +/* Copyright 2015,2016 Canonical Ltd. This software is licensed under the |
1273 | + * GNU Affero General Public License version 3 (see the file LICENSE). |
1274 | + * |
1275 | + * MAAS Domain Details Controller |
1276 | + */ |
1277 | + |
1278 | +angular.module('MAAS').controller('DomainDetailsController', [ |
1279 | + '$scope', '$rootScope', '$routeParams', '$location', |
1280 | + 'DomainsManager', 'ManagerHelperService', 'ErrorService', |
1281 | + function( |
1282 | + $scope, $rootScope, $routeParams, $location, |
1283 | + DomainsManager, ManagerHelperService, ErrorService) { |
1284 | + |
1285 | + // Set title and page. |
1286 | + $rootScope.title = "Loading..."; |
1287 | + |
1288 | + // Note: this value must match the top-level tab, in order for |
1289 | + // highlighting to occur properly. |
1290 | + $rootScope.page = "domains"; |
1291 | + |
1292 | + // Initial values. |
1293 | + $scope.loaded = false; |
1294 | + $scope.domain = null; |
1295 | + $scope.predicate = "name"; |
1296 | + $scope.reverse = false; |
1297 | + |
1298 | + // Updates the page title. |
1299 | + function updateTitle() { |
1300 | + $rootScope.title = $scope.domain.name; |
1301 | + } |
1302 | + |
1303 | + // Called when the domain has been loaded. |
1304 | + function domainLoaded(domain) { |
1305 | + $scope.domain = domain; |
1306 | + $scope.loaded = true; |
1307 | + |
1308 | + updateTitle(); |
1309 | + } |
1310 | + |
1311 | + // Load all the required managers. |
1312 | + ManagerHelperService.loadManager(DomainsManager).then(function() { |
1313 | + // Possibly redirected from another controller that already had |
1314 | + // this domain set to active. Only call setActiveItem if not |
1315 | + // already the activeItem. |
1316 | + var activeDomain = DomainsManager.getActiveItem(); |
1317 | + var requestedDomain = parseInt($routeParams.domain_id, 10); |
1318 | + if(isNaN(requestedDomain)) { |
1319 | + ErrorService.raiseError("Invalid domain identifier."); |
1320 | + } else if(angular.isObject(activeDomain) && |
1321 | + activeDomain.id === requestedDomain) { |
1322 | + domainLoaded(activeDomain); |
1323 | + } else { |
1324 | + DomainsManager.setActiveItem( |
1325 | + requestedDomain).then(function(node) { |
1326 | + domainLoaded(node); |
1327 | + }, function(error) { |
1328 | + ErrorService.raiseError(error); |
1329 | + }); |
1330 | + } |
1331 | + }); |
1332 | + }]); |
1333 | |
1334 | === modified file 'src/maasserver/static/js/angular/controllers/domains_list.js' |
1335 | --- src/maasserver/static/js/angular/controllers/domains_list.js 2016-02-04 16:17:08 +0000 |
1336 | +++ src/maasserver/static/js/angular/controllers/domains_list.js 2016-02-18 16:28:50 +0000 |
1337 | @@ -23,7 +23,7 @@ |
1338 | $scope.reverse = false; |
1339 | $scope.loading = true; |
1340 | |
1341 | - ManagerHelperService.loadManagers([DomainsManager]).then( |
1342 | + ManagerHelperService.loadManager(DomainsManager).then( |
1343 | function() { |
1344 | $scope.loading = false; |
1345 | }); |
1346 | |
1347 | === added file 'src/maasserver/static/js/angular/controllers/tests/test_domain_details.js' |
1348 | --- src/maasserver/static/js/angular/controllers/tests/test_domain_details.js 1970-01-01 00:00:00 +0000 |
1349 | +++ src/maasserver/static/js/angular/controllers/tests/test_domain_details.js 2016-02-18 16:28:50 +0000 |
1350 | @@ -0,0 +1,158 @@ |
1351 | +/* Copyright 2015 Canonical Ltd. This software is licensed under the |
1352 | + * GNU Affero General Public License version 3 (see the file LICENSE). |
1353 | + * |
1354 | + * Unit tests for DomainsListController. |
1355 | + */ |
1356 | + |
1357 | +describe("DomainDetailsController", function() { |
1358 | + |
1359 | + // Load the MAAS module. |
1360 | + beforeEach(module("MAAS")); |
1361 | + |
1362 | + // Make a fake domain |
1363 | + function makeDomain() { |
1364 | + var domain = { |
1365 | + id: makeInteger(1, 10000), |
1366 | + name: 'example.com', |
1367 | + authoritative: true |
1368 | + }; |
1369 | + DomainsManager._items.push(domain); |
1370 | + return domain; |
1371 | + } |
1372 | + |
1373 | + // Grab the needed angular pieces. |
1374 | + var $controller, $rootScope, $location, $scope, $q, $routeParams; |
1375 | + beforeEach(inject(function($injector) { |
1376 | + $controller = $injector.get("$controller"); |
1377 | + $rootScope = $injector.get("$rootScope"); |
1378 | + $location = $injector.get("$location"); |
1379 | + $scope = $rootScope.$new(); |
1380 | + $q = $injector.get("$q"); |
1381 | + $routeParams = {}; |
1382 | + })); |
1383 | + |
1384 | + // Load any injected managers and services. |
1385 | + var DomainsManager, ManagerHelperService, ErrorService; |
1386 | + beforeEach(inject(function($injector) { |
1387 | + DomainsManager = $injector.get("DomainsManager"); |
1388 | + ManagerHelperService = $injector.get("ManagerHelperService"); |
1389 | + ErrorService = $injector.get("ErrorService"); |
1390 | + })); |
1391 | + |
1392 | + var domain; |
1393 | + beforeEach(function() { |
1394 | + domain = makeDomain(); |
1395 | + }); |
1396 | + |
1397 | + // Makes the NodesListController |
1398 | + function makeController(loadManagerDefer) { |
1399 | + var loadManager = spyOn(ManagerHelperService, "loadManager"); |
1400 | + if(angular.isObject(loadManagerDefer)) { |
1401 | + loadManager.and.returnValue(loadManagerDefer.promise); |
1402 | + } else { |
1403 | + loadManager.and.returnValue($q.defer().promise); |
1404 | + } |
1405 | + |
1406 | + // Create the controller. |
1407 | + var controller = $controller("DomainDetailsController", { |
1408 | + $scope: $scope, |
1409 | + $rootScope: $rootScope, |
1410 | + $routeParams: $routeParams, |
1411 | + $location: $location, |
1412 | + DomainsManager: DomainsManager, |
1413 | + ManagerHelperService: ManagerHelperService, |
1414 | + ErrorService: ErrorService |
1415 | + }); |
1416 | + |
1417 | + return controller; |
1418 | + } |
1419 | + |
1420 | + // Make the controller and resolve the setActiveItem call. |
1421 | + function makeControllerResolveSetActiveItem() { |
1422 | + var setActiveDefer = $q.defer(); |
1423 | + spyOn(DomainsManager, "setActiveItem").and.returnValue( |
1424 | + setActiveDefer.promise); |
1425 | + var defer = $q.defer(); |
1426 | + var controller = makeController(defer); |
1427 | + $routeParams.domain_id = domain.id; |
1428 | + |
1429 | + defer.resolve(); |
1430 | + $rootScope.$digest(); |
1431 | + setActiveDefer.resolve(domain); |
1432 | + $rootScope.$digest(); |
1433 | + |
1434 | + return controller; |
1435 | + } |
1436 | + |
1437 | + it("sets title and page on $rootScope", function() { |
1438 | + var controller = makeController(); |
1439 | + expect($rootScope.title).toBe("Loading..."); |
1440 | + expect($rootScope.page).toBe("domains"); |
1441 | + }); |
1442 | + |
1443 | + it("calls loadManager with DomainsManager" + |
1444 | + function() { |
1445 | + var controller = makeController(); |
1446 | + expect(ManagerHelperService.loadManager).toHaveBeenCalledWith( |
1447 | + DomainsManager); |
1448 | + }); |
1449 | + |
1450 | + it("raises error if domain identifier is invalid", function() { |
1451 | + spyOn(DomainsManager, "setActiveItem").and.returnValue( |
1452 | + $q.defer().promise); |
1453 | + spyOn(ErrorService, "raiseError").and.returnValue( |
1454 | + $q.defer().promise); |
1455 | + var defer = $q.defer(); |
1456 | + var controller = makeController(defer); |
1457 | + $routeParams.domain_id = 'xyzzy'; |
1458 | + |
1459 | + defer.resolve(); |
1460 | + $rootScope.$digest(); |
1461 | + |
1462 | + expect($scope.domain).toBe(null); |
1463 | + expect($scope.loaded).toBe(false); |
1464 | + expect(DomainsManager.setActiveItem).not.toHaveBeenCalled(); |
1465 | + expect(ErrorService.raiseError).toHaveBeenCalled(); |
1466 | + }); |
1467 | + |
1468 | + it("doesn't call setActiveItem if domain is loaded", function() { |
1469 | + spyOn(DomainsManager, "setActiveItem").and.returnValue( |
1470 | + $q.defer().promise); |
1471 | + var defer = $q.defer(); |
1472 | + var controller = makeController(defer); |
1473 | + DomainsManager._activeItem = domain; |
1474 | + $routeParams.domain_id = domain.id; |
1475 | + |
1476 | + defer.resolve(); |
1477 | + $rootScope.$digest(); |
1478 | + |
1479 | + expect($scope.domain).toBe(domain); |
1480 | + expect($scope.loaded).toBe(true); |
1481 | + expect(DomainsManager.setActiveItem).not.toHaveBeenCalled(); |
1482 | + }); |
1483 | + |
1484 | + it("calls setActiveItem if domain is not active", function() { |
1485 | + spyOn(DomainsManager, "setActiveItem").and.returnValue( |
1486 | + $q.defer().promise); |
1487 | + var defer = $q.defer(); |
1488 | + var controller = makeController(defer); |
1489 | + $routeParams.domain_id = domain.id; |
1490 | + |
1491 | + defer.resolve(); |
1492 | + $rootScope.$digest(); |
1493 | + |
1494 | + expect(DomainsManager.setActiveItem).toHaveBeenCalledWith( |
1495 | + domain.id); |
1496 | + }); |
1497 | + |
1498 | + it("sets domain and loaded once setActiveItem resolves", function() { |
1499 | + var controller = makeControllerResolveSetActiveItem(); |
1500 | + expect($scope.domain).toBe(domain); |
1501 | + expect($scope.loaded).toBe(true); |
1502 | + }); |
1503 | + |
1504 | + it("title is updated once setActiveItem resolves", function() { |
1505 | + var controller = makeControllerResolveSetActiveItem(); |
1506 | + expect($rootScope.title).toBe(domain.name); |
1507 | + }); |
1508 | +}); |
1509 | |
1510 | === added file 'src/maasserver/static/js/angular/controllers/tests/test_domains_list.js' |
1511 | --- src/maasserver/static/js/angular/controllers/tests/test_domains_list.js 1970-01-01 00:00:00 +0000 |
1512 | +++ src/maasserver/static/js/angular/controllers/tests/test_domains_list.js 2016-02-18 16:28:50 +0000 |
1513 | @@ -0,0 +1,95 @@ |
1514 | +/* Copyright 2015,2016 Canonical Ltd. This software is licensed under the |
1515 | + * GNU Affero General Public License version 3 (see the file LICENSE). |
1516 | + * |
1517 | + * Unit tests for DomainsListController. |
1518 | + */ |
1519 | + |
1520 | +describe("DomainsListController", function() { |
1521 | + |
1522 | + // Load the MAAS module. |
1523 | + beforeEach(module("MAAS")); |
1524 | + |
1525 | + // Grab the needed angular pieces. |
1526 | + var $controller, $rootScope, $scope, $q, $routeParams; |
1527 | + beforeEach(inject(function($injector) { |
1528 | + $controller = $injector.get("$controller"); |
1529 | + $rootScope = $injector.get("$rootScope"); |
1530 | + $scope = $rootScope.$new(); |
1531 | + $q = $injector.get("$q"); |
1532 | + $routeParams = {}; |
1533 | + })); |
1534 | + |
1535 | + // Load the managers and services. |
1536 | + var DomainsManager; |
1537 | + var ManagerHelperService, RegionConnection; |
1538 | + beforeEach(inject(function($injector) { |
1539 | + DomainsManager = $injector.get("DomainsManager"); |
1540 | + ManagerHelperService = $injector.get("ManagerHelperService"); |
1541 | + })); |
1542 | + |
1543 | + // Makes the DomainsListController |
1544 | + function makeController(loadManagerDefer, defaultConnectDefer) { |
1545 | + var loadManager = spyOn(ManagerHelperService, "loadManager"); |
1546 | + if(angular.isObject(loadManagerDefer)) { |
1547 | + loadManager.and.returnValue(loadManagerDefer.promise); |
1548 | + } else { |
1549 | + loadManager.and.returnValue($q.defer().promise); |
1550 | + } |
1551 | + |
1552 | + // Create the controller. |
1553 | + var controller = $controller("DomainsListController", { |
1554 | + $scope: $scope, |
1555 | + $rootScope: $rootScope, |
1556 | + $routeParams: $routeParams, |
1557 | + DomainsManager: DomainsManager, |
1558 | + ManagerHelperService: ManagerHelperService |
1559 | + }); |
1560 | + |
1561 | + return controller; |
1562 | + } |
1563 | + |
1564 | + it("sets title and page on $rootScope", function() { |
1565 | + var controller = makeController(); |
1566 | + expect($rootScope.title).toBe("Domains"); |
1567 | + expect($rootScope.page).toBe("domains"); |
1568 | + }); |
1569 | + |
1570 | + it("sets initial values on $scope", function() { |
1571 | + // tab-independent variables. |
1572 | + var controller = makeController(); |
1573 | + expect($scope.domains).toBe(DomainsManager.getItems()); |
1574 | + expect($scope.loading).toBe(true); |
1575 | + }); |
1576 | + |
1577 | + it("calls loadManager with DomainsManager", |
1578 | + function() { |
1579 | + var controller = makeController(); |
1580 | + expect(ManagerHelperService.loadManager).toHaveBeenCalledWith( |
1581 | + DomainsManager); |
1582 | + }); |
1583 | + |
1584 | + it("sets loading to false with loadManager resolves", function() { |
1585 | + var defer = $q.defer(); |
1586 | + var controller = makeController(defer); |
1587 | + defer.resolve(); |
1588 | + $rootScope.$digest(); |
1589 | + expect($scope.loading).toBe(false); |
1590 | + }); |
1591 | + |
1592 | + setupController = function(domains) { |
1593 | + var defer = $q.defer(); |
1594 | + var controller = makeController(defer); |
1595 | + $scope.domains = domains; |
1596 | + DomainsManager._items = domains; |
1597 | + defer.resolve(); |
1598 | + $rootScope.$digest(); |
1599 | + return controller; |
1600 | + }; |
1601 | + |
1602 | + testUpdates = function(controller, domains, expectedDomainsData) { |
1603 | + $scope.domains = domains; |
1604 | + DomainsManager._items = domains; |
1605 | + $rootScope.$digest(); |
1606 | + expect($scope.data).toEqual(expectedDomainsData); |
1607 | + }; |
1608 | +}); |
1609 | |
1610 | === modified file 'src/maasserver/static/js/angular/controllers/tests/test_networks_list.js' |
1611 | --- src/maasserver/static/js/angular/controllers/tests/test_networks_list.js 2016-02-16 22:56:08 +0000 |
1612 | +++ src/maasserver/static/js/angular/controllers/tests/test_networks_list.js 2016-02-18 16:28:50 +0000 |
1613 | @@ -19,7 +19,7 @@ |
1614 | $routeParams = {}; |
1615 | })); |
1616 | |
1617 | - // Load the manages and services. |
1618 | + // Load the managers and services. |
1619 | var SubnetsManager, FabricsManager, SpacesManager, VLANsManager; |
1620 | var ManagerHelperService, RegionConnection; |
1621 | beforeEach(inject(function($injector) { |
1622 | @@ -30,7 +30,7 @@ |
1623 | ManagerHelperService = $injector.get("ManagerHelperService"); |
1624 | })); |
1625 | |
1626 | - // Makes the NodesListController |
1627 | + // Makes the SubnetsListController |
1628 | function makeController(loadManagersDefer, defaultConnectDefer) { |
1629 | var loadManagers = spyOn(ManagerHelperService, "loadManagers"); |
1630 | if(angular.isObject(loadManagersDefer)) { |
1631 | |
1632 | === modified file 'src/maasserver/static/js/angular/controllers/tests/test_subnet_details.js' |
1633 | --- src/maasserver/static/js/angular/controllers/tests/test_subnet_details.js 2016-02-16 22:56:08 +0000 |
1634 | +++ src/maasserver/static/js/angular/controllers/tests/test_subnet_details.js 2016-02-18 16:28:50 +0000 |
1635 | @@ -1,7 +1,7 @@ |
1636 | /* Copyright 2015 Canonical Ltd. This software is licensed under the |
1637 | * GNU Affero General Public License version 3 (see the file LICENSE). |
1638 | * |
1639 | - * Unit tests for SubentsListController. |
1640 | + * Unit tests for SubnetsListController. |
1641 | */ |
1642 | |
1643 | describe("SubnetDetailsController", function() { |
1644 | @@ -131,7 +131,7 @@ |
1645 | expect(SubnetsManager.setActiveItem).not.toHaveBeenCalled(); |
1646 | }); |
1647 | |
1648 | - it("calls setActiveItem if node is not active", function() { |
1649 | + it("calls setActiveItem if subnet is not active", function() { |
1650 | spyOn(SubnetsManager, "setActiveItem").and.returnValue( |
1651 | $q.defer().promise); |
1652 | var defer = $q.defer(); |
1653 | |
1654 | === modified file 'src/maasserver/static/js/angular/maas.js' |
1655 | --- src/maasserver/static/js/angular/maas.js 2016-02-17 01:05:38 +0000 |
1656 | +++ src/maasserver/static/js/angular/maas.js 2016-02-18 16:28:50 +0000 |
1657 | @@ -47,6 +47,14 @@ |
1658 | templateUrl: 'static/partials/domains-list.html', |
1659 | controller: 'DomainsListController' |
1660 | }). |
1661 | + when('/domain/:domain_id', { |
1662 | + templateUrl: 'static/partials/domain-details.html', |
1663 | + controller: 'DomainDetailsController' |
1664 | + }). |
1665 | + when('/subnets', { |
1666 | + templateUrl: 'static/partials/subnets-list.html', |
1667 | + controller: 'SubnetsListController' |
1668 | + }). |
1669 | when('/networks', { |
1670 | templateUrl: 'static/partials/networks-list.html', |
1671 | controller: 'NetworksListController' |
1672 | |
1673 | === added file 'src/maasserver/static/partials/domain-details.html' |
1674 | --- src/maasserver/static/partials/domain-details.html 1970-01-01 00:00:00 +0000 |
1675 | +++ src/maasserver/static/partials/domain-details.html 2016-02-18 16:28:50 +0000 |
1676 | @@ -0,0 +1,56 @@ |
1677 | +<header class="page-header margin-bottom" data-maas-sticky-header> |
1678 | + <div class="inner-wrapper"> |
1679 | + <h1 class="page-header__title twelvel-col"> |
1680 | + {$ domain.name $}: |
1681 | + <ng-pluralize data-ng-hide="loading" count="domain.hosts" |
1682 | + when="{'one': '{$ domain.hosts $} host,', 'other': '{$ domain.hosts $} hosts,'}"></ng-pluralize> |
1683 | + <ng-pluralize data-ng-hide="loading" count="domain.resource_count" |
1684 | + when="{'one': ' {$ domain.resource_count $} record total', 'other': ' {$ domain.resource_count $} records total'}"></ng-pluralize> |
1685 | + <span class="page-header__title--identicator" id="bulk-actions"> |
1686 | + <span class="power-status--power" data-ng-show="loading"> |
1687 | + <span class="loader"></span> |
1688 | + Loading... |
1689 | + </span> |
1690 | + </span> |
1691 | + </h1> |
1692 | + </div> |
1693 | +</header> |
1694 | +<div data-ng-show="!loading"> |
1695 | + <section class="row"> |
1696 | + <div class="inner-wrapper"> |
1697 | + <div class="twelve-col"> |
1698 | + <div class="table"> |
1699 | + <header class="table__head"> |
1700 | + <div class="table__row"> |
1701 | + <div class="table__header table__column--20" data-ng-click="predicate='name'; reverse = !reverse" |
1702 | + data-ng-class="{sort: predicate === 'name', 'sort-asc': reverse === false, 'sort-desc': reverse === true}">Name</div> |
1703 | + <div class="table__header table__column--5" data-ng-click="predicate='rrtype'; reverse = !reverse" |
1704 | + data-ng-class="{sort: predicate === 'rrtype', 'sort-asc': reverse === false, 'sort-desc': reverse === true}">Type</div> |
1705 | + <div class="table__header table__column--75" data-ng-click="predicate='rrdata'; reverse = !reverse" |
1706 | + data-ng-class="{sort: predicate === 'rrdata', 'sort-asc': reverse === false, 'sort-desc': reverse === true}">Data</div> |
1707 | + </div> |
1708 | + </header> |
1709 | + <main class="table-body"> |
1710 | + <div class="table__row table__row--no-hover" data-ng-repeat="row in domain.rrsets | orderBy:predicate:reverse track by $index"> |
1711 | + <div class="table__data table__column--20"> |
1712 | + <span data-ng-if="row.system_id == null">{$ row.name $}</span> |
1713 | + <span data-ng-if="row.system_id !== null"> |
1714 | + <div data-ng-switch="row.node_type"> |
1715 | + <!-- |
1716 | + XXX lamont 2016-02-10 |
1717 | + Node type is an enum (see node-details.html) and the comment therein. |
1718 | + --> |
1719 | + <span data-ng-switch-when="0"><a href="#/node/{$ row.system_id $}">{$ row.name $}</a></span> |
1720 | + <span data-ng-switch-default>{$ row.name $}</span> |
1721 | + </div> |
1722 | + </span> |
1723 | + </div> |
1724 | + <div class="table__data table__column--5">{$ row.rrtype $}</div> |
1725 | + <div class="table__data table__column--75">{$ row.rrdata $}</div> |
1726 | + </div> |
1727 | + </main> |
1728 | + </div> |
1729 | + </div> |
1730 | + </div> |
1731 | + </section> |
1732 | +</div> |
1733 | |
1734 | === modified file 'src/maasserver/static/partials/domains-list.html' |
1735 | --- src/maasserver/static/partials/domains-list.html 2016-02-04 16:17:08 +0000 |
1736 | +++ src/maasserver/static/partials/domains-list.html 2016-02-18 16:28:50 +0000 |
1737 | @@ -4,11 +4,9 @@ |
1738 | <ng-pluralize data-ng-hide="loading" count="domains.length" |
1739 | when="{'one': '{$ domains.length $} domain in ', 'other': '{$ domains.length $} domains in '}"></ng-pluralize> |
1740 | {$ $parent.site $} MAAS |
1741 | - <span class="page-header__title--identicator" id="bulk-actions"> |
1742 | - <span class="power-status--power" data-ng-show="loading"> |
1743 | - <span class="loader"></span> |
1744 | - Loading... |
1745 | - </span> |
1746 | + <span class="page-header__title--identicator" id="bulk-actions" data-ng-show="loading"> |
1747 | + <span class="loader"></span> |
1748 | + Loading... |
1749 | </span> |
1750 | </h1> |
1751 | </div> |
1752 | @@ -20,19 +18,19 @@ |
1753 | <div class="table"> |
1754 | <header class="table__head"> |
1755 | <div class="table__row"> |
1756 | - <div class="table__header table__column--70" data-ng-click="predicate='name'; reverse = !reverse" data-ng-class="{sort: predicate === 'name', 'sort-asc': reverse === false, 'sort-desc': reverse === true}">Domain</div> |
1757 | - <div class="table__header table__column--15" data-ng-click="predicate='authoritative'; reverse = !reverse" |
1758 | - data-ng-class="{sort: predicate === 'authoritative', 'sort-asc': reverse === false, 'sort-desc': reverse === true}">Authoritative</div> |
1759 | - <div class="table__header table__column--15" data-ng-click="predicate='resource_count'; reverse = !reverse" |
1760 | - data-ng-class="{sort: predicate === 'resource_count', 'sort-asc': reverse === false, 'sort-desc': reverse === true}">Resource Count</div> |
1761 | + <div class="table__header table__column--55" data-ng-click="predicate='name'; reverse = !reverse" data-ng-class="{sort: predicate === 'name', 'sort-asc': reverse === false, 'sort-desc': reverse === true}">Domain</div> |
1762 | + <div class="table__header table__column--15" data-ng-click="predicate='hosts'; reverse = !reverse" data-ng-class="{sort: predicate === 'hosts', 'sort-asc': reverse === false, 'sort-desc': reverse === true}">Hosts</div> |
1763 | + <div class="table__header table__column--15" data-ng-click="predicate='resource_count'; reverse = !reverse" data-ng-class="{sort: predicate === 'resource_count', 'sort-asc': reverse === false, 'sort-desc': reverse === true}">Total Records</div> |
1764 | + <div class="table__header table__column--15" data-ng-click="predicate='authoritative'; reverse = !reverse" data-ng-class="{sort: predicate === 'authoritative', 'sort-asc': reverse === false, 'sort-desc': reverse === true}">Authoritative</div> |
1765 | </div> |
1766 | </header> |
1767 | <main class="table-body"> |
1768 | <div class="table__row table__row--no-hover" |
1769 | data-ng-repeat="row in domains | orderBy:predicate:reverse track by row.id"> |
1770 | - <div class="table__data table__column--70">{$ row.name $}</div> |
1771 | - <div class="table__data table__column--15">{$ row.authoritative $}</div> |
1772 | + <div class="table__data table__column--55"><a href="#/domain/{$ row.id $}">{$ row.name $}</a></div> |
1773 | + <div class="table__data table__column--15">{$ row.hosts $}</div> |
1774 | <div class="table__data table__column--15">{$ row.resource_count $}</div> |
1775 | + <div class="table__data table__column--15">{$ row.authoritative ? "Yes" : "No" $}</div> |
1776 | </div> |
1777 | </main> |
1778 | </div> |
1779 | |
1780 | === modified file 'src/maasserver/static/partials/subnet-details.html' |
1781 | --- src/maasserver/static/partials/subnet-details.html 2015-12-03 01:44:24 +0000 |
1782 | +++ src/maasserver/static/partials/subnet-details.html 2016-02-18 16:28:50 +0000 |
1783 | @@ -54,7 +54,8 @@ |
1784 | <div class="table__header table__column--15">Owner</div> |
1785 | <div class="table__header table__column--20">Usage</div> |
1786 | <div class="table__header table__column--15">Usage type</div> |
1787 | - <div class="table__header table__column--35">Allocation type</div> |
1788 | + <div class="table__header table__column--15">Allocation type</div> |
1789 | + <div class="table__header table__column--20">Fully Qualified Name</div> |
1790 | </div> |
1791 | </header> |
1792 | <section class="table__body"> |
1793 | @@ -70,7 +71,7 @@ |
1794 | <span data-ng-if="ip.node_summary.node_type == 1">{$ ip.node_summary.hostname $}</span> |
1795 | </div> |
1796 | <div class="table__data table__column--15">{$ ip.node_summary.node_type == 0 ? "Node" : "Device" $}</div> |
1797 | - <div class="table__data table__column--35" data-ng-switch="ip.alloc_type"> |
1798 | + <div class="table__data table__column--15" data-ng-switch="ip.alloc_type"> |
1799 | <span data-ng-switch-when="0">Automatic</span> |
1800 | <span data-ng-switch-when="1">Sticky</span> |
1801 | <span data-ng-switch-when="4">User reserved</span> |
1802 | @@ -78,6 +79,10 @@ |
1803 | <span data-ng-switch-when="6">Observed</span> |
1804 | <span data-ng-switch-default>Unknown</span> |
1805 | </div> |
1806 | + <div class="table__data table__column--20"> |
1807 | + <a href="#/node/{$ ip.node_summary.system_id $}" data-ng-if="ip.node_summary.node_type == 0">{$ ip.node_summary.fqdn $}.</a> |
1808 | + <span data-ng-if="ip.node_summary.node_type != 0">{$ ip.node_summary.fqdn $}.</span> |
1809 | + </div> |
1810 | </div> |
1811 | </section> |
1812 | </div> |
1813 | |
1814 | === modified file 'src/maasserver/testing/factory.py' |
1815 | --- src/maasserver/testing/factory.py 2016-02-11 01:01:45 +0000 |
1816 | +++ src/maasserver/testing/factory.py 2016-02-18 16:28:50 +0000 |
1817 | @@ -692,10 +692,16 @@ |
1818 | interface.save() |
1819 | if hostname is not None: |
1820 | if not isinstance(hostname, (tuple, list)): |
1821 | - hostname = (hostname) |
1822 | + hostname = [hostname] |
1823 | for name in hostname: |
1824 | - ipaddress.dnsresource_set.add( |
1825 | - DNSResource.objects.create(name=name)) |
1826 | + if name.find('.') > 0: |
1827 | + name, domain = name.split('.', 1) |
1828 | + domain = Domain.objects.get(name=domain) |
1829 | + else: |
1830 | + domain = None |
1831 | + dnsrr, created = DNSResource.objects.get_or_create( |
1832 | + name=name, domain=domain) |
1833 | + ipaddress.dnsresource_set.add(dnsrr) |
1834 | return reload_object(ipaddress) |
1835 | |
1836 | def make_email(self): |
1837 | |
1838 | === modified file 'src/maasserver/views/combo.py' |
1839 | --- src/maasserver/views/combo.py 2016-02-17 01:05:38 +0000 |
1840 | +++ src/maasserver/views/combo.py 2016-02-18 16:28:50 +0000 |
1841 | @@ -102,6 +102,7 @@ |
1842 | "js/angular/controllers/node_result.js", |
1843 | "js/angular/controllers/node_events.js", |
1844 | "js/angular/controllers/domains_list.js", |
1845 | + "js/angular/controllers/domain_details.js", |
1846 | "js/angular/controllers/networks_list.js", |
1847 | "js/angular/controllers/subnet_details.js", |
1848 | ] |
1849 | |
1850 | === modified file 'src/maasserver/websockets/handlers/domain.py' |
1851 | --- src/maasserver/websockets/handlers/domain.py 2016-02-05 10:57:56 +0000 |
1852 | +++ src/maasserver/websockets/handlers/domain.py 2016-02-18 16:28:50 +0000 |
1853 | @@ -24,10 +24,10 @@ |
1854 | ] |
1855 | |
1856 | def dehydrate(self, domain, data, for_list=False): |
1857 | - ip_addresses, ipcount = domain.render_json_for_related_ips(for_list) |
1858 | - rrsets, rrcount = domain.render_json_for_related_rrdata(for_list) |
1859 | + rrsets = domain.render_json_for_related_rrdata(for_list=for_list) |
1860 | if not for_list: |
1861 | - data["ip_addresses"] = ip_addresses |
1862 | data["rrsets"] = rrsets |
1863 | - data["resource_count"] = ipcount + rrcount |
1864 | + data["hosts"] = len({ |
1865 | + rr['system_id'] for rr in rrsets if rr['system_id'] is not None}) |
1866 | + data["resource_count"] = len(rrsets) |
1867 | return data |
1868 | |
1869 | === modified file 'src/maasserver/websockets/handlers/tests/test_domain.py' |
1870 | --- src/maasserver/websockets/handlers/tests/test_domain.py 2016-02-04 17:59:04 +0000 |
1871 | +++ src/maasserver/websockets/handlers/tests/test_domain.py 2016-02-18 16:28:50 +0000 |
1872 | @@ -14,6 +14,7 @@ |
1873 | from maasserver.testing.testcase import MAASServerTestCase |
1874 | from maasserver.websockets.handlers.domain import DomainHandler |
1875 | from maasserver.websockets.handlers.timestampedmodel import dehydrate_datetime |
1876 | +from netaddr import IPAddress |
1877 | |
1878 | |
1879 | class TestDomainHandler(MAASServerTestCase): |
1880 | @@ -30,32 +31,35 @@ |
1881 | ip_map = StaticIPAddress.objects.get_hostname_ip_mapping(domain) |
1882 | rr_map = DNSData.objects.get_hostname_dnsdata_mapping(domain) |
1883 | domainname_len = len(domain.name) |
1884 | - ip_addresses = [ |
1885 | - { |
1886 | - # strip off the domain name. |
1887 | - 'hostname': hostname[:-domainname_len - 1], |
1888 | - 'system_id': info.system_id, |
1889 | - 'ttl': info.ttl, |
1890 | - 'ips': info.ips} |
1891 | - for hostname, info in ip_map.items() |
1892 | - ] |
1893 | + for name, info in ip_map.items(): |
1894 | + name = name[:-domainname_len - 1] |
1895 | + if info.system_id is not None: |
1896 | + rr_map[name].system_id = info.system_id |
1897 | + for ip in info.ips: |
1898 | + if IPAddress(ip).version == 4: |
1899 | + rr_map[name].rrset.add((info.ttl, 'A', ip)) |
1900 | + else: |
1901 | + rr_map[name].rrset.add((info.ttl, 'AAAA', ip)) |
1902 | rrsets = [ |
1903 | { |
1904 | - 'hostname': hostname, |
1905 | + 'name': hostname, |
1906 | 'system_id': info.system_id, |
1907 | - 'rrsets': info.rrset, |
1908 | + 'node_type': info.node_type, |
1909 | + 'ttl': ttl, |
1910 | + 'rrtype': rrtype, |
1911 | + 'rrdata': rrdata, |
1912 | } |
1913 | for hostname, info in rr_map.items() |
1914 | + for ttl, rrtype, rrdata in info.rrset |
1915 | ] |
1916 | - count = 0 |
1917 | - for record in ip_addresses: |
1918 | - count += len(record['ips']) |
1919 | + data['resource_count'] = len(rrsets) |
1920 | + hosts = set() |
1921 | for record in rrsets: |
1922 | - count += len(record['rrsets']) |
1923 | - data['resource_count'] = count |
1924 | + if record['system_id'] is not None: |
1925 | + hosts.add(record['system_id']) |
1926 | + data['hosts'] = len(hosts) |
1927 | if not for_list: |
1928 | data.update({ |
1929 | - "ip_addresses": ip_addresses, |
1930 | "rrsets": rrsets, |
1931 | }) |
1932 | return data |
1933 | @@ -66,7 +70,7 @@ |
1934 | domain = factory.make_Domain() |
1935 | factory.make_DNSData(domain=domain) |
1936 | factory.make_DNSResource(domain=domain) |
1937 | - self.assertEqual( |
1938 | + self.assertItemsEqual( |
1939 | self.dehydrate_domain(domain), |
1940 | handler.get({"id": domain.id})) |
1941 | |
1942 | |
1943 | === modified file 'src/provisioningserver/dns/tests/test_zoneconfig.py' |
1944 | --- src/provisioningserver/dns/tests/test_zoneconfig.py 2016-02-03 09:11:25 +0000 |
1945 | +++ src/provisioningserver/dns/tests/test_zoneconfig.py 2016-02-18 16:28:50 +0000 |
1946 | @@ -44,7 +44,8 @@ |
1947 | self.ips = ips.copy() |
1948 | |
1949 | def __repr__(self): |
1950 | - return "%s:%s:%s" % (self.system_id, self.ttl, self.ips) |
1951 | + return "HostnameIPMapping(%r, %r, %r, %r)" % ( |
1952 | + self.system_id, self.ttl, self.ips, self.node_type) |
1953 | |
1954 | def __eq__(self, other): |
1955 | return self.__dict__ == other.__dict__ |
1956 | @@ -59,7 +60,8 @@ |
1957 | self.rrset = rrset.copy() |
1958 | |
1959 | def __repr__(self): |
1960 | - return "%s:%s" % (self.system_id, self.rrset) |
1961 | + return "HostnameRRSetMapping(%r, %r, %r)" % ( |
1962 | + self.system_id, self.rrset, self.node_type) |
1963 | |
1964 | def __eq__(self, other): |
1965 | return self.__dict__ == other.__dict__ |
I basically approve, but please check my comments below regarding some mostly-minor nits.
Also, after staring at your HTML and JavaScript long enough, I convinced myself that it was good, but that was after the text morphed into a minotaur and said "ENOUGH! SHIP IT!". And I said "BACK, VILE BEAST! SHOW ME YOUR UNIT TESTS!" ... and it screamed a horrible scream and turned into a pile of dust.
So yeah. Please add a unit test for the new controller. =)
Also, what I think I'm trying to say is, I'd feel more comfortable Blake reviews lines 874 through 1093. ;-)