Merge lp:~julian-edwards/maas/allocate-ip-on-start-2 into lp:~maas-committers/maas/trunk
- allocate-ip-on-start-2
- Merge into trunk
Proposed by
Julian Edwards
Status: | Superseded | ||||
---|---|---|---|---|---|
Proposed branch: | lp:~julian-edwards/maas/allocate-ip-on-start-2 | ||||
Merge into: | lp:~maas-committers/maas/trunk | ||||
Diff against target: |
575 lines (+300/-45) 7 files modified
src/maasserver/models/__init__.py (+1/-1) src/maasserver/models/macaddress.py (+4/-1) src/maasserver/models/node.py (+105/-11) src/maasserver/models/staticipaddress.py (+1/-2) src/maasserver/models/tests/test_macaddress.py (+3/-15) src/maasserver/models/tests/test_node.py (+165/-13) src/maasserver/testing/factory.py (+21/-2) |
||||
To merge this branch: | bzr merge lp:~julian-edwards/maas/allocate-ip-on-start-2 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
MAAS Maintainers | Pending | ||
Review via email:
|
This proposal has been superseded by a proposal from 2014-06-11.
Commit message
Description of the change
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'src/maasserver/models/__init__.py' | |||
2 | --- src/maasserver/models/__init__.py 2014-06-10 14:45:14 +0000 | |||
3 | +++ src/maasserver/models/__init__.py 2014-06-11 05:52:49 +0000 | |||
4 | @@ -51,7 +51,6 @@ | |||
5 | 51 | from maasserver.models.dhcplease import DHCPLease | 51 | from maasserver.models.dhcplease import DHCPLease |
6 | 52 | from maasserver.models.downloadprogress import DownloadProgress | 52 | from maasserver.models.downloadprogress import DownloadProgress |
7 | 53 | from maasserver.models.filestorage import FileStorage | 53 | from maasserver.models.filestorage import FileStorage |
8 | 54 | from maasserver.models.staticipaddress import StaticIPAddress | ||
9 | 55 | from maasserver.models.macaddress import MACAddress | 54 | from maasserver.models.macaddress import MACAddress |
10 | 56 | from maasserver.models.macipaddresslink import MACStaticIPAddressLink | 55 | from maasserver.models.macipaddresslink import MACStaticIPAddressLink |
11 | 57 | from maasserver.models.network import Network | 56 | from maasserver.models.network import Network |
12 | @@ -59,6 +58,7 @@ | |||
13 | 59 | from maasserver.models.nodegroup import NodeGroup | 58 | from maasserver.models.nodegroup import NodeGroup |
14 | 60 | from maasserver.models.nodegroupinterface import NodeGroupInterface | 59 | from maasserver.models.nodegroupinterface import NodeGroupInterface |
15 | 61 | from maasserver.models.sshkey import SSHKey | 60 | from maasserver.models.sshkey import SSHKey |
16 | 61 | from maasserver.models.staticipaddress import StaticIPAddress | ||
17 | 62 | from maasserver.models.tag import Tag | 62 | from maasserver.models.tag import Tag |
18 | 63 | from maasserver.models.user import create_user | 63 | from maasserver.models.user import create_user |
19 | 64 | from maasserver.models.userprofile import UserProfile | 64 | from maasserver.models.userprofile import UserProfile |
20 | 65 | 65 | ||
21 | === modified file 'src/maasserver/models/macaddress.py' | |||
22 | --- src/maasserver/models/macaddress.py 2014-06-10 14:45:14 +0000 | |||
23 | +++ src/maasserver/models/macaddress.py 2014-06-11 05:52:49 +0000 | |||
24 | @@ -90,7 +90,10 @@ | |||
25 | 90 | def claim_static_ip(self, alloc_type=IPADDRESS_TYPE.AUTO): | 90 | def claim_static_ip(self, alloc_type=IPADDRESS_TYPE.AUTO): |
26 | 91 | """Assign a static IP to this MAC. | 91 | """Assign a static IP to this MAC. |
27 | 92 | 92 | ||
29 | 93 | TODO: Also set a host DHCP entry. | 93 | It is the caller's responsibility to create a celery Task that will |
30 | 94 | write the dhcp host. It is not done here because celery doesn't | ||
31 | 95 | guarantee job ordering, and if the host entry is written after | ||
32 | 96 | the host boots it is too late. | ||
33 | 94 | 97 | ||
34 | 95 | :param alloc_type: See :class:`StaticIPAddress`.alloc_type. | 98 | :param alloc_type: See :class:`StaticIPAddress`.alloc_type. |
35 | 96 | :return: A :class:`StaticIPAddress` object. Returns None if | 99 | :return: A :class:`StaticIPAddress` object. Returns None if |
36 | 97 | 100 | ||
37 | === modified file 'src/maasserver/models/node.py' | |||
38 | --- src/maasserver/models/node.py 2014-06-10 15:26:47 +0000 | |||
39 | +++ src/maasserver/models/node.py 2014-06-11 05:52:49 +0000 | |||
40 | @@ -28,6 +28,7 @@ | |||
41 | 28 | from string import whitespace | 28 | from string import whitespace |
42 | 29 | from uuid import uuid1 | 29 | from uuid import uuid1 |
43 | 30 | 30 | ||
44 | 31 | import celery | ||
45 | 31 | from django.contrib.auth.models import User | 32 | from django.contrib.auth.models import User |
46 | 32 | from django.core.exceptions import ( | 33 | from django.core.exceptions import ( |
47 | 33 | PermissionDenied, | 34 | PermissionDenied, |
48 | @@ -54,16 +55,20 @@ | |||
49 | 54 | NODE_STATUS, | 55 | NODE_STATUS, |
50 | 55 | NODE_STATUS_CHOICES, | 56 | NODE_STATUS_CHOICES, |
51 | 56 | NODE_STATUS_CHOICES_DICT, | 57 | NODE_STATUS_CHOICES_DICT, |
52 | 58 | NODEGROUPINTERFACE_MANAGEMENT, | ||
53 | 57 | ) | 59 | ) |
54 | 58 | from maasserver.exceptions import NodeStateViolation | 60 | from maasserver.exceptions import NodeStateViolation |
55 | 59 | from maasserver.fields import ( | 61 | from maasserver.fields import ( |
56 | 60 | JSONObjectField, | 62 | JSONObjectField, |
57 | 61 | MAC, | 63 | MAC, |
58 | 62 | ) | 64 | ) |
59 | 63 | from maasserver.models import StaticIPAddress | ||
60 | 64 | from maasserver.models.cleansave import CleanSave | 65 | from maasserver.models.cleansave import CleanSave |
61 | 65 | from maasserver.models.config import Config | 66 | from maasserver.models.config import Config |
62 | 66 | from maasserver.models.dhcplease import DHCPLease | 67 | from maasserver.models.dhcplease import DHCPLease |
63 | 68 | from maasserver.models.staticipaddress import ( | ||
64 | 69 | StaticIPAddress, | ||
65 | 70 | StaticIPAddressExhaustion, | ||
66 | 71 | ) | ||
67 | 67 | from maasserver.models.tag import Tag | 72 | from maasserver.models.tag import Tag |
68 | 68 | from maasserver.models.timestampedmodel import TimestampedModel | 73 | from maasserver.models.timestampedmodel import TimestampedModel |
69 | 69 | from maasserver.models.zone import Zone | 74 | from maasserver.models.zone import Zone |
70 | @@ -74,6 +79,7 @@ | |||
71 | 74 | from piston.models import Token | 79 | from piston.models import Token |
72 | 75 | from provisioningserver.drivers.osystem import OperatingSystemRegistry | 80 | from provisioningserver.drivers.osystem import OperatingSystemRegistry |
73 | 76 | from provisioningserver.tasks import ( | 81 | from provisioningserver.tasks import ( |
74 | 82 | add_new_dhcp_host_map, | ||
75 | 77 | power_off, | 83 | power_off, |
76 | 78 | power_on, | 84 | power_on, |
77 | 79 | remove_dhcp_host_map, | 85 | remove_dhcp_host_map, |
78 | @@ -400,9 +406,24 @@ | |||
79 | 400 | else: | 406 | else: |
80 | 401 | do_start = True | 407 | do_start = True |
81 | 402 | if do_start: | 408 | if do_start: |
85 | 403 | power_on.apply_async( | 409 | try: |
86 | 404 | queue=node.work_queue, args=[node_power_type], | 410 | tasks = node.claim_static_ips() |
87 | 405 | kwargs=power_params) | 411 | except StaticIPAddressExhaustion: |
88 | 412 | # TODO: send error back to user, or fall back to a | ||
89 | 413 | # dynamic IP? | ||
90 | 414 | logger.error( | ||
91 | 415 | "Node %s: Unable to allocate static IP due to address" | ||
92 | 416 | " exhaustion." % node.system_id) | ||
93 | 417 | continue | ||
94 | 418 | |||
95 | 419 | task = power_on.si(node_power_type, **power_params) | ||
96 | 420 | task.set(queue=node.work_queue) | ||
97 | 421 | tasks.append(task) | ||
98 | 422 | chained_tasks = celery.chain(tasks) | ||
99 | 423 | chained_tasks.apply_async() | ||
100 | 424 | # TODO: if any of this fails it needs to release the | ||
101 | 425 | # static IPs back to the pool. As part of the robustness | ||
102 | 426 | # work coming up, it also needs to inform the user. | ||
103 | 406 | processed_nodes.append(node) | 427 | processed_nodes.append(node) |
104 | 407 | return processed_nodes | 428 | return processed_nodes |
105 | 408 | 429 | ||
106 | @@ -566,6 +587,49 @@ | |||
107 | 566 | else: | 587 | else: |
108 | 567 | return self.hostname | 588 | return self.hostname |
109 | 568 | 589 | ||
110 | 590 | def claim_static_ips(self): | ||
111 | 591 | """Assign static IPs for our MACs and return an array of Celery tasks | ||
112 | 592 | that need executing. If nothing needs executing, the empty array | ||
113 | 593 | is returned. | ||
114 | 594 | |||
115 | 595 | Each MAC on the node that is connected to a managed cluster | ||
116 | 596 | interface will get an IP. | ||
117 | 597 | |||
118 | 598 | This operation is atomic, claiming an IP on a particular MAC fails | ||
119 | 599 | then none of the MACs will get an IP and StaticIPAddressExhaustion | ||
120 | 600 | is raised. | ||
121 | 601 | """ | ||
122 | 602 | # TODO: Release claimed MACs inside loop if fail to claim single | ||
123 | 603 | # one (ie make this atomic). | ||
124 | 604 | tasks = [] | ||
125 | 605 | # Get a new AUTO static IP for each MAC on a managed interface. | ||
126 | 606 | macs = self.mac_addresses_on_managed_interfaces() | ||
127 | 607 | for mac in macs: | ||
128 | 608 | sip = mac.claim_static_ip() | ||
129 | 609 | # This is creating an array of celery 'Signatures' which will be | ||
130 | 610 | # chained together later. We make the Signatures immutable | ||
131 | 611 | # otherwise the result of the previous in the chain is passed to | ||
132 | 612 | # the next, this is done with the "si()" call. | ||
133 | 613 | # See docs.celeryproject.org/en/latest/userguide/canvas.html | ||
134 | 614 | |||
135 | 615 | # Note that this may be None if the static range is not yet | ||
136 | 616 | # defined, which will be the case when migrating from older | ||
137 | 617 | # versions of the code. | ||
138 | 618 | if sip is not None: | ||
139 | 619 | # Delete any existing dynamic maps first. | ||
140 | 620 | del_existing = self._build_dynamic_host_map_deletion_task() | ||
141 | 621 | if del_existing is not None: | ||
142 | 622 | # del_existing is a chain so does not need an explicit | ||
143 | 623 | # queue to be set as each subtask will have one. | ||
144 | 624 | tasks.append(del_existing) | ||
145 | 625 | dhcp_key = self.nodegroup.dhcp_key | ||
146 | 626 | mapping = {sip.ip: mac.mac_address} | ||
147 | 627 | dhcp_task = add_new_dhcp_host_map.si( | ||
148 | 628 | mapping, '127.0.0.1', dhcp_key) | ||
149 | 629 | dhcp_task.set(queue=self.work_queue) | ||
150 | 630 | tasks.append(dhcp_task) | ||
151 | 631 | return tasks | ||
152 | 632 | |||
153 | 569 | def ip_addresses(self): | 633 | def ip_addresses(self): |
154 | 570 | """IP addresses allocated to this node. | 634 | """IP addresses allocated to this node. |
155 | 571 | 635 | ||
156 | @@ -632,6 +696,16 @@ | |||
157 | 632 | query = dhcpleases_qs.filter(mac__in=macs) | 696 | query = dhcpleases_qs.filter(mac__in=macs) |
158 | 633 | return query.values_list('ip', flat=True) | 697 | return query.values_list('ip', flat=True) |
159 | 634 | 698 | ||
160 | 699 | def mac_addresses_on_managed_interfaces(self): | ||
161 | 700 | """Return MACAddresses for this node that have a managed cluster | ||
162 | 701 | interface.""" | ||
163 | 702 | # Avoid circular imports | ||
164 | 703 | from maasserver.models import MACAddress | ||
165 | 704 | unmanaged = NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED | ||
166 | 705 | return MACAddress.objects.filter( | ||
167 | 706 | node=self, cluster_interface__isnull=False).exclude( | ||
168 | 707 | cluster_interface__management=unmanaged) | ||
169 | 708 | |||
170 | 635 | def tag_names(self): | 709 | def tag_names(self): |
171 | 636 | # We don't use self.tags.values_list here because this does not | 710 | # We don't use self.tags.values_list here because this does not |
172 | 637 | # take advantage of the cache. | 711 | # take advantage of the cache. |
173 | @@ -761,6 +835,21 @@ | |||
174 | 761 | raise NodeStateViolation( | 835 | raise NodeStateViolation( |
175 | 762 | "Cannot delete node %s: node is in state %s." | 836 | "Cannot delete node %s: node is in state %s." |
176 | 763 | % (self.system_id, NODE_STATUS_CHOICES_DICT[self.status])) | 837 | % (self.system_id, NODE_STATUS_CHOICES_DICT[self.status])) |
177 | 838 | # Delete any dynamic host maps in the DHCP server. | ||
178 | 839 | self._delete_dynamic_host_maps() | ||
179 | 840 | # Delete the related mac addresses. | ||
180 | 841 | # The DHCPLease objects corresponding to these MACs will be deleted | ||
181 | 842 | # as well. See maasserver/models/dhcplease:delete_lease(). | ||
182 | 843 | self.macaddress_set.all().delete() | ||
183 | 844 | |||
184 | 845 | super(Node, self).delete() | ||
185 | 846 | |||
186 | 847 | def _build_dynamic_host_map_deletion_task(self): | ||
187 | 848 | """Create a chained celery task that will delete dhcp host maps. | ||
188 | 849 | |||
189 | 850 | Return None if there is nothing to delete. | ||
190 | 851 | """ | ||
191 | 852 | tasks = [] | ||
192 | 764 | nodegroup = self.nodegroup | 853 | nodegroup = self.nodegroup |
193 | 765 | if len(nodegroup.get_managed_interfaces()) > 0: | 854 | if len(nodegroup.get_managed_interfaces()) > 0: |
194 | 766 | # Delete the host map(s) in the DHCP server. | 855 | # Delete the host map(s) in the DHCP server. |
195 | @@ -772,14 +861,19 @@ | |||
196 | 772 | ip_address=lease.ip, | 861 | ip_address=lease.ip, |
197 | 773 | server_address="127.0.0.1", | 862 | server_address="127.0.0.1", |
198 | 774 | omapi_key=nodegroup.dhcp_key) | 863 | omapi_key=nodegroup.dhcp_key) |
205 | 775 | remove_dhcp_host_map.apply_async( | 864 | task = remove_dhcp_host_map.si(**task_kwargs) |
206 | 776 | queue=nodegroup.uuid, kwargs=task_kwargs) | 865 | task.set(queue=self.work_queue) |
207 | 777 | # Delete the related mac addresses. | 866 | tasks.append(task) |
208 | 778 | # The DHCPLease objects corresponding to these MACs will be deleted | 867 | if len(tasks) > 0: |
209 | 779 | # as well. See maasserver/models/dhcplease:delete_lease(). | 868 | return celery.chain(tasks) |
210 | 780 | self.macaddress_set.all().delete() | 869 | return None |
211 | 781 | 870 | ||
213 | 782 | super(Node, self).delete() | 871 | def _delete_dynamic_host_maps(self): |
214 | 872 | """If any DHCPLeases exist for this node, remove any associated | ||
215 | 873 | host maps.""" | ||
216 | 874 | chain = self._build_dynamic_host_map_deletion_task() | ||
217 | 875 | if chain is not None: | ||
218 | 876 | chain.apply_async() | ||
219 | 783 | 877 | ||
220 | 784 | def set_random_hostname(self): | 878 | def set_random_hostname(self): |
221 | 785 | """Set 5 character `hostname` using non-ambiguous characters. | 879 | """Set 5 character `hostname` using non-ambiguous characters. |
222 | 786 | 880 | ||
223 | === modified file 'src/maasserver/models/staticipaddress.py' | |||
224 | --- src/maasserver/models/staticipaddress.py 2014-06-05 01:28:35 +0000 | |||
225 | +++ src/maasserver/models/staticipaddress.py 2014-06-11 05:52:49 +0000 | |||
226 | @@ -120,7 +120,7 @@ | |||
227 | 120 | # __iter__ does not work here for some reason, so using | 120 | # __iter__ does not work here for some reason, so using |
228 | 121 | # iteritems(). | 121 | # iteritems(). |
229 | 122 | # XXX: convert this into a reverse_map_enum in maasserver.utils. | 122 | # XXX: convert this into a reverse_map_enum in maasserver.utils. |
231 | 123 | for k,v in IPADDRESS_TYPE.__dict__.iteritems(): | 123 | for k, v in IPADDRESS_TYPE.__dict__.iteritems(): |
232 | 124 | if v == self.alloc_type: | 124 | if v == self.alloc_type: |
233 | 125 | strtype = k | 125 | strtype = k |
234 | 126 | break | 126 | break |
235 | @@ -135,4 +135,3 @@ | |||
236 | 135 | After return, this object is no longer valid. | 135 | After return, this object is no longer valid. |
237 | 136 | """ | 136 | """ |
238 | 137 | self.delete() | 137 | self.delete() |
239 | 138 | |||
240 | 139 | 138 | ||
241 | === modified file 'src/maasserver/models/tests/test_macaddress.py' | |||
242 | --- src/maasserver/models/tests/test_macaddress.py 2014-06-10 14:45:14 +0000 | |||
243 | +++ src/maasserver/models/tests/test_macaddress.py 2014-06-11 05:52:49 +0000 | |||
244 | @@ -84,25 +84,13 @@ | |||
245 | 84 | 84 | ||
246 | 85 | class TestMACAddressForStaticIPClaiming(MAASServerTestCase): | 85 | class TestMACAddressForStaticIPClaiming(MAASServerTestCase): |
247 | 86 | 86 | ||
248 | 87 | def make_node_with_mac_attached_to_nodegroupinterface(self): | ||
249 | 88 | nodegroup = factory.make_node_group() | ||
250 | 89 | node = factory.make_node(mac=True, nodegroup=nodegroup) | ||
251 | 90 | low_ip, high_ip = factory.make_ip_range() | ||
252 | 91 | ngi = factory.make_node_group_interface( | ||
253 | 92 | nodegroup, static_ip_range_low=low_ip.ipv4().format(), | ||
254 | 93 | static_ip_range_high=high_ip.ipv4().format()) | ||
255 | 94 | mac = node.get_primary_mac() | ||
256 | 95 | mac.cluster_interface = ngi | ||
257 | 96 | mac.save() | ||
258 | 97 | return node | ||
259 | 98 | |||
260 | 99 | def test_claim_static_ip_returns_none_if_no_cluster_interface(self): | 87 | def test_claim_static_ip_returns_none_if_no_cluster_interface(self): |
261 | 100 | # If mac.cluster_interface is None, we can't allocate any IP. | 88 | # If mac.cluster_interface is None, we can't allocate any IP. |
262 | 101 | mac = factory.make_mac_address() | 89 | mac = factory.make_mac_address() |
263 | 102 | self.assertIsNone(mac.claim_static_ip()) | 90 | self.assertIsNone(mac.claim_static_ip()) |
264 | 103 | 91 | ||
265 | 104 | def test_claim_static_ip_reserves_an_ip_address(self): | 92 | def test_claim_static_ip_reserves_an_ip_address(self): |
267 | 105 | node = self.make_node_with_mac_attached_to_nodegroupinterface() | 93 | node = factory.make_node_with_mac_attached_to_nodegroupinterface() |
268 | 106 | mac = node.get_primary_mac() | 94 | mac = node.get_primary_mac() |
269 | 107 | claimed_ip = mac.claim_static_ip() | 95 | claimed_ip = mac.claim_static_ip() |
270 | 108 | self.assertIsInstance(claimed_ip, StaticIPAddress) | 96 | self.assertIsInstance(claimed_ip, StaticIPAddress) |
271 | @@ -111,13 +99,13 @@ | |||
272 | 111 | IPADDRESS_TYPE.AUTO, StaticIPAddress.objects.all()[0].alloc_type) | 99 | IPADDRESS_TYPE.AUTO, StaticIPAddress.objects.all()[0].alloc_type) |
273 | 112 | 100 | ||
274 | 113 | def test_claim_static_ip_sets_type_as_required(self): | 101 | def test_claim_static_ip_sets_type_as_required(self): |
276 | 114 | node = self.make_node_with_mac_attached_to_nodegroupinterface() | 102 | node = factory.make_node_with_mac_attached_to_nodegroupinterface() |
277 | 115 | mac = node.get_primary_mac() | 103 | mac = node.get_primary_mac() |
278 | 116 | claimed_ip = mac.claim_static_ip(alloc_type=IPADDRESS_TYPE.STICKY) | 104 | claimed_ip = mac.claim_static_ip(alloc_type=IPADDRESS_TYPE.STICKY) |
279 | 117 | self.assertEqual(IPADDRESS_TYPE.STICKY, claimed_ip.alloc_type) | 105 | self.assertEqual(IPADDRESS_TYPE.STICKY, claimed_ip.alloc_type) |
280 | 118 | 106 | ||
281 | 119 | def test_claim_static_ip_returns_none_if_no_static_range_defined(self): | 107 | def test_claim_static_ip_returns_none_if_no_static_range_defined(self): |
283 | 120 | node = self.make_node_with_mac_attached_to_nodegroupinterface() | 108 | node = factory.make_node_with_mac_attached_to_nodegroupinterface() |
284 | 121 | mac = node.get_primary_mac() | 109 | mac = node.get_primary_mac() |
285 | 122 | mac.cluster_interface.static_ip_range_low = None | 110 | mac.cluster_interface.static_ip_range_low = None |
286 | 123 | mac.cluster_interface.static_ip_range_high = None | 111 | mac.cluster_interface.static_ip_range_high = None |
287 | 124 | 112 | ||
288 | === modified file 'src/maasserver/models/tests/test_node.py' | |||
289 | --- src/maasserver/models/tests/test_node.py 2014-06-10 14:45:14 +0000 | |||
290 | +++ src/maasserver/models/tests/test_node.py 2014-06-11 05:52:49 +0000 | |||
291 | @@ -17,6 +17,7 @@ | |||
292 | 17 | from datetime import timedelta | 17 | from datetime import timedelta |
293 | 18 | import random | 18 | import random |
294 | 19 | 19 | ||
295 | 20 | import celery | ||
296 | 20 | from django.core.exceptions import ValidationError | 21 | from django.core.exceptions import ValidationError |
297 | 21 | from maasserver.clusterrpc.power_parameters import get_power_types | 22 | from maasserver.clusterrpc.power_parameters import get_power_types |
298 | 22 | from maasserver.enum import ( | 23 | from maasserver.enum import ( |
299 | @@ -55,12 +56,12 @@ | |||
300 | 55 | NodeUserData, | 56 | NodeUserData, |
301 | 56 | ) | 57 | ) |
302 | 57 | from provisioningserver.power.poweraction import PowerAction | 58 | from provisioningserver.power.poweraction import PowerAction |
303 | 59 | from provisioningserver.tasks import Omshell | ||
304 | 58 | from testtools.matchers import ( | 60 | from testtools.matchers import ( |
305 | 59 | AllMatch, | 61 | AllMatch, |
306 | 60 | Contains, | 62 | Contains, |
307 | 61 | Equals, | 63 | Equals, |
308 | 62 | MatchesAll, | 64 | MatchesAll, |
309 | 63 | MatchesListwise, | ||
310 | 64 | Not, | 65 | Not, |
311 | 65 | ) | 66 | ) |
312 | 66 | 67 | ||
313 | @@ -294,19 +295,26 @@ | |||
314 | 294 | lease = factory.make_dhcp_lease() | 295 | lease = factory.make_dhcp_lease() |
315 | 295 | node = factory.make_node(nodegroup=lease.nodegroup) | 296 | node = factory.make_node(nodegroup=lease.nodegroup) |
316 | 296 | node.add_mac_address(lease.mac) | 297 | node.add_mac_address(lease.mac) |
319 | 297 | mocked_task = self.patch(node_module, "remove_dhcp_host_map") | 298 | self.patch(Omshell, 'remove') |
318 | 298 | mocked_apply_async = self.patch(mocked_task, "apply_async") | ||
320 | 299 | node.delete() | 299 | node.delete() |
324 | 300 | args, kwargs = mocked_apply_async.call_args | 300 | self.assertThat( |
325 | 301 | expected = ( | 301 | self.celery.tasks[0]['kwargs'], |
323 | 302 | Equals(kwargs['queue']), | ||
326 | 303 | Equals({ | 302 | Equals({ |
327 | 304 | 'ip_address': lease.ip, | 303 | 'ip_address': lease.ip, |
328 | 305 | 'server_address': "127.0.0.1", | 304 | 'server_address': "127.0.0.1", |
329 | 306 | 'omapi_key': lease.nodegroup.dhcp_key, | 305 | 'omapi_key': lease.nodegroup.dhcp_key, |
330 | 307 | })) | 306 | })) |
333 | 308 | observed = node.work_queue, kwargs['kwargs'] | 307 | |
334 | 309 | self.assertThat(observed, MatchesListwise(expected)) | 308 | def test_delete_dynamic_host_maps_sends_to_correct_queue(self): |
335 | 309 | lease = factory.make_dhcp_lease() | ||
336 | 310 | node = factory.make_node(nodegroup=lease.nodegroup) | ||
337 | 311 | node.add_mac_address(lease.mac) | ||
338 | 312 | self.patch(Omshell, 'remove') | ||
339 | 313 | option_call = self.patch(celery.canvas.Signature, 'set') | ||
340 | 314 | work_queue = node.work_queue | ||
341 | 315 | node.delete() | ||
342 | 316 | args, kwargs = option_call.call_args | ||
343 | 317 | self.assertEqual(work_queue, kwargs['queue']) | ||
344 | 310 | 318 | ||
345 | 311 | def test_delete_node_removes_multiple_host_maps(self): | 319 | def test_delete_node_removes_multiple_host_maps(self): |
346 | 312 | lease1 = factory.make_dhcp_lease() | 320 | lease1 = factory.make_dhcp_lease() |
347 | @@ -314,10 +322,9 @@ | |||
348 | 314 | node = factory.make_node(nodegroup=lease1.nodegroup) | 322 | node = factory.make_node(nodegroup=lease1.nodegroup) |
349 | 315 | node.add_mac_address(lease1.mac) | 323 | node.add_mac_address(lease1.mac) |
350 | 316 | node.add_mac_address(lease2.mac) | 324 | node.add_mac_address(lease2.mac) |
353 | 317 | mocked_task = self.patch(node_module, "remove_dhcp_host_map") | 325 | self.patch(Omshell, 'remove') |
352 | 318 | mocked_apply_async = self.patch(mocked_task, "apply_async") | ||
354 | 319 | node.delete() | 326 | node.delete() |
356 | 320 | self.assertEqual(2, mocked_apply_async.call_count) | 327 | self.assertEqual(2, len(self.celery.tasks)) |
357 | 321 | 328 | ||
358 | 322 | def test_set_random_hostname_set_hostname(self): | 329 | def test_set_random_hostname_set_hostname(self): |
359 | 323 | # Blank out enlistment_domain. | 330 | # Blank out enlistment_domain. |
360 | @@ -986,6 +993,31 @@ | |||
361 | 986 | node = factory.make_node(architecture=full_arch) | 993 | node = factory.make_node(architecture=full_arch) |
362 | 987 | self.assertEqual((main_arch, sub_arch), node.split_arch()) | 994 | self.assertEqual((main_arch, sub_arch), node.split_arch()) |
363 | 988 | 995 | ||
364 | 996 | def test_mac_addresses_on_managed_interfaces_returns_only_managed(self): | ||
365 | 997 | node = factory.make_node_with_mac_attached_to_nodegroupinterface() | ||
366 | 998 | primary_cluster_interface = node.get_primary_mac().cluster_interface | ||
367 | 999 | primary_cluster_interface.management = ( | ||
368 | 1000 | NODEGROUPINTERFACE_MANAGEMENT.DHCP) | ||
369 | 1001 | primary_cluster_interface.save() | ||
370 | 1002 | |||
371 | 1003 | mac_with_no_interface = factory.make_mac_address(node=node) | ||
372 | 1004 | mac_with_no_interface = mac_with_no_interface # STFU linter | ||
373 | 1005 | unmanaged_interface = factory.make_node_group_interface( | ||
374 | 1006 | nodegroup=node.nodegroup, | ||
375 | 1007 | management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED) | ||
376 | 1008 | |||
377 | 1009 | mac_with_unmanaged_interface = factory.make_mac_address( | ||
378 | 1010 | node=node, cluster_interface=unmanaged_interface) | ||
379 | 1011 | mac_with_unmanaged_interface = mac_with_unmanaged_interface # linter | ||
380 | 1012 | |||
381 | 1013 | observed = node.mac_addresses_on_managed_interfaces() | ||
382 | 1014 | self.assertItemsEqual([node.get_primary_mac()], observed) | ||
383 | 1015 | |||
384 | 1016 | def test_mac_addresses_on_managed_interfaces_returns_empty_if_none(self): | ||
385 | 1017 | node = factory.make_node(mac=True) | ||
386 | 1018 | observed = node.mac_addresses_on_managed_interfaces() | ||
387 | 1019 | self.assertItemsEqual([], observed) | ||
388 | 1020 | |||
389 | 989 | 1021 | ||
390 | 990 | class NodeRoutersTest(MAASServerTestCase): | 1022 | class NodeRoutersTest(MAASServerTestCase): |
391 | 991 | 1023 | ||
392 | @@ -1274,13 +1306,69 @@ | |||
393 | 1274 | self.celery.tasks[0]['kwargs']['mac_address'], | 1306 | self.celery.tasks[0]['kwargs']['mac_address'], |
394 | 1275 | )) | 1307 | )) |
395 | 1276 | 1308 | ||
396 | 1309 | def test_start_nodes_issues_dhcp_host_task(self): | ||
397 | 1310 | user = factory.make_user() | ||
398 | 1311 | node = factory.make_node_with_mac_attached_to_nodegroupinterface( | ||
399 | 1312 | owner=user, power_type='ether_wake') | ||
400 | 1313 | omshell_create = self.patch(Omshell, 'create') | ||
401 | 1314 | output = Node.objects.start_nodes([node.system_id], user) | ||
402 | 1315 | |||
403 | 1316 | # Check that the single node was started, and that the tasks | ||
404 | 1317 | # issued are all there and in the right order. | ||
405 | 1318 | self.assertItemsEqual([node], output) | ||
406 | 1319 | self.assertEqual( | ||
407 | 1320 | [ | ||
408 | 1321 | 'provisioningserver.tasks.add_new_dhcp_host_map', | ||
409 | 1322 | 'provisioningserver.tasks.power_on', | ||
410 | 1323 | ], | ||
411 | 1324 | [ | ||
412 | 1325 | task['task'].name for task in self.celery.tasks | ||
413 | 1326 | ]) | ||
414 | 1327 | |||
415 | 1328 | # Also check that Omshell.create() was called with the right | ||
416 | 1329 | # parameters. | ||
417 | 1330 | mac = node.get_primary_mac() | ||
418 | 1331 | [ip] = mac.ip_addresses.all() | ||
419 | 1332 | expected_ip = ip.ip | ||
420 | 1333 | expected_mac = mac.mac_address | ||
421 | 1334 | args, kwargs = omshell_create.call_args | ||
422 | 1335 | self.assertEqual((expected_ip, expected_mac), args) | ||
423 | 1336 | |||
424 | 1337 | def test_start_nodes_clears_existing_dynamic_maps(self): | ||
425 | 1338 | user = factory.make_user() | ||
426 | 1339 | node = factory.make_node_with_mac_attached_to_nodegroupinterface( | ||
427 | 1340 | owner=user, power_type='ether_wake') | ||
428 | 1341 | factory.make_dhcp_lease( | ||
429 | 1342 | nodegroup=node.nodegroup, mac=node.get_primary_mac().mac_address) | ||
430 | 1343 | self.patch(Omshell, 'create') | ||
431 | 1344 | self.patch(Omshell, 'remove') | ||
432 | 1345 | output = Node.objects.start_nodes([node.system_id], user) | ||
433 | 1346 | |||
434 | 1347 | # Check that the single node was started, and that the tasks | ||
435 | 1348 | # issued are all there and in the right order. | ||
436 | 1349 | self.assertItemsEqual([node], output) | ||
437 | 1350 | self.assertEqual( | ||
438 | 1351 | [ | ||
439 | 1352 | 'provisioningserver.tasks.remove_dhcp_host_map', | ||
440 | 1353 | 'provisioningserver.tasks.add_new_dhcp_host_map', | ||
441 | 1354 | 'provisioningserver.tasks.power_on', | ||
442 | 1355 | ], | ||
443 | 1356 | [ | ||
444 | 1357 | task['task'].name for task in self.celery.tasks | ||
445 | 1358 | ]) | ||
446 | 1359 | |||
447 | 1277 | def test_start_nodes_task_routed_to_nodegroup_worker(self): | 1360 | def test_start_nodes_task_routed_to_nodegroup_worker(self): |
448 | 1361 | # Startup jobs are chained, so the normal way of inspecting a | ||
449 | 1362 | # task directly for routing options doesn't work here, because | ||
450 | 1363 | # in EAGER mode that we use in the test suite, the options are | ||
451 | 1364 | # not passed all the way down to the tasks. Instead, we patch | ||
452 | 1365 | # some celery code to inspect the options that were passed. | ||
453 | 1278 | user = factory.make_user() | 1366 | user = factory.make_user() |
454 | 1279 | node, mac = self.make_node_with_mac( | 1367 | node, mac = self.make_node_with_mac( |
455 | 1280 | user, power_type='ether_wake') | 1368 | user, power_type='ether_wake') |
457 | 1281 | task = self.patch(node_module, 'power_on') | 1369 | option_call = self.patch(celery.canvas.Signature, 'set') |
458 | 1282 | Node.objects.start_nodes([node.system_id], user) | 1370 | Node.objects.start_nodes([node.system_id], user) |
460 | 1283 | args, kwargs = task.apply_async.call_args | 1371 | args, kwargs = option_call.call_args |
461 | 1284 | self.assertEqual(node.work_queue, kwargs['queue']) | 1372 | self.assertEqual(node.work_queue, kwargs['queue']) |
462 | 1285 | 1373 | ||
463 | 1286 | def test_start_nodes_does_not_attempt_power_task_if_no_power_type(self): | 1374 | def test_start_nodes_does_not_attempt_power_task_if_no_power_type(self): |
464 | @@ -1401,3 +1489,67 @@ | |||
465 | 1401 | node = factory.make_node(netboot=True) | 1489 | node = factory.make_node(netboot=True) |
466 | 1402 | node.set_netboot(False) | 1490 | node.set_netboot(False) |
467 | 1403 | self.assertFalse(node.netboot) | 1491 | self.assertFalse(node.netboot) |
468 | 1492 | |||
469 | 1493 | def test_claim_static_ips_ignores_unmanaged_macs(self): | ||
470 | 1494 | node = factory.make_node() | ||
471 | 1495 | for _ in range(0, 10): | ||
472 | 1496 | factory.make_mac_address(node=node) | ||
473 | 1497 | observed = node.claim_static_ips() | ||
474 | 1498 | self.assertItemsEqual([], observed) | ||
475 | 1499 | |||
476 | 1500 | def test_claim_static_ips_creates_task_for_each_managed_mac(self): | ||
477 | 1501 | nodegroup = factory.make_node_group() | ||
478 | 1502 | node = factory.make_node(nodegroup=nodegroup) | ||
479 | 1503 | |||
480 | 1504 | # Add a bunch of MACs attached to managed interfaces. | ||
481 | 1505 | for _ in range(0, 10): | ||
482 | 1506 | low_ip, high_ip = factory.make_ip_range() | ||
483 | 1507 | ngi = factory.make_node_group_interface( | ||
484 | 1508 | nodegroup, static_ip_range_low=low_ip.ipv4().format(), | ||
485 | 1509 | static_ip_range_high=high_ip.ipv4().format(), | ||
486 | 1510 | management=NODEGROUPINTERFACE_MANAGEMENT.DHCP) | ||
487 | 1511 | mac = factory.make_mac_address(node=node) | ||
488 | 1512 | mac.cluster_interface = ngi | ||
489 | 1513 | mac.save() | ||
490 | 1514 | |||
491 | 1515 | observed = node.claim_static_ips() | ||
492 | 1516 | expected = ['provisioningserver.tasks.add_new_dhcp_host_map'] * 10 | ||
493 | 1517 | |||
494 | 1518 | self.assertEqual( | ||
495 | 1519 | expected, | ||
496 | 1520 | [task.task for task in observed] | ||
497 | 1521 | ) | ||
498 | 1522 | |||
499 | 1523 | def test_claim_static_ips_creates_deletion_task(self): | ||
500 | 1524 | # If dhcp leases exist before creating a static IP, the code | ||
501 | 1525 | # should attempt to remove their host maps. | ||
502 | 1526 | node = factory.make_node_with_mac_attached_to_nodegroupinterface() | ||
503 | 1527 | factory.make_dhcp_lease( | ||
504 | 1528 | nodegroup=node.nodegroup, mac=node.get_primary_mac().mac_address) | ||
505 | 1529 | |||
506 | 1530 | observed = node.claim_static_ips() | ||
507 | 1531 | |||
508 | 1532 | self.assertEqual( | ||
509 | 1533 | [ | ||
510 | 1534 | 'celery.chain', | ||
511 | 1535 | 'provisioningserver.tasks.add_new_dhcp_host_map', | ||
512 | 1536 | ], | ||
513 | 1537 | [ | ||
514 | 1538 | task.task for task in observed | ||
515 | 1539 | ]) | ||
516 | 1540 | |||
517 | 1541 | # Probe the chain to make sure it has the deletion task. | ||
518 | 1542 | self.assertEqual( | ||
519 | 1543 | 'provisioningserver.tasks.remove_dhcp_host_map', | ||
520 | 1544 | observed[0].tasks[0].task, | ||
521 | 1545 | ) | ||
522 | 1546 | |||
523 | 1547 | def test_claim_static_ips_ignores_interface_with_no_static_range(self): | ||
524 | 1548 | node = factory.make_node_with_mac_attached_to_nodegroupinterface() | ||
525 | 1549 | ngi = node.get_primary_mac().cluster_interface | ||
526 | 1550 | ngi.static_ip_range_low = None | ||
527 | 1551 | ngi.static_ip_range_high = None | ||
528 | 1552 | ngi.save() | ||
529 | 1553 | |||
530 | 1554 | observed = node.claim_static_ips() | ||
531 | 1555 | self.assertItemsEqual([], observed) | ||
532 | 1404 | 1556 | ||
533 | === modified file 'src/maasserver/testing/factory.py' | |||
534 | --- src/maasserver/testing/factory.py 2014-06-11 04:38:41 +0000 | |||
535 | +++ src/maasserver/testing/factory.py 2014-06-11 05:52:49 +0000 | |||
536 | @@ -354,18 +354,37 @@ | |||
537 | 354 | """Generate a random MAC address, in the form of a MAC object.""" | 354 | """Generate a random MAC address, in the form of a MAC object.""" |
538 | 355 | return MAC(self.getRandomMACAddress()) | 355 | return MAC(self.getRandomMACAddress()) |
539 | 356 | 356 | ||
541 | 357 | def make_mac_address(self, address=None, node=None, networks=None): | 357 | def make_mac_address(self, address=None, node=None, networks=None, |
542 | 358 | **kwargs): | ||
543 | 358 | """Create a MACAddress model object.""" | 359 | """Create a MACAddress model object.""" |
544 | 359 | if node is None: | 360 | if node is None: |
545 | 360 | node = self.make_node() | 361 | node = self.make_node() |
546 | 361 | if address is None: | 362 | if address is None: |
547 | 362 | address = self.getRandomMACAddress() | 363 | address = self.getRandomMACAddress() |
549 | 363 | mac = MACAddress(mac_address=MAC(address), node=node) | 364 | mac = MACAddress(mac_address=MAC(address), node=node, **kwargs) |
550 | 364 | mac.save() | 365 | mac.save() |
551 | 365 | if networks is not None: | 366 | if networks is not None: |
552 | 366 | mac.networks.add(*networks) | 367 | mac.networks.add(*networks) |
553 | 367 | return mac | 368 | return mac |
554 | 368 | 369 | ||
555 | 370 | def make_node_with_mac_attached_to_nodegroupinterface(self, **kwargs): | ||
556 | 371 | """Create a Node that has a MACAddress which has a | ||
557 | 372 | NodeGroupInterface. | ||
558 | 373 | |||
559 | 374 | :param **kwargs: Additional parameters to pass to make_node. | ||
560 | 375 | """ | ||
561 | 376 | nodegroup = self.make_node_group() | ||
562 | 377 | node = self.make_node(mac=True, nodegroup=nodegroup, **kwargs) | ||
563 | 378 | low_ip, high_ip = factory.make_ip_range() | ||
564 | 379 | ngi = self.make_node_group_interface( | ||
565 | 380 | nodegroup, static_ip_range_low=low_ip.ipv4().format(), | ||
566 | 381 | static_ip_range_high=high_ip.ipv4().format(), | ||
567 | 382 | management=NODEGROUPINTERFACE_MANAGEMENT.DHCP) | ||
568 | 383 | mac = node.get_primary_mac() | ||
569 | 384 | mac.cluster_interface = ngi | ||
570 | 385 | mac.save() | ||
571 | 386 | return node | ||
572 | 387 | |||
573 | 369 | def make_staticipaddress(self, ip=None, alloc_type=IPADDRESS_TYPE.AUTO, | 388 | def make_staticipaddress(self, ip=None, alloc_type=IPADDRESS_TYPE.AUTO, |
574 | 370 | mac=None): | 389 | mac=None): |
575 | 371 | """Create and return a StaticIPAddress model object. | 390 | """Create and return a StaticIPAddress model object. |