Merge lp:~mpontillo/maas/discovered-ip-address-api-fix--bug-1686732 into lp:~maas-committers/maas/trunk

Proposed by Mike Pontillo
Status: Merged
Approved by: Mike Pontillo
Approved revision: no longer in the source branch.
Merged at revision: 6036
Proposed branch: lp:~mpontillo/maas/discovered-ip-address-api-fix--bug-1686732
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 106 lines (+43/-8)
2 files modified
src/maasserver/api/ip_addresses.py (+17/-7)
src/maasserver/api/tests/test_ipaddresses.py (+26/-1)
To merge this branch: bzr merge lp:~mpontillo/maas/discovered-ip-address-api-fix--bug-1686732
Reviewer Review Type Date Requested Status
Lee Trager (community) Approve
Review via email: mp+323355@code.launchpad.net

Commit message

Allow releasing IP addresses of type DISCOVERED using the API.

To post a comment you must log in.
Revision history for this message
Lee Trager (ltrager) wrote :

Why not allow the force option to release any type of IP address?

review: Needs Information
Revision history for this message
Mike Pontillo (mpontillo) wrote :

The IP address release code expects to find a single IP address. Since discovered addresses can exist in the database at the same time as non-Discovered addresses, we need a way to ensure that we only ever pick one StaticIPAddress to delete.

Revision history for this message
Mike Pontillo (mpontillo) wrote :

... the alternatives would be:

(1) user intends to delete a static IP, but deletes both the static IP and the discovered address

(2) user intends to delete just the discovered address, but deletes both the discovered address and the static address.

Revision history for this message
Lee Trager (ltrager) wrote :

Thanks for the explanation.

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

The attempt to merge lp:~mpontillo/maas/discovered-ip-address-api-fix--bug-1686732 into lp:maas failed. Below is the output from the failed tests.

Get:1 http://security.ubuntu.com/ubuntu xenial-security InRelease [102 kB]
Hit:2 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial InRelease
Get:3 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates InRelease [102 kB]
Get:4 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-backports InRelease [102 kB]
Get:5 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates/main amd64 Packages [527 kB]
Get:6 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates/universe amd64 Packages [460 kB]
Fetched 1,294 kB in 0s (2,236 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
    --no-install-recommends install apache2 archdetect-deb authbind avahi-utils bash bind9 bind9utils build-essential bzr 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 isc-dhcp-server libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libnss-wrapper libpq-dev make nodejs-legacy npm postgresql psmisc pxelinux python3-all python3-apt python3-attr python3-bson python3-convoy python3-crochet python3-cssselect python3-curtin python3-dev python3-distro-info python3-django python3-django-nose python3-django-piston3 python3-dnspython python3-docutils python3-formencode python3-hivex python3-httplib2 python3-jinja2 python3-jsonschema python3-lxml python3-netaddr python3-netifaces python3-novaclient python3-oauth python3-oauthlib python3-openssl python3-paramiko python3-petname python3-pexpect python3-psycopg2 python3-pyinotify python3-pyparsing python3-pyvmomi python3-requests python3-seamicroclient python3-setuptools 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-simplejson python-tempita python-twisted python-yaml socat syslinux-common tgt ubuntu-cloudimage-keyring wget xvfb
Reading package lists...
Building dependency tree...
Reading state information...
authbind is already the newest version (2.1.1+nmu1).
avahi-utils is already the newest version (0.6.32~rc+dfsg-1ubuntu2).
build-essential is already the newest version (12.1ubuntu2).
debhelper is already the newest version (9.20160115ubuntu3).
distro-info is already the newest version (0.14build1).
git is already the newest version (1:2.7.4-0ubuntu1).
libjs-angularjs is already the newest version (1.2.28-1ubuntu2).
libjs-jquery is already the newest version (1.11.3+dfsg-4).
libjs-yui3-full is already the newest version (3.5.1-1ubuntu3).
libjs-yui3-min is already the newest version (3.5.1-1ubuntu3).
make is already the newest version (4.1-6).
postgresql is already the newest version (9.5+173).
psmisc is already the newest version (22.21-2.1build1).
pxelinux is a...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api/ip_addresses.py'
2--- src/maasserver/api/ip_addresses.py 2016-10-12 17:50:40 +0000
3+++ src/maasserver/api/ip_addresses.py 2017-05-05 04:10:34 +0000
4@@ -231,11 +231,18 @@
5 caution.
6 :type force: bool
7
8+ :param discovered: If True, allows a MAAS administrator to release
9+ a discovered address. Only valid if 'force' is specified. If not
10+ specified, MAAS will attempt to release any type of address except
11+ for discovered addresses.
12+
13 Returns 404 if the provided IP address is not found.
14 """
15 ip = get_mandatory_param(request.POST, "ip")
16 force = get_optional_param(
17 request.POST, 'force', default=False, validator=StringBool)
18+ discovered = get_optional_param(
19+ request.POST, 'discovered', default=False, validator=StringBool)
20
21 if force is True and not request.user.is_superuser:
22 return HttpResponseForbidden(
23@@ -248,21 +255,24 @@
24 raise MAASAPIValidationError(form.errors)
25
26 if force:
27- query_args = dict(ip=ip)
28+ query = StaticIPAddress.objects.filter(ip=ip)
29+ if discovered:
30+ query = query.filter(alloc_type=IPADDRESS_TYPE.DISCOVERED)
31+ else:
32+ query = query.exclude(alloc_type=IPADDRESS_TYPE.DISCOVERED)
33 else:
34- query_args = dict(
35- alloc_type=IPADDRESS_TYPE.USER_RESERVED, ip=ip,
36+ query = StaticIPAddress.objects.filter(
37+ ip=ip, alloc_type=IPADDRESS_TYPE.USER_RESERVED,
38 user=request.user)
39
40 # Get the reserved IP address, or raise bad request.
41- try:
42- ip_address = StaticIPAddress.objects.get(**query_args)
43- except StaticIPAddress.DoesNotExist:
44+ ip_address = query.first()
45+ if ip_address is None:
46 if force:
47 error = "IP address %s does not exist."
48 else:
49 error = (
50- "IP address %s does not exist, is not a user-reserved "
51+ "IP address %s does not exist, is not the correct type of "
52 "address, or does not belong to the requesting user.\n"
53 "If you are sure you want to release this address, use "
54 "force=true as a MAAS administrator.")
55
56=== modified file 'src/maasserver/api/tests/test_ipaddresses.py'
57--- src/maasserver/api/tests/test_ipaddresses.py 2017-01-28 00:51:47 +0000
58+++ src/maasserver/api/tests/test_ipaddresses.py 2017-05-05 04:10:34 +0000
59@@ -240,7 +240,7 @@
60 else:
61 return status
62
63- def post_release_request(self, ip, mac=None):
64+ def post_release_request(self, ip, mac=None, discovered=None):
65 params = {
66 'op': 'release',
67 'ip': ip,
68@@ -249,6 +249,8 @@
69 params["mac"] = mac
70 if self.force is not None:
71 params["force"] = str(self.force)
72+ if discovered is not None:
73+ params["discovered"] = str(discovered)
74 return self.client.post(reverse('ipaddresses_handler'), params)
75
76 @transactional
77@@ -310,6 +312,29 @@
78 Equals(""))
79
80 @transactional
81+ def test_POST_release_allows_admin_to_release_discovered_ip(self):
82+ # Make an "orphaned" IP address, like the one in bug #1630034.
83+ static_ip = factory.make_StaticIPAddress(
84+ alloc_type=IPADDRESS_TYPE.DISCOVERED)
85+ response = self.post_release_request(
86+ str(static_ip.ip), discovered=True)
87+ if self.expect_forbidden:
88+ self.assertEqual(http.client.FORBIDDEN, response.status_code)
89+ self.assertEqual(
90+ "Force-releasing an IP address requires admin privileges.",
91+ response.content.decode("utf-8"))
92+ elif not self.force:
93+ self.assertEqual(http.client.BAD_REQUEST, response.status_code)
94+ self.assertThat(
95+ response.content.decode("utf-8"),
96+ DocTestMatches("...does not belong to the requesting user..."))
97+ else:
98+ self.assertEqual(http.client.NO_CONTENT, response.status_code)
99+ self.assertThat(
100+ response.content.decode("utf-8"),
101+ Equals(""))
102+
103+ @transactional
104 def test_POST_release_deallocates_address(self):
105 ipaddress = factory.make_StaticIPAddress(
106 alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=self.user)