Merge lp:~lamont/maas/domain-detail into lp:~maas-committers/maas/trunk

Proposed by LaMont Jones
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
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.

To post a comment you must log in.
Revision history for this message
Mike Pontillo (mpontillo) wrote :

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. ;-)

review: Needs Fixing
Revision history for this message
LaMont Jones (lamont) wrote :

Addressed the various.

Revision history for this message
Blake Rouse (blake-rouse) wrote :

Looks almost there. Couple of issues to fix.

review: Approve
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Sorry that was wrong.

review: Needs Fixing
Revision history for this message
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.

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (1023.0 KiB)

The attempt to merge lp:~lamont/maas/domain-detail into lp:maas failed. Below is the output from the failed tests.

Hit:1 http://security.ubuntu.com/ubuntu xenial-security InRelease
Get:2 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial InRelease [95.8 kB]
Hit:3 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates InRelease
Hit:4 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-backports InRelease
Fetched 95.8 kB in 0s (216 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
    --no-install-recommends install apache2 archdetect-deb authbind bind9 bind9utils build-essential bzr-builddeb chromium-browser chromium-chromedriver curl daemontools debhelper dh-apport dh-systemd distro-info dnsutils firefox freeipmi-tools git gjs ipython isc-dhcp-common libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libpq-dev make nodejs-legacy npm postgresql pxelinux python3-apt python3-bson python3-convoy python3-coverage python3-crochet python3-cssselect python3-curtin python3-dev python3-distro-info python3-django python3-django-nose python3-django-piston3 python3-docutils python3-formencode python3-hivex python3-httplib2 python3-jinja2 python3-jsonschema python3-lxml python3-mock python3-netaddr python3-netifaces python3-oauth python3-openssl python3-paramiko python3-petname python3-pexpect python3-psycopg2 python3-pyinotify python3-pyparsing python3-pyvmomi python3-seamicroclient python3-simplestreams python3-sphinx python3-tempita python3-twisted python3-txtftp python3-tz python3-yaml python3-zope.interface python-bson python-crochet python-django python-django-piston python-djorm-ext-pgarray python-formencode python-lxml python-netaddr python-netifaces python-pocket-lint python-psycopg2 python-tempita python-twisted python-yaml socat syslinux-common tgt ubuntu-cloudimage-keyring wget xvfb
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.5.dfsg-12.1ubuntu1).
bind9utils is already the newest version (1:9.9.5.dfsg-12.1ubuntu1).
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.20160115ubuntu2).
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.5.dfsg-12.1ubuntu1).
firefox is already the newest version (44.0.2+build1-0ubuntu1).
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-hotkeys is already the newest version (0~20130707+git2d51e3a9+dfsg-2ubuntu1).
libjs-yui3-full is already the n...

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (34.5 KiB)

The attempt to merge lp:~lamont/maas/domain-detail into lp:maas failed. Below is the output from the failed tests.

Hit:1 http://security.ubuntu.com/ubuntu xenial-security InRelease
Get:2 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial InRelease [95.8 kB]
Hit:3 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates InRelease
Hit:4 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-backports InRelease
Get:5 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/main Sources [1,133 kB]
Get:6 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/universe Sources [7,686 kB]
Get:7 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/multiverse Sources [179 kB]
Get:8 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/main amd64 Packages [1,471 kB]
Get:9 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/main Translation-en [854 kB]
Get:10 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/universe amd64 Packages [7,282 kB]
Get:11 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/universe Translation-en [4,861 kB]
Get:12 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/multiverse amd64 Packages [139 kB]
Get:13 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/multiverse Translation-en [110 kB]
Fetched 23.8 MB in 8s (2,963 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
    --no-install-recommends install apache2 archdetect-deb authbind bind9 bind9utils build-essential bzr-builddeb chromium-browser chromium-chromedriver curl daemontools debhelper dh-apport dh-systemd distro-info dnsutils firefox freeipmi-tools git gjs ipython isc-dhcp-common libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libpq-dev make nodejs-legacy npm postgresql pxelinux python3-apt python3-bson python3-convoy python3-coverage python3-crochet python3-cssselect python3-curtin python3-dev python3-distro-info python3-django python3-django-nose python3-django-piston3 python3-docutils python3-formencode python3-hivex python3-httplib2 python3-jinja2 python3-jsonschema python3-lxml python3-mock python3-netaddr python3-netifaces python3-oauth python3-openssl python3-paramiko python3-petname python3-pexpect python3-psycopg2 python3-pyinotify python3-pyparsing python3-pyvmomi python3-seamicroclient python3-simplestreams python3-sphinx python3-tempita python3-twisted python3-txtftp python3-tz python3-yaml python3-zope.interface python-bson python-crochet python-django python-django-piston python-djorm-ext-pgarray python-formencode python-lxml python-netaddr python-netifaces python-pocket-lint python-psycopg2 python-tempita python-twisted python-yaml socat syslinux-common tgt ubuntu-cloudimage-keyring wget xvfb
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.5.dfsg-12.1ubuntu1).
bind9utils is already the newest version (1:9.9.5.df...

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (1.4 MiB)

The attempt to merge lp:~lamont/maas/domain-detail into lp:maas failed. Below is the output from the failed tests.

Hit:1 http://security.ubuntu.com/ubuntu xenial-security InRelease
Get:2 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial InRelease [95.8 kB]
Hit:3 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates InRelease
Hit:4 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-backports InRelease
Get:5 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/main Sources [1,133 kB]
Get:6 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/universe Sources [7,687 kB]
Get:7 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/main amd64 Packages [1,472 kB]
Get:8 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/main Translation-en [854 kB]
Get:9 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/universe amd64 Packages [7,282 kB]
Get:10 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/universe Translation-en [4,863 kB]
Fetched 23.4 MB in 6s (3,495 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
    --no-install-recommends install apache2 archdetect-deb authbind bind9 bind9utils build-essential bzr-builddeb chromium-browser chromium-chromedriver curl daemontools debhelper dh-apport dh-systemd distro-info dnsutils firefox freeipmi-tools git gjs ipython isc-dhcp-common libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libpq-dev make nodejs-legacy npm postgresql pxelinux python3-apt python3-bson python3-convoy python3-coverage python3-crochet python3-cssselect python3-curtin python3-dev python3-distro-info python3-django python3-django-nose python3-django-piston3 python3-docutils python3-formencode python3-hivex python3-httplib2 python3-jinja2 python3-jsonschema python3-lxml python3-mock python3-netaddr python3-netifaces python3-oauth python3-openssl python3-paramiko python3-petname python3-pexpect python3-psycopg2 python3-pyinotify python3-pyparsing python3-pyvmomi python3-seamicroclient python3-simplestreams python3-sphinx python3-tempita python3-twisted python3-txtftp python3-tz python3-yaml python3-zope.interface python-bson python-crochet python-django python-django-piston python-djorm-ext-pgarray python-formencode python-lxml python-netaddr python-netifaces python-pocket-lint python-psycopg2 python-tempita python-twisted python-yaml socat syslinux-common tgt ubuntu-cloudimage-keyring wget xvfb
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.5.dfsg-12.1ubuntu1).
bind9utils is already the newest version (1:9.9.5.dfsg-12.1ubuntu1).
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.20160115ubuntu2).
dh-apport is already the newest version (2.20-0ubuntu3).
dh-systemd is already the newest version (1.28ubuntu2).
distr...

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (1.0 MiB)

The attempt to merge lp:~lamont/maas/domain-detail into lp:maas failed. Below is the output from the failed tests.

Hit:1 http://security.ubuntu.com/ubuntu xenial-security InRelease
Get:2 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial InRelease [95.8 kB]
Hit:3 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates InRelease
Hit:4 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-backports InRelease
Get:5 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/universe Sources [7,687 kB]
Get:6 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/universe amd64 Packages [7,281 kB]
Fetched 15.1 MB in 5s (2,756 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
    --no-install-recommends install apache2 archdetect-deb authbind bind9 bind9utils build-essential bzr-builddeb chromium-browser chromium-chromedriver curl daemontools debhelper dh-apport dh-systemd distro-info dnsutils firefox freeipmi-tools git gjs ipython isc-dhcp-common libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libpq-dev make nodejs-legacy npm postgresql pxelinux python3-apt python3-bson python3-convoy python3-coverage python3-crochet python3-cssselect python3-curtin python3-dev python3-distro-info python3-django python3-django-nose python3-django-piston3 python3-docutils python3-formencode python3-hivex python3-httplib2 python3-jinja2 python3-jsonschema python3-lxml python3-mock python3-netaddr python3-netifaces python3-oauth python3-openssl python3-paramiko python3-petname python3-pexpect python3-psycopg2 python3-pyinotify python3-pyparsing python3-pyvmomi python3-seamicroclient python3-simplestreams python3-sphinx python3-tempita python3-twisted python3-txtftp python3-tz python3-yaml python3-zope.interface python-bson python-crochet python-django python-django-piston python-djorm-ext-pgarray python-formencode python-lxml python-netaddr python-netifaces python-pocket-lint python-psycopg2 python-tempita python-twisted python-yaml socat syslinux-common tgt ubuntu-cloudimage-keyring wget xvfb
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.5.dfsg-12.1ubuntu1).
bind9utils is already the newest version (1:9.9.5.dfsg-12.1ubuntu1).
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.20160115ubuntu2).
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.5.dfsg-12.1ubuntu1).
firefox is already the newest version (44.0.2+build1-0ubuntu1).
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...

Revision history for this message
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.

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (551.9 KiB)

The attempt to merge lp:~lamont/maas/domain-detail into lp:maas failed. Below is the output from the failed tests.

Hit:1 http://security.ubuntu.com/ubuntu xenial-security InRelease
Get:2 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial InRelease [95.8 kB]
Hit:3 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates InRelease
Hit:4 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-backports InRelease
Get:5 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/main Sources [1,135 kB]
Get:6 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/universe Sources [7,689 kB]
Get:7 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/main amd64 Packages [1,482 kB]
Get:8 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/main Translation-en [861 kB]
Get:9 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/universe amd64 Packages [7,286 kB]
Get:10 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/universe Translation-en [4,863 kB]
Fetched 23.4 MB in 7s (3,059 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
    --no-install-recommends install apache2 archdetect-deb authbind bind9 bind9utils build-essential bzr-builddeb chromium-browser chromium-chromedriver curl daemontools debhelper dh-apport dh-systemd distro-info dnsutils firefox freeipmi-tools git gjs ipython isc-dhcp-common libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libpq-dev make nodejs-legacy npm postgresql pxelinux python3-apt python3-bson python3-convoy python3-coverage python3-crochet python3-cssselect python3-curtin python3-dev python3-distro-info python3-django python3-django-nose python3-django-piston3 python3-docutils python3-formencode python3-hivex python3-httplib2 python3-jinja2 python3-jsonschema python3-lxml python3-mock python3-netaddr python3-netifaces python3-oauth python3-openssl python3-paramiko python3-petname python3-pexpect python3-psycopg2 python3-pyinotify python3-pyparsing python3-pyvmomi python3-seamicroclient python3-simplestreams python3-sphinx python3-tempita python3-twisted python3-txtftp python3-tz python3-yaml python3-zope.interface python-bson python-crochet python-django python-django-piston python-djorm-ext-pgarray python-formencode python-lxml python-netaddr python-netifaces python-pocket-lint python-psycopg2 python-tempita python-twisted python-yaml socat syslinux-common tgt ubuntu-cloudimage-keyring wget xvfb
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.5.dfsg-12.1ubuntu1).
bind9utils is already the newest version (1:9.9.5.dfsg-12.1ubuntu1).
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.20160115ubuntu2).
dh-apport is already the newest version (2.20-0ubuntu3).
dh-systemd is already the newest version (1.28ubuntu2).
distr...

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (1004.7 KiB)

The attempt to merge lp:~lamont/maas/domain-detail into lp:maas failed. Below is the output from the failed tests.

Hit:1 http://security.ubuntu.com/ubuntu xenial-security InRelease
Get:2 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial InRelease [95.8 kB]
Hit:3 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates InRelease
Hit:4 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-backports InRelease
Get:5 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/universe Sources [7,689 kB]
Get:6 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/main amd64 Packages [1,482 kB]
Get:7 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/universe amd64 Packages [7,286 kB]
Get:8 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/universe Translation-en [4,863 kB]
Fetched 21.4 MB in 6s (3,285 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
    --no-install-recommends install apache2 archdetect-deb authbind bind9 bind9utils build-essential bzr-builddeb chromium-browser chromium-chromedriver curl daemontools debhelper dh-apport dh-systemd distro-info dnsutils firefox freeipmi-tools git gjs ipython isc-dhcp-common libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libpq-dev make nodejs-legacy npm postgresql pxelinux python3-apt python3-bson python3-convoy python3-coverage python3-crochet python3-cssselect python3-curtin python3-dev python3-distro-info python3-django python3-django-nose python3-django-piston3 python3-docutils python3-formencode python3-hivex python3-httplib2 python3-jinja2 python3-jsonschema python3-lxml python3-mock python3-netaddr python3-netifaces python3-oauth python3-openssl python3-paramiko python3-petname python3-pexpect python3-psycopg2 python3-pyinotify python3-pyparsing python3-pyvmomi python3-seamicroclient python3-simplestreams python3-sphinx python3-tempita python3-twisted python3-txtftp python3-tz python3-yaml python3-zope.interface python-bson python-crochet python-django python-django-piston python-djorm-ext-pgarray python-formencode python-lxml python-netaddr python-netifaces python-pocket-lint python-psycopg2 python-tempita python-twisted python-yaml socat syslinux-common tgt ubuntu-cloudimage-keyring wget xvfb
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.5.dfsg-12.1ubuntu1).
bind9utils is already the newest version (1:9.9.5.dfsg-12.1ubuntu1).
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.20160115ubuntu2).
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.5.dfsg-12.1ubuntu1).
firefox is already the newest version (44.0.2+build1-0ubuntu1).
freeipmi-too...

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (1.0 MiB)

The attempt to merge lp:~lamont/maas/domain-detail into lp:maas failed. Below is the output from the failed tests.

Hit:1 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial InRelease
Hit:2 http://security.ubuntu.com/ubuntu xenial-security InRelease
Hit:3 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates InRelease
Hit:4 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-backports InRelease
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
    --no-install-recommends install apache2 archdetect-deb authbind bind9 bind9utils build-essential bzr-builddeb chromium-browser chromium-chromedriver curl daemontools debhelper dh-apport dh-systemd distro-info dnsutils firefox freeipmi-tools git gjs ipython isc-dhcp-common libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libpq-dev make nodejs-legacy npm postgresql pxelinux python3-apt python3-bson python3-convoy python3-coverage python3-crochet python3-cssselect python3-curtin python3-dev python3-distro-info python3-django python3-django-nose python3-django-piston3 python3-docutils python3-formencode python3-hivex python3-httplib2 python3-jinja2 python3-jsonschema python3-lxml python3-mock python3-netaddr python3-netifaces python3-oauth python3-openssl python3-paramiko python3-petname python3-pexpect python3-psycopg2 python3-pyinotify python3-pyparsing python3-pyvmomi python3-seamicroclient python3-simplestreams python3-sphinx python3-tempita python3-twisted python3-txtftp python3-tz python3-yaml python3-zope.interface python-bson python-crochet python-django python-django-piston python-djorm-ext-pgarray python-formencode python-lxml python-netaddr python-netifaces python-pocket-lint python-psycopg2 python-tempita python-twisted python-yaml socat syslinux-common tgt ubuntu-cloudimage-keyring wget xvfb
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.5.dfsg-12.1ubuntu1).
bind9utils is already the newest version (1:9.9.5.dfsg-12.1ubuntu1).
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.20160115ubuntu2).
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.5.dfsg-12.1ubuntu1).
firefox is already the newest version (44.0.2+build1-0ubuntu1).
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-hotkeys is already the newest version (0~20130707+git2d51e3a9+dfsg-2ubuntu1).
libjs-yui3-full is already the newest version (3.5.1-1ubuntu3).
libjs-yui3-...

lp:~lamont/maas/domain-detail updated
4668. By LaMont Jones

Use a bigger random space for the test, so that we collide and fail less often.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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__