Merge lp:~julian-edwards/maas/allocate-ip-on-start-2 into lp:~maas-committers/maas/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
Reviewer Review Type Date Requested Status
MAAS Maintainers Pending
Review via email: mp+222613@code.launchpad.net

This proposal has been superseded by a proposal from 2014-06-11.

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
=== modified file 'src/maasserver/models/__init__.py'
--- src/maasserver/models/__init__.py 2014-06-10 14:45:14 +0000
+++ src/maasserver/models/__init__.py 2014-06-11 05:52:49 +0000
@@ -51,7 +51,6 @@
51from maasserver.models.dhcplease import DHCPLease51from maasserver.models.dhcplease import DHCPLease
52from maasserver.models.downloadprogress import DownloadProgress52from maasserver.models.downloadprogress import DownloadProgress
53from maasserver.models.filestorage import FileStorage53from maasserver.models.filestorage import FileStorage
54from maasserver.models.staticipaddress import StaticIPAddress
55from maasserver.models.macaddress import MACAddress54from maasserver.models.macaddress import MACAddress
56from maasserver.models.macipaddresslink import MACStaticIPAddressLink55from maasserver.models.macipaddresslink import MACStaticIPAddressLink
57from maasserver.models.network import Network56from maasserver.models.network import Network
@@ -59,6 +58,7 @@
59from maasserver.models.nodegroup import NodeGroup58from maasserver.models.nodegroup import NodeGroup
60from maasserver.models.nodegroupinterface import NodeGroupInterface59from maasserver.models.nodegroupinterface import NodeGroupInterface
61from maasserver.models.sshkey import SSHKey60from maasserver.models.sshkey import SSHKey
61from maasserver.models.staticipaddress import StaticIPAddress
62from maasserver.models.tag import Tag62from maasserver.models.tag import Tag
63from maasserver.models.user import create_user63from maasserver.models.user import create_user
64from maasserver.models.userprofile import UserProfile64from maasserver.models.userprofile import UserProfile
6565
=== modified file 'src/maasserver/models/macaddress.py'
--- src/maasserver/models/macaddress.py 2014-06-10 14:45:14 +0000
+++ src/maasserver/models/macaddress.py 2014-06-11 05:52:49 +0000
@@ -90,7 +90,10 @@
90 def claim_static_ip(self, alloc_type=IPADDRESS_TYPE.AUTO):90 def claim_static_ip(self, alloc_type=IPADDRESS_TYPE.AUTO):
91 """Assign a static IP to this MAC.91 """Assign a static IP to this MAC.
9292
93 TODO: Also set a host DHCP entry.93 It is the caller's responsibility to create a celery Task that will
94 write the dhcp host. It is not done here because celery doesn't
95 guarantee job ordering, and if the host entry is written after
96 the host boots it is too late.
9497
95 :param alloc_type: See :class:`StaticIPAddress`.alloc_type.98 :param alloc_type: See :class:`StaticIPAddress`.alloc_type.
96 :return: A :class:`StaticIPAddress` object. Returns None if99 :return: A :class:`StaticIPAddress` object. Returns None if
97100
=== modified file 'src/maasserver/models/node.py'
--- src/maasserver/models/node.py 2014-06-10 15:26:47 +0000
+++ src/maasserver/models/node.py 2014-06-11 05:52:49 +0000
@@ -28,6 +28,7 @@
28from string import whitespace28from string import whitespace
29from uuid import uuid129from uuid import uuid1
3030
31import celery
31from django.contrib.auth.models import User32from django.contrib.auth.models import User
32from django.core.exceptions import (33from django.core.exceptions import (
33 PermissionDenied,34 PermissionDenied,
@@ -54,16 +55,20 @@
54 NODE_STATUS,55 NODE_STATUS,
55 NODE_STATUS_CHOICES,56 NODE_STATUS_CHOICES,
56 NODE_STATUS_CHOICES_DICT,57 NODE_STATUS_CHOICES_DICT,
58 NODEGROUPINTERFACE_MANAGEMENT,
57 )59 )
58from maasserver.exceptions import NodeStateViolation60from maasserver.exceptions import NodeStateViolation
59from maasserver.fields import (61from maasserver.fields import (
60 JSONObjectField,62 JSONObjectField,
61 MAC,63 MAC,
62 )64 )
63from maasserver.models import StaticIPAddress
64from maasserver.models.cleansave import CleanSave65from maasserver.models.cleansave import CleanSave
65from maasserver.models.config import Config66from maasserver.models.config import Config
66from maasserver.models.dhcplease import DHCPLease67from maasserver.models.dhcplease import DHCPLease
68from maasserver.models.staticipaddress import (
69 StaticIPAddress,
70 StaticIPAddressExhaustion,
71 )
67from maasserver.models.tag import Tag72from maasserver.models.tag import Tag
68from maasserver.models.timestampedmodel import TimestampedModel73from maasserver.models.timestampedmodel import TimestampedModel
69from maasserver.models.zone import Zone74from maasserver.models.zone import Zone
@@ -74,6 +79,7 @@
74from piston.models import Token79from piston.models import Token
75from provisioningserver.drivers.osystem import OperatingSystemRegistry80from provisioningserver.drivers.osystem import OperatingSystemRegistry
76from provisioningserver.tasks import (81from provisioningserver.tasks import (
82 add_new_dhcp_host_map,
77 power_off,83 power_off,
78 power_on,84 power_on,
79 remove_dhcp_host_map,85 remove_dhcp_host_map,
@@ -400,9 +406,24 @@
400 else:406 else:
401 do_start = True407 do_start = True
402 if do_start:408 if do_start:
403 power_on.apply_async(409 try:
404 queue=node.work_queue, args=[node_power_type],410 tasks = node.claim_static_ips()
405 kwargs=power_params)411 except StaticIPAddressExhaustion:
412 # TODO: send error back to user, or fall back to a
413 # dynamic IP?
414 logger.error(
415 "Node %s: Unable to allocate static IP due to address"
416 " exhaustion." % node.system_id)
417 continue
418
419 task = power_on.si(node_power_type, **power_params)
420 task.set(queue=node.work_queue)
421 tasks.append(task)
422 chained_tasks = celery.chain(tasks)
423 chained_tasks.apply_async()
424 # TODO: if any of this fails it needs to release the
425 # static IPs back to the pool. As part of the robustness
426 # work coming up, it also needs to inform the user.
406 processed_nodes.append(node)427 processed_nodes.append(node)
407 return processed_nodes428 return processed_nodes
408429
@@ -566,6 +587,49 @@
566 else:587 else:
567 return self.hostname588 return self.hostname
568589
590 def claim_static_ips(self):
591 """Assign static IPs for our MACs and return an array of Celery tasks
592 that need executing. If nothing needs executing, the empty array
593 is returned.
594
595 Each MAC on the node that is connected to a managed cluster
596 interface will get an IP.
597
598 This operation is atomic, claiming an IP on a particular MAC fails
599 then none of the MACs will get an IP and StaticIPAddressExhaustion
600 is raised.
601 """
602 # TODO: Release claimed MACs inside loop if fail to claim single
603 # one (ie make this atomic).
604 tasks = []
605 # Get a new AUTO static IP for each MAC on a managed interface.
606 macs = self.mac_addresses_on_managed_interfaces()
607 for mac in macs:
608 sip = mac.claim_static_ip()
609 # This is creating an array of celery 'Signatures' which will be
610 # chained together later. We make the Signatures immutable
611 # otherwise the result of the previous in the chain is passed to
612 # the next, this is done with the "si()" call.
613 # See docs.celeryproject.org/en/latest/userguide/canvas.html
614
615 # Note that this may be None if the static range is not yet
616 # defined, which will be the case when migrating from older
617 # versions of the code.
618 if sip is not None:
619 # Delete any existing dynamic maps first.
620 del_existing = self._build_dynamic_host_map_deletion_task()
621 if del_existing is not None:
622 # del_existing is a chain so does not need an explicit
623 # queue to be set as each subtask will have one.
624 tasks.append(del_existing)
625 dhcp_key = self.nodegroup.dhcp_key
626 mapping = {sip.ip: mac.mac_address}
627 dhcp_task = add_new_dhcp_host_map.si(
628 mapping, '127.0.0.1', dhcp_key)
629 dhcp_task.set(queue=self.work_queue)
630 tasks.append(dhcp_task)
631 return tasks
632
569 def ip_addresses(self):633 def ip_addresses(self):
570 """IP addresses allocated to this node.634 """IP addresses allocated to this node.
571635
@@ -632,6 +696,16 @@
632 query = dhcpleases_qs.filter(mac__in=macs)696 query = dhcpleases_qs.filter(mac__in=macs)
633 return query.values_list('ip', flat=True)697 return query.values_list('ip', flat=True)
634698
699 def mac_addresses_on_managed_interfaces(self):
700 """Return MACAddresses for this node that have a managed cluster
701 interface."""
702 # Avoid circular imports
703 from maasserver.models import MACAddress
704 unmanaged = NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED
705 return MACAddress.objects.filter(
706 node=self, cluster_interface__isnull=False).exclude(
707 cluster_interface__management=unmanaged)
708
635 def tag_names(self):709 def tag_names(self):
636 # We don't use self.tags.values_list here because this does not710 # We don't use self.tags.values_list here because this does not
637 # take advantage of the cache.711 # take advantage of the cache.
@@ -761,6 +835,21 @@
761 raise NodeStateViolation(835 raise NodeStateViolation(
762 "Cannot delete node %s: node is in state %s."836 "Cannot delete node %s: node is in state %s."
763 % (self.system_id, NODE_STATUS_CHOICES_DICT[self.status]))837 % (self.system_id, NODE_STATUS_CHOICES_DICT[self.status]))
838 # Delete any dynamic host maps in the DHCP server.
839 self._delete_dynamic_host_maps()
840 # Delete the related mac addresses.
841 # The DHCPLease objects corresponding to these MACs will be deleted
842 # as well. See maasserver/models/dhcplease:delete_lease().
843 self.macaddress_set.all().delete()
844
845 super(Node, self).delete()
846
847 def _build_dynamic_host_map_deletion_task(self):
848 """Create a chained celery task that will delete dhcp host maps.
849
850 Return None if there is nothing to delete.
851 """
852 tasks = []
764 nodegroup = self.nodegroup853 nodegroup = self.nodegroup
765 if len(nodegroup.get_managed_interfaces()) > 0:854 if len(nodegroup.get_managed_interfaces()) > 0:
766 # Delete the host map(s) in the DHCP server.855 # Delete the host map(s) in the DHCP server.
@@ -772,14 +861,19 @@
772 ip_address=lease.ip,861 ip_address=lease.ip,
773 server_address="127.0.0.1",862 server_address="127.0.0.1",
774 omapi_key=nodegroup.dhcp_key)863 omapi_key=nodegroup.dhcp_key)
775 remove_dhcp_host_map.apply_async(864 task = remove_dhcp_host_map.si(**task_kwargs)
776 queue=nodegroup.uuid, kwargs=task_kwargs)865 task.set(queue=self.work_queue)
777 # Delete the related mac addresses.866 tasks.append(task)
778 # The DHCPLease objects corresponding to these MACs will be deleted867 if len(tasks) > 0:
779 # as well. See maasserver/models/dhcplease:delete_lease().868 return celery.chain(tasks)
780 self.macaddress_set.all().delete()869 return None
781870
782 super(Node, self).delete()871 def _delete_dynamic_host_maps(self):
872 """If any DHCPLeases exist for this node, remove any associated
873 host maps."""
874 chain = self._build_dynamic_host_map_deletion_task()
875 if chain is not None:
876 chain.apply_async()
783877
784 def set_random_hostname(self):878 def set_random_hostname(self):
785 """Set 5 character `hostname` using non-ambiguous characters.879 """Set 5 character `hostname` using non-ambiguous characters.
786880
=== modified file 'src/maasserver/models/staticipaddress.py'
--- src/maasserver/models/staticipaddress.py 2014-06-05 01:28:35 +0000
+++ src/maasserver/models/staticipaddress.py 2014-06-11 05:52:49 +0000
@@ -120,7 +120,7 @@
120 # __iter__ does not work here for some reason, so using120 # __iter__ does not work here for some reason, so using
121 # iteritems().121 # iteritems().
122 # XXX: convert this into a reverse_map_enum in maasserver.utils.122 # XXX: convert this into a reverse_map_enum in maasserver.utils.
123 for k,v in IPADDRESS_TYPE.__dict__.iteritems():123 for k, v in IPADDRESS_TYPE.__dict__.iteritems():
124 if v == self.alloc_type:124 if v == self.alloc_type:
125 strtype = k125 strtype = k
126 break126 break
@@ -135,4 +135,3 @@
135 After return, this object is no longer valid.135 After return, this object is no longer valid.
136 """136 """
137 self.delete()137 self.delete()
138
139138
=== modified file 'src/maasserver/models/tests/test_macaddress.py'
--- src/maasserver/models/tests/test_macaddress.py 2014-06-10 14:45:14 +0000
+++ src/maasserver/models/tests/test_macaddress.py 2014-06-11 05:52:49 +0000
@@ -84,25 +84,13 @@
8484
85class TestMACAddressForStaticIPClaiming(MAASServerTestCase):85class TestMACAddressForStaticIPClaiming(MAASServerTestCase):
8686
87 def make_node_with_mac_attached_to_nodegroupinterface(self):
88 nodegroup = factory.make_node_group()
89 node = factory.make_node(mac=True, nodegroup=nodegroup)
90 low_ip, high_ip = factory.make_ip_range()
91 ngi = factory.make_node_group_interface(
92 nodegroup, static_ip_range_low=low_ip.ipv4().format(),
93 static_ip_range_high=high_ip.ipv4().format())
94 mac = node.get_primary_mac()
95 mac.cluster_interface = ngi
96 mac.save()
97 return node
98
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):
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.
101 mac = factory.make_mac_address()89 mac = factory.make_mac_address()
102 self.assertIsNone(mac.claim_static_ip())90 self.assertIsNone(mac.claim_static_ip())
10391
104 def test_claim_static_ip_reserves_an_ip_address(self):92 def test_claim_static_ip_reserves_an_ip_address(self):
105 node = self.make_node_with_mac_attached_to_nodegroupinterface()93 node = factory.make_node_with_mac_attached_to_nodegroupinterface()
106 mac = node.get_primary_mac()94 mac = node.get_primary_mac()
107 claimed_ip = mac.claim_static_ip()95 claimed_ip = mac.claim_static_ip()
108 self.assertIsInstance(claimed_ip, StaticIPAddress)96 self.assertIsInstance(claimed_ip, StaticIPAddress)
@@ -111,13 +99,13 @@
111 IPADDRESS_TYPE.AUTO, StaticIPAddress.objects.all()[0].alloc_type)99 IPADDRESS_TYPE.AUTO, StaticIPAddress.objects.all()[0].alloc_type)
112100
113 def test_claim_static_ip_sets_type_as_required(self):101 def test_claim_static_ip_sets_type_as_required(self):
114 node = self.make_node_with_mac_attached_to_nodegroupinterface()102 node = factory.make_node_with_mac_attached_to_nodegroupinterface()
115 mac = node.get_primary_mac()103 mac = node.get_primary_mac()
116 claimed_ip = mac.claim_static_ip(alloc_type=IPADDRESS_TYPE.STICKY)104 claimed_ip = mac.claim_static_ip(alloc_type=IPADDRESS_TYPE.STICKY)
117 self.assertEqual(IPADDRESS_TYPE.STICKY, claimed_ip.alloc_type)105 self.assertEqual(IPADDRESS_TYPE.STICKY, claimed_ip.alloc_type)
118106
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):
120 node = self.make_node_with_mac_attached_to_nodegroupinterface()108 node = factory.make_node_with_mac_attached_to_nodegroupinterface()
121 mac = node.get_primary_mac()109 mac = node.get_primary_mac()
122 mac.cluster_interface.static_ip_range_low = None110 mac.cluster_interface.static_ip_range_low = None
123 mac.cluster_interface.static_ip_range_high = None111 mac.cluster_interface.static_ip_range_high = None
124112
=== modified file 'src/maasserver/models/tests/test_node.py'
--- src/maasserver/models/tests/test_node.py 2014-06-10 14:45:14 +0000
+++ src/maasserver/models/tests/test_node.py 2014-06-11 05:52:49 +0000
@@ -17,6 +17,7 @@
17from datetime import timedelta17from datetime import timedelta
18import random18import random
1919
20import celery
20from django.core.exceptions import ValidationError21from django.core.exceptions import ValidationError
21from maasserver.clusterrpc.power_parameters import get_power_types22from maasserver.clusterrpc.power_parameters import get_power_types
22from maasserver.enum import (23from maasserver.enum import (
@@ -55,12 +56,12 @@
55 NodeUserData,56 NodeUserData,
56 )57 )
57from provisioningserver.power.poweraction import PowerAction58from provisioningserver.power.poweraction import PowerAction
59from provisioningserver.tasks import Omshell
58from testtools.matchers import (60from testtools.matchers import (
59 AllMatch,61 AllMatch,
60 Contains,62 Contains,
61 Equals,63 Equals,
62 MatchesAll,64 MatchesAll,
63 MatchesListwise,
64 Not,65 Not,
65 )66 )
6667
@@ -294,19 +295,26 @@
294 lease = factory.make_dhcp_lease()295 lease = factory.make_dhcp_lease()
295 node = factory.make_node(nodegroup=lease.nodegroup)296 node = factory.make_node(nodegroup=lease.nodegroup)
296 node.add_mac_address(lease.mac)297 node.add_mac_address(lease.mac)
297 mocked_task = self.patch(node_module, "remove_dhcp_host_map")298 self.patch(Omshell, 'remove')
298 mocked_apply_async = self.patch(mocked_task, "apply_async")
299 node.delete()299 node.delete()
300 args, kwargs = mocked_apply_async.call_args300 self.assertThat(
301 expected = (301 self.celery.tasks[0]['kwargs'],
302 Equals(kwargs['queue']),
303 Equals({302 Equals({
304 'ip_address': lease.ip,303 'ip_address': lease.ip,
305 'server_address': "127.0.0.1",304 'server_address': "127.0.0.1",
306 'omapi_key': lease.nodegroup.dhcp_key,305 'omapi_key': lease.nodegroup.dhcp_key,
307 }))306 }))
308 observed = node.work_queue, kwargs['kwargs']307
309 self.assertThat(observed, MatchesListwise(expected))308 def test_delete_dynamic_host_maps_sends_to_correct_queue(self):
309 lease = factory.make_dhcp_lease()
310 node = factory.make_node(nodegroup=lease.nodegroup)
311 node.add_mac_address(lease.mac)
312 self.patch(Omshell, 'remove')
313 option_call = self.patch(celery.canvas.Signature, 'set')
314 work_queue = node.work_queue
315 node.delete()
316 args, kwargs = option_call.call_args
317 self.assertEqual(work_queue, kwargs['queue'])
310318
311 def test_delete_node_removes_multiple_host_maps(self):319 def test_delete_node_removes_multiple_host_maps(self):
312 lease1 = factory.make_dhcp_lease()320 lease1 = factory.make_dhcp_lease()
@@ -314,10 +322,9 @@
314 node = factory.make_node(nodegroup=lease1.nodegroup)322 node = factory.make_node(nodegroup=lease1.nodegroup)
315 node.add_mac_address(lease1.mac)323 node.add_mac_address(lease1.mac)
316 node.add_mac_address(lease2.mac)324 node.add_mac_address(lease2.mac)
317 mocked_task = self.patch(node_module, "remove_dhcp_host_map")325 self.patch(Omshell, 'remove')
318 mocked_apply_async = self.patch(mocked_task, "apply_async")
319 node.delete()326 node.delete()
320 self.assertEqual(2, mocked_apply_async.call_count)327 self.assertEqual(2, len(self.celery.tasks))
321328
322 def test_set_random_hostname_set_hostname(self):329 def test_set_random_hostname_set_hostname(self):
323 # Blank out enlistment_domain.330 # Blank out enlistment_domain.
@@ -986,6 +993,31 @@
986 node = factory.make_node(architecture=full_arch)993 node = factory.make_node(architecture=full_arch)
987 self.assertEqual((main_arch, sub_arch), node.split_arch())994 self.assertEqual((main_arch, sub_arch), node.split_arch())
988995
996 def test_mac_addresses_on_managed_interfaces_returns_only_managed(self):
997 node = factory.make_node_with_mac_attached_to_nodegroupinterface()
998 primary_cluster_interface = node.get_primary_mac().cluster_interface
999 primary_cluster_interface.management = (
1000 NODEGROUPINTERFACE_MANAGEMENT.DHCP)
1001 primary_cluster_interface.save()
1002
1003 mac_with_no_interface = factory.make_mac_address(node=node)
1004 mac_with_no_interface = mac_with_no_interface # STFU linter
1005 unmanaged_interface = factory.make_node_group_interface(
1006 nodegroup=node.nodegroup,
1007 management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)
1008
1009 mac_with_unmanaged_interface = factory.make_mac_address(
1010 node=node, cluster_interface=unmanaged_interface)
1011 mac_with_unmanaged_interface = mac_with_unmanaged_interface # linter
1012
1013 observed = node.mac_addresses_on_managed_interfaces()
1014 self.assertItemsEqual([node.get_primary_mac()], observed)
1015
1016 def test_mac_addresses_on_managed_interfaces_returns_empty_if_none(self):
1017 node = factory.make_node(mac=True)
1018 observed = node.mac_addresses_on_managed_interfaces()
1019 self.assertItemsEqual([], observed)
1020
9891021
990class NodeRoutersTest(MAASServerTestCase):1022class NodeRoutersTest(MAASServerTestCase):
9911023
@@ -1274,13 +1306,69 @@
1274 self.celery.tasks[0]['kwargs']['mac_address'],1306 self.celery.tasks[0]['kwargs']['mac_address'],
1275 ))1307 ))
12761308
1309 def test_start_nodes_issues_dhcp_host_task(self):
1310 user = factory.make_user()
1311 node = factory.make_node_with_mac_attached_to_nodegroupinterface(
1312 owner=user, power_type='ether_wake')
1313 omshell_create = self.patch(Omshell, 'create')
1314 output = Node.objects.start_nodes([node.system_id], user)
1315
1316 # Check that the single node was started, and that the tasks
1317 # issued are all there and in the right order.
1318 self.assertItemsEqual([node], output)
1319 self.assertEqual(
1320 [
1321 'provisioningserver.tasks.add_new_dhcp_host_map',
1322 'provisioningserver.tasks.power_on',
1323 ],
1324 [
1325 task['task'].name for task in self.celery.tasks
1326 ])
1327
1328 # Also check that Omshell.create() was called with the right
1329 # parameters.
1330 mac = node.get_primary_mac()
1331 [ip] = mac.ip_addresses.all()
1332 expected_ip = ip.ip
1333 expected_mac = mac.mac_address
1334 args, kwargs = omshell_create.call_args
1335 self.assertEqual((expected_ip, expected_mac), args)
1336
1337 def test_start_nodes_clears_existing_dynamic_maps(self):
1338 user = factory.make_user()
1339 node = factory.make_node_with_mac_attached_to_nodegroupinterface(
1340 owner=user, power_type='ether_wake')
1341 factory.make_dhcp_lease(
1342 nodegroup=node.nodegroup, mac=node.get_primary_mac().mac_address)
1343 self.patch(Omshell, 'create')
1344 self.patch(Omshell, 'remove')
1345 output = Node.objects.start_nodes([node.system_id], user)
1346
1347 # Check that the single node was started, and that the tasks
1348 # issued are all there and in the right order.
1349 self.assertItemsEqual([node], output)
1350 self.assertEqual(
1351 [
1352 'provisioningserver.tasks.remove_dhcp_host_map',
1353 'provisioningserver.tasks.add_new_dhcp_host_map',
1354 'provisioningserver.tasks.power_on',
1355 ],
1356 [
1357 task['task'].name for task in self.celery.tasks
1358 ])
1359
1277 def test_start_nodes_task_routed_to_nodegroup_worker(self):1360 def test_start_nodes_task_routed_to_nodegroup_worker(self):
1361 # Startup jobs are chained, so the normal way of inspecting a
1362 # task directly for routing options doesn't work here, because
1363 # in EAGER mode that we use in the test suite, the options are
1364 # not passed all the way down to the tasks. Instead, we patch
1365 # some celery code to inspect the options that were passed.
1278 user = factory.make_user()1366 user = factory.make_user()
1279 node, mac = self.make_node_with_mac(1367 node, mac = self.make_node_with_mac(
1280 user, power_type='ether_wake')1368 user, power_type='ether_wake')
1281 task = self.patch(node_module, 'power_on')1369 option_call = self.patch(celery.canvas.Signature, 'set')
1282 Node.objects.start_nodes([node.system_id], user)1370 Node.objects.start_nodes([node.system_id], user)
1283 args, kwargs = task.apply_async.call_args1371 args, kwargs = option_call.call_args
1284 self.assertEqual(node.work_queue, kwargs['queue'])1372 self.assertEqual(node.work_queue, kwargs['queue'])
12851373
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):
@@ -1401,3 +1489,67 @@
1401 node = factory.make_node(netboot=True)1489 node = factory.make_node(netboot=True)
1402 node.set_netboot(False)1490 node.set_netboot(False)
1403 self.assertFalse(node.netboot)1491 self.assertFalse(node.netboot)
1492
1493 def test_claim_static_ips_ignores_unmanaged_macs(self):
1494 node = factory.make_node()
1495 for _ in range(0, 10):
1496 factory.make_mac_address(node=node)
1497 observed = node.claim_static_ips()
1498 self.assertItemsEqual([], observed)
1499
1500 def test_claim_static_ips_creates_task_for_each_managed_mac(self):
1501 nodegroup = factory.make_node_group()
1502 node = factory.make_node(nodegroup=nodegroup)
1503
1504 # Add a bunch of MACs attached to managed interfaces.
1505 for _ in range(0, 10):
1506 low_ip, high_ip = factory.make_ip_range()
1507 ngi = factory.make_node_group_interface(
1508 nodegroup, static_ip_range_low=low_ip.ipv4().format(),
1509 static_ip_range_high=high_ip.ipv4().format(),
1510 management=NODEGROUPINTERFACE_MANAGEMENT.DHCP)
1511 mac = factory.make_mac_address(node=node)
1512 mac.cluster_interface = ngi
1513 mac.save()
1514
1515 observed = node.claim_static_ips()
1516 expected = ['provisioningserver.tasks.add_new_dhcp_host_map'] * 10
1517
1518 self.assertEqual(
1519 expected,
1520 [task.task for task in observed]
1521 )
1522
1523 def test_claim_static_ips_creates_deletion_task(self):
1524 # If dhcp leases exist before creating a static IP, the code
1525 # should attempt to remove their host maps.
1526 node = factory.make_node_with_mac_attached_to_nodegroupinterface()
1527 factory.make_dhcp_lease(
1528 nodegroup=node.nodegroup, mac=node.get_primary_mac().mac_address)
1529
1530 observed = node.claim_static_ips()
1531
1532 self.assertEqual(
1533 [
1534 'celery.chain',
1535 'provisioningserver.tasks.add_new_dhcp_host_map',
1536 ],
1537 [
1538 task.task for task in observed
1539 ])
1540
1541 # Probe the chain to make sure it has the deletion task.
1542 self.assertEqual(
1543 'provisioningserver.tasks.remove_dhcp_host_map',
1544 observed[0].tasks[0].task,
1545 )
1546
1547 def test_claim_static_ips_ignores_interface_with_no_static_range(self):
1548 node = factory.make_node_with_mac_attached_to_nodegroupinterface()
1549 ngi = node.get_primary_mac().cluster_interface
1550 ngi.static_ip_range_low = None
1551 ngi.static_ip_range_high = None
1552 ngi.save()
1553
1554 observed = node.claim_static_ips()
1555 self.assertItemsEqual([], observed)
14041556
=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py 2014-06-11 04:38:41 +0000
+++ src/maasserver/testing/factory.py 2014-06-11 05:52:49 +0000
@@ -354,18 +354,37 @@
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."""
355 return MAC(self.getRandomMACAddress())355 return MAC(self.getRandomMACAddress())
356356
357 def make_mac_address(self, address=None, node=None, networks=None):357 def make_mac_address(self, address=None, node=None, networks=None,
358 **kwargs):
358 """Create a MACAddress model object."""359 """Create a MACAddress model object."""
359 if node is None:360 if node is None:
360 node = self.make_node()361 node = self.make_node()
361 if address is None:362 if address is None:
362 address = self.getRandomMACAddress()363 address = self.getRandomMACAddress()
363 mac = MACAddress(mac_address=MAC(address), node=node)364 mac = MACAddress(mac_address=MAC(address), node=node, **kwargs)
364 mac.save()365 mac.save()
365 if networks is not None:366 if networks is not None:
366 mac.networks.add(*networks)367 mac.networks.add(*networks)
367 return mac368 return mac
368369
370 def make_node_with_mac_attached_to_nodegroupinterface(self, **kwargs):
371 """Create a Node that has a MACAddress which has a
372 NodeGroupInterface.
373
374 :param **kwargs: Additional parameters to pass to make_node.
375 """
376 nodegroup = self.make_node_group()
377 node = self.make_node(mac=True, nodegroup=nodegroup, **kwargs)
378 low_ip, high_ip = factory.make_ip_range()
379 ngi = self.make_node_group_interface(
380 nodegroup, static_ip_range_low=low_ip.ipv4().format(),
381 static_ip_range_high=high_ip.ipv4().format(),
382 management=NODEGROUPINTERFACE_MANAGEMENT.DHCP)
383 mac = node.get_primary_mac()
384 mac.cluster_interface = ngi
385 mac.save()
386 return node
387
369 def make_staticipaddress(self, ip=None, alloc_type=IPADDRESS_TYPE.AUTO,388 def make_staticipaddress(self, ip=None, alloc_type=IPADDRESS_TYPE.AUTO,
370 mac=None):389 mac=None):
371 """Create and return a StaticIPAddress model object.390 """Create and return a StaticIPAddress model object.