Merge ~ltrager/maas:release_auto_ip_on_power_off into maas:master

Proposed by Lee Trager
Status: Merged
Approved by: Lee Trager
Approved revision: de74f32fff29c1d05368ef99806464956346ea8a
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~ltrager/maas:release_auto_ip_on_power_off
Merge into: maas:master
Diff against target: 192 lines (+134/-4)
3 files modified
src/maasserver/models/signals/nodes.py (+29/-2)
src/maasserver/models/signals/tests/test_nodes.py (+101/-1)
src/maasserver/models/tests/test_node.py (+4/-1)
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
MAAS Lander Needs Fixing
Review via email: mp+371380@code.launchpad.net

Commit message

Release AUTOIP addresses when powered off in a non-use status

Commissioning and testing now require AUTOIP addresses to be assigned to
allow for network validation testing. The signal to end these statuses
comes before the machine is actually turned off. Further users may keep
the machine on after commissioning/testing to debug script issues. To
ensure that AUTOIPs are released MAAS will now release any assigned
AUTOIP if the machine isn't currently in a status that needs an IP and
is off.

To post a comment you must log in.
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b release_auto_ip_on_power_off lp:~ltrager/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/6186/console
COMMIT: 979449786d721e16c0e607e6744933a63efcae87

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

Looks good.

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :
de74f32... by Lee Trager

Fix broken test

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/models/signals/nodes.py b/src/maasserver/models/signals/nodes.py
2index c6b4a33..a1d2f06 100644
3--- a/src/maasserver/models/signals/nodes.py
4+++ b/src/maasserver/models/signals/nodes.py
5@@ -1,4 +1,4 @@
6-# Copyright 2015-2017 Canonical Ltd. This software is licensed under the
7+# Copyright 2015-2019 Canonical Ltd. This software is licensed under the
8 # GNU Affero General Public License version 3 (see the file LICENSE).
9
10 """Respond to node changes."""
11@@ -13,7 +13,10 @@ from django.db.models.signals import (
12 pre_delete,
13 pre_save,
14 )
15-from maasserver.enum import NODE_STATUS
16+from maasserver.enum import (
17+ NODE_STATUS,
18+ POWER_STATE,
19+)
20 from maasserver.models import (
21 Controller,
22 Device,
23@@ -136,5 +139,29 @@ for klass in NODE_CLASSES:
24 sender=klass)
25
26
27+def release_auto_ips(node, old_values, deleted=False):
28+ """Release auto assigned IPs once the machine is off and ready."""
29+ # Only machines use AUTO_IPs.
30+ if not node.is_machine:
31+ return
32+ # Commissioning and testing may acquire an AUTO_IP for network testing.
33+ # Users may keep the machine on after commissioning/testing to debug
34+ # issues where the assigned IP is still in use. Wait till the machine
35+ # is off and not in a status which will have an IP in use.
36+ if (node.power_state == POWER_STATE.OFF and node.status not in (
37+ NODE_STATUS.COMMISSIONING,
38+ NODE_STATUS.DEPLOYED,
39+ NODE_STATUS.DEPLOYING,
40+ NODE_STATUS.DISK_ERASING,
41+ NODE_STATUS.RESCUE_MODE,
42+ NODE_STATUS.ENTERING_RESCUE_MODE,
43+ NODE_STATUS.TESTING)):
44+ node.release_interface_config()
45+
46+
47+for klass in [Node, Machine]:
48+ signals.watch_fields(release_auto_ips, klass, ['power_state'])
49+
50+
51 # Enable all signals by default.
52 signals.enable()
53diff --git a/src/maasserver/models/signals/tests/test_nodes.py b/src/maasserver/models/signals/tests/test_nodes.py
54index 5e8ab15..d909b4a 100644
55--- a/src/maasserver/models/signals/tests/test_nodes.py
56+++ b/src/maasserver/models/signals/tests/test_nodes.py
57@@ -1,4 +1,4 @@
58-# Copyright 2015-2017 Canonical Ltd. This software is licensed under the
59+# Copyright 2015-2019 Canonical Ltd. This software is licensed under the
60 # GNU Affero General Public License version 3 (see the file LICENSE).
61
62 """Test the behaviour of node signals."""
63@@ -8,9 +8,17 @@ __all__ = []
64 import random
65
66 from maasserver.enum import (
67+ IPADDRESS_TYPE,
68 NODE_STATUS,
69 NODE_STATUS_CHOICES,
70 NODE_TYPE,
71+ NODE_TYPE_CHOICES,
72+ POWER_STATE,
73+ POWER_STATE_CHOICES,
74+)
75+from maasserver.models import (
76+ Node,
77+ StaticIPAddress,
78 )
79 from maasserver.models.service import (
80 RACK_SERVICES,
81@@ -212,3 +220,95 @@ class TestNodeCreateServices(MAASServerTestCase):
82 self.assertThat(
83 {service.name for service in services},
84 Equals(REGION_SERVICES))
85+
86+
87+class TestNodeReleasesAutoIPs(MAASServerTestCase):
88+ """Test that auto ips are released when node power is off."""
89+
90+ def __init__(self, *args, **kwargs):
91+ self.reserved_statuses = [
92+ NODE_STATUS.COMMISSIONING,
93+ NODE_STATUS.DEPLOYED,
94+ NODE_STATUS.DEPLOYING,
95+ NODE_STATUS.DISK_ERASING,
96+ NODE_STATUS.RESCUE_MODE,
97+ NODE_STATUS.ENTERING_RESCUE_MODE,
98+ NODE_STATUS.TESTING,
99+ ]
100+ self.scenarios = [
101+ (status_label, {'status': status})
102+ for status, status_label in NODE_STATUS_CHOICES
103+ if status not in self.reserved_statuses
104+ ]
105+ super().__init__(*args, **kwargs)
106+
107+ def test__releases_interface_config_when_turned_off(self):
108+ machine = factory.make_Machine_with_Interface_on_Subnet(
109+ status=random.choice(self.reserved_statuses),
110+ power_state=POWER_STATE.ON)
111+ for interface in machine.interface_set.all():
112+ interface.claim_auto_ips()
113+
114+ # Hack to get around node transition model
115+ Node.objects.filter(id=machine.id).update(status=self.status)
116+ machine = reload_object(machine)
117+ machine.power_state = POWER_STATE.OFF
118+ machine.save()
119+
120+ for ip in StaticIPAddress.objects.filter(
121+ interface__node=machine, alloc_type=IPADDRESS_TYPE.AUTO):
122+ self.assertIsNone(ip.ip)
123+
124+ def test__does_nothing_if_not_off(self):
125+ machine = factory.make_Machine_with_Interface_on_Subnet(
126+ status=random.choice(self.reserved_statuses),
127+ power_state=POWER_STATE.ON)
128+ for interface in machine.interface_set.all():
129+ interface.claim_auto_ips()
130+
131+ # Hack to get around node transition model
132+ Node.objects.filter(id=machine.id).update(status=self.status)
133+ machine = reload_object(machine)
134+ machine.power_state = factory.pick_choice(
135+ POWER_STATE_CHOICES, but_not=[POWER_STATE.OFF])
136+ machine.save()
137+
138+ for ip in StaticIPAddress.objects.filter(
139+ interface__node=machine, alloc_type=IPADDRESS_TYPE.AUTO):
140+ self.assertIsNotNone(ip.ip)
141+
142+ def test__does_nothing_if_reserved_status(self):
143+ machine = factory.make_Machine_with_Interface_on_Subnet(
144+ status=self.status, power_state=POWER_STATE.ON)
145+ for interface in machine.interface_set.all():
146+ interface.claim_auto_ips()
147+
148+ # Hack to get around node transition model
149+ Node.objects.filter(id=machine.id).update(
150+ status=random.choice(self.reserved_statuses))
151+ machine = reload_object(machine)
152+ machine.power_state = POWER_STATE.OFF
153+ machine.save()
154+
155+ for ip in StaticIPAddress.objects.filter(
156+ interface__node=machine, alloc_type=IPADDRESS_TYPE.AUTO):
157+ self.assertIsNotNone(ip.ip)
158+
159+ def test__does_nothing_if_not_machine(self):
160+ node = factory.make_Node_with_Interface_on_Subnet(
161+ node_type=factory.pick_choice(
162+ NODE_TYPE_CHOICES, but_not=[NODE_TYPE.MACHINE]),
163+ status=random.choice(self.reserved_statuses),
164+ power_state=POWER_STATE.ON)
165+ for interface in node.interface_set.all():
166+ interface.claim_auto_ips()
167+
168+ # Hack to get around node transition model
169+ Node.objects.filter(id=node.id).update(status=self.status)
170+ node = reload_object(node)
171+ node.power_state = POWER_STATE.OFF
172+ node.save()
173+
174+ for ip in StaticIPAddress.objects.filter(
175+ interface__node=node, alloc_type=IPADDRESS_TYPE.AUTO):
176+ self.assertIsNotNone(ip.ip)
177diff --git a/src/maasserver/models/tests/test_node.py b/src/maasserver/models/tests/test_node.py
178index f428cc4..92d391f 100644
179--- a/src/maasserver/models/tests/test_node.py
180+++ b/src/maasserver/models/tests/test_node.py
181@@ -3868,7 +3868,10 @@ class TestNode(MAASServerTestCase):
182 release = self.patch_autospec(node, 'release_interface_config')
183 self.patch(Node, '_clear_status_expires')
184 node.update_power_state(POWER_STATE.OFF)
185- self.assertThat(release, MockCalledOnceWith())
186+ # release_interface_config() is called once by update_power_state and
187+ # a second time by the release_auto_ips() signal. Whichever runs
188+ # second is a noop but is needed for commissioning/testing.
189+ self.assertThat(release, MockCallsMatch(call(), call()))
190
191 def test_update_power_state_doesnt_release_interface_config_if_on(self):
192 node = factory.make_Node(

Subscribers

People subscribed via source and target branches