Merge lp:~blake-rouse/maas/external-dhcp-probe into lp:~maas-committers/maas/trunk

Proposed by Blake Rouse
Status: Merged
Approved by: Blake Rouse
Approved revision: no longer in the source branch.
Merged at revision: 4884
Proposed branch: lp:~blake-rouse/maas/external-dhcp-probe
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 1353 lines (+240/-688)
12 files modified
Makefile (+1/-2)
buildout.cfg (+0/-2)
src/maasserver/rpc/rackcontrollers.py (+28/-47)
src/maasserver/rpc/regionservice.py (+5/-18)
src/maasserver/rpc/tests/test_rackcontrollers.py (+54/-1)
src/maasserver/rpc/tests/test_regionservice.py (+36/-90)
src/provisioningserver/dhcp/detect.py (+4/-127)
src/provisioningserver/dhcp/probe.py (+0/-41)
src/provisioningserver/dhcp/tests/test_detect.py (+4/-213)
src/provisioningserver/pserv_services/dhcp_probe_service.py (+36/-47)
src/provisioningserver/pserv_services/tests/test_dhcp_probe_service.py (+68/-77)
src/provisioningserver/rpc/region.py (+4/-23)
To merge this branch: bzr merge lp:~blake-rouse/maas/external-dhcp-probe
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+291145@code.launchpad.net

Commit message

Update the external_dhcp field on the VLAN for discovered DHCP servers. Remove code that is no longer be used for DHCP probe.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

This looks like a welcome simplification. What's made it possible now?

How is rackd given permission to access the necessary port?

I think you can also remove p.dhcp.detect.process_request() and its imports.

Good stuff though.

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

Thanks for the review. I fixed you inline comment.

Believe it or not the bulk of this code could have been removed back in 1.7. When MAAS was switched from celery to RPC most of this code stopped being used. I have tested this on a installed system and MAAS has permission. Really I didn't change any of the probing code, I just removed what is no longer used.

Revision history for this message
Gavin Panella (allenap) wrote :

Tip top.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'Makefile'
--- Makefile 2016-03-28 13:54:47 +0000
+++ Makefile 2016-04-07 13:22:34 +0000
@@ -54,7 +54,6 @@
54 bin/buildout \54 bin/buildout \
55 bin/database \55 bin/database \
56 bin/maas \56 bin/maas \
57 bin/maas-probe-dhcp \
58 bin/maas-rack \57 bin/maas-rack \
59 bin/maas-region \58 bin/maas-region \
60 bin/twistd.rack \59 bin/twistd.rack \
@@ -135,7 +134,7 @@
135 $(buildout) install testing-test134 $(buildout) install testing-test
136 @touch --no-create $@135 @touch --no-create $@
137136
138bin/maas-probe-dhcp bin/maas-rack bin/twistd.rack: \137bin/maas-rack bin/twistd.rack: \
139 bin/buildout buildout.cfg versions.cfg setup.py138 bin/buildout buildout.cfg versions.cfg setup.py
140 $(buildout) install rack139 $(buildout) install rack
141 @touch --no-create $@140 @touch --no-create $@
142141
=== modified file 'buildout.cfg'
--- buildout.cfg 2016-03-28 13:54:47 +0000
+++ buildout.cfg 2016-04-07 13:22:34 +0000
@@ -189,13 +189,11 @@
189recipe = zc.recipe.egg189recipe = zc.recipe.egg
190eggs =190eggs =
191entry-points =191entry-points =
192 maas-probe-dhcp=provisioningserver.dhcp.probe:main
193 maas-rack=provisioningserver.__main__:main192 maas-rack=provisioningserver.__main__:main
194 twistd.rack=twisted.scripts.twistd:run193 twistd.rack=twisted.scripts.twistd:run
195extra-paths =194extra-paths =
196 ${common:extra-paths}195 ${common:extra-paths}
197scripts =196scripts =
198 maas-probe-dhcp
199 maas-rack197 maas-rack
200 twistd.rack198 twistd.rack
201initialization =199initialization =
202200
=== modified file 'src/maasserver/rpc/rackcontrollers.py'
--- src/maasserver/rpc/rackcontrollers.py 2016-03-28 13:54:47 +0000
+++ src/maasserver/rpc/rackcontrollers.py 2016-04-07 13:22:34 +0000
@@ -16,10 +16,10 @@
16from maasserver import worker_user16from maasserver import worker_user
17from maasserver.enum import NODE_TYPE17from maasserver.enum import NODE_TYPE
18from maasserver.models import (18from maasserver.models import (
19 Interface,
20 Node,19 Node,
21 NodeGroupToRackController,20 NodeGroupToRackController,
22 RackController,21 RackController,
22 StaticIPAddress,
23)23)
24from maasserver.models.node import typecast_node24from maasserver.models.node import typecast_node
25from maasserver.utils.orm import transactional25from maasserver.utils.orm import transactional
@@ -163,52 +163,33 @@
163163
164164
165@transactional165@transactional
166def update_foreign_dhcp_ip(cluster_uuid, interface_name, foreign_dhcp_ip):166def update_foreign_dhcp(system_id, interface_name, dhcp_ip=None):
167 """Update the foreign_dhcp_ip field of a given interface on a cluster.167 """Update the external_dhcp field of the VLAN for the interface.
168168
169 Note: We do this through an update, not a read/modify/write.169 :param system_id: Rack controller system_id.
170 Updating NodeGroupInterface client-side may inadvertently trigger170 :param interface_name: The name of the interface.
171 Django signals that cause a rewrite of the DHCP config, plus restart171 :param dhcp_ip: The IP address of the responding DHCP server.
172 of the DHCP server. The inadvertent triggering has been known to172 """
173 happen because of race conditions between read/modify/write173 rack_controller = RackController.objects.get(system_id=system_id)
174 transactions that were enabled by Django defaulting to, and being174 interface = rack_controller.interface_set.filter(
175 designed for, the READ COMMITTED isolation level; the ORM writing175 name=interface_name).select_related("vlan").first()
176 back even unmodified fields; and GenericIPAddressField's default176 if interface is not None:
177 value being prone to problems where NULL is sometimes represented as177 if dhcp_ip is not None:
178 None, sometimes as an empty string, and the difference being enough178 sip = StaticIPAddress.objects.filter(ip=dhcp_ip).first()
179 to convince the signal machinery that these fields have changed when179 if sip is not None:
180 in fact they have not.180 # Check that its not an IP address of a rack controller
181181 # providing that DHCP service.
182 :param cluster_uuid: Cluster's UUID.182 rack_interfaces_serving_dhcp = sip.interface_set.filter(
183 :param interface_name: The name of the cluster interface on which the183 node__node_type__in=[
184 foreign DHCP server was (or wasn't) discovered.184 NODE_TYPE.RACK_CONTROLLER,
185 :param foreign_dhcp_ip: IP address of foreign DCHP server, if any.185 NODE_TYPE.REGION_AND_RACK_CONTROLLER],
186 """186 vlan__dhcp_on=True)
187 # XXX 2016-01-20 blake_r - Currently no where to place this information.187 if rack_interfaces_serving_dhcp.exists():
188 # Need to add to the model to store this information.188 # Not external. It's a MAAS DHCP server.
189 pass189 dhcp_ip = None
190190 if interface.vlan.external_dhcp != dhcp_ip:
191191 interface.vlan.external_dhcp = dhcp_ip
192@transactional192 interface.vlan.save()
193def get_rack_controllers_interfaces_as_dicts(system_id):
194 """Return all the interfaces on a given rack controller as a list of dicts.
195
196 :return: A list of dicts in the form {'name': interface.name,
197 'interface': interface.interface, 'ip': interface.ip}, one dict per
198 interface on the cluster.
199 """
200 interfaces = Interface.objects.filter(node__system_id=system_id)
201 # XXX 2016-01-20 blake_r - Currently not passing any IP address as it now
202 # should take a list of IP addresses and not just one IP address. To make
203 # it work for now nothing its filtered out.
204 return [
205 {
206 'name': interface.name,
207 'interface': interface.name,
208 'ip': '',
209 }
210 for interface in interfaces
211 ]
212193
213194
214@synchronous195@synchronous
215196
=== modified file 'src/maasserver/rpc/regionservice.py'
--- src/maasserver/rpc/regionservice.py 2016-03-31 23:34:55 +0000
+++ src/maasserver/rpc/regionservice.py 2016-04-07 13:22:34 +0000
@@ -345,32 +345,19 @@
345 return succeed({})345 return succeed({})
346346
347 @region.ReportForeignDHCPServer.responder347 @region.ReportForeignDHCPServer.responder
348 def report_foreign_dhcp_server(self, cluster_uuid, interface_name,348 def report_foreign_dhcp_server(
349 foreign_dhcp_ip):349 self, system_id, interface_name, dhcp_ip=None):
350 """report_foreign_dhcp_server()350 """report_foreign_dhcp_server()
351351
352 Implementation of352 Implementation of
353 :py:class:`~provisioningserver.rpc.region.SendEvent`.353 :py:class:`~provisioningserver.rpc.region.ReportForeignDHCPServer`.
354 """354 """
355 d = deferToDatabase(355 d = deferToDatabase(
356 rackcontrollers.update_foreign_dhcp_ip,356 rackcontrollers.update_foreign_dhcp,
357 cluster_uuid, interface_name, foreign_dhcp_ip)357 system_id, interface_name, dhcp_ip)
358 d.addCallback(lambda _: {})358 d.addCallback(lambda _: {})
359 return d359 return d
360360
361 @region.GetClusterInterfaces.responder
362 def get_cluster_interfaces(self, cluster_uuid):
363 """get_cluster_interfaces()
364
365 Implementation of
366 :py:class:`~provisioningserver.rpc.region.GetClusterInterfaces`.
367 """
368 d = deferToDatabase(
369 rackcontrollers.get_rack_controllers_interfaces_as_dicts,
370 cluster_uuid)
371 d.addCallback(lambda interfaces: {'interfaces': interfaces})
372 return d
373
374 @region.CreateNode.responder361 @region.CreateNode.responder
375 def create_node(self, architecture, power_type, power_parameters,362 def create_node(self, architecture, power_type, power_parameters,
376 mac_addresses, domain=None, hostname=None):363 mac_addresses, domain=None, hostname=None):
377364
=== modified file 'src/maasserver/rpc/tests/test_rackcontrollers.py'
--- src/maasserver/rpc/tests/test_rackcontrollers.py 2016-03-28 13:54:47 +0000
+++ src/maasserver/rpc/tests/test_rackcontrollers.py 2016-04-07 13:22:34 +0000
@@ -10,7 +10,11 @@
10from django.db import IntegrityError10from django.db import IntegrityError
11from fixtures import FakeLogger11from fixtures import FakeLogger
12from maasserver import worker_user12from maasserver import worker_user
13from maasserver.enum import NODE_TYPE13from maasserver.enum import (
14 INTERFACE_TYPE,
15 IPADDRESS_TYPE,
16 NODE_TYPE,
17)
14from maasserver.models import (18from maasserver.models import (
15 Node,19 Node,
16 NodeGroupToRackController,20 NodeGroupToRackController,
@@ -20,6 +24,7 @@
20 handle_upgrade,24 handle_upgrade,
21 register_new_rackcontroller,25 register_new_rackcontroller,
22 register_rackcontroller,26 register_rackcontroller,
27 update_foreign_dhcp,
23 update_interfaces,28 update_interfaces,
24)29)
25from maasserver.testing.factory import factory30from maasserver.testing.factory import factory
@@ -248,6 +253,54 @@
248 hostname, logger.output.strip())253 hostname, logger.output.strip())
249254
250255
256class TestUpdateForeignDHCP(MAASServerTestCase):
257
258 def test__doesnt_fail_if_interface_missing(self):
259 rack_controller = factory.make_RackController()
260 # No error should be raised.
261 update_foreign_dhcp(
262 rack_controller.system_id, factory.make_name("eth"), None)
263
264 def test__clears_external_dhcp_on_vlan(self):
265 rack_controller = factory.make_RackController(interface=False)
266 interface = factory.make_Interface(
267 INTERFACE_TYPE.PHYSICAL, node=rack_controller)
268 interface.vlan.external_dhcp = factory.make_ip_address()
269 interface.vlan.save()
270 update_foreign_dhcp(
271 rack_controller.system_id, interface.name, None)
272 self.assertIsNone(reload_object(interface.vlan).external_dhcp)
273
274 def test__sets_external_dhcp_when_not_managed_vlan(self):
275 rack_controller = factory.make_RackController(interface=False)
276 interface = factory.make_Interface(
277 INTERFACE_TYPE.PHYSICAL, node=rack_controller)
278 dhcp_ip = factory.make_ip_address()
279 update_foreign_dhcp(
280 rack_controller.system_id, interface.name, dhcp_ip)
281 self.assertEquals(
282 dhcp_ip, reload_object(interface.vlan).external_dhcp)
283
284 def test__clears_external_dhcp_when_managed_vlan(self):
285 rack_controller = factory.make_RackController(interface=False)
286 fabric = factory.make_Fabric()
287 vlan = fabric.get_default_vlan()
288 interface = factory.make_Interface(
289 INTERFACE_TYPE.PHYSICAL, node=rack_controller, vlan=vlan)
290 subnet = factory.make_Subnet()
291 dhcp_ip = factory.pick_ip_in_Subnet(subnet)
292 vlan.dhcp_on = True
293 vlan.primary_rack = rack_controller
294 vlan.external_dhcp = dhcp_ip
295 vlan.save()
296 factory.make_StaticIPAddress(
297 alloc_type=IPADDRESS_TYPE.STICKY, ip=dhcp_ip,
298 subnet=subnet, interface=interface)
299 update_foreign_dhcp(
300 rack_controller.system_id, interface.name, dhcp_ip)
301 self.assertIsNone(reload_object(interface.vlan).external_dhcp)
302
303
251class TestUpdateInterfaces(MAASServerTestCase):304class TestUpdateInterfaces(MAASServerTestCase):
252305
253 def test__calls_update_interfaces_on_rack_controller(self):306 def test__calls_update_interfaces_on_rack_controller(self):
254307
=== modified file 'src/maasserver/rpc/tests/test_regionservice.py'
--- src/maasserver/rpc/tests/test_regionservice.py 2016-03-31 23:34:55 +0000
+++ src/maasserver/rpc/tests/test_regionservice.py 2016-04-07 13:22:34 +0000
@@ -121,7 +121,6 @@
121 GetBootConfig,121 GetBootConfig,
122 GetBootSources,122 GetBootSources,
123 GetBootSourcesV2,123 GetBootSourcesV2,
124 GetClusterInterfaces,
125 GetProxies,124 GetProxies,
126 Identify,125 Identify,
127 ListNodePowerParameters,126 ListNodePowerParameters,
@@ -129,6 +128,7 @@
129 RegisterEventType,128 RegisterEventType,
130 RegisterRackController,129 RegisterRackController,
131 ReportBootImages,130 ReportBootImages,
131 ReportForeignDHCPServer,
132 RequestNodeInfoByMACAddress,132 RequestNodeInfoByMACAddress,
133 SendEvent,133 SendEvent,
134 SendEventMACAddress,134 SendEventMACAddress,
@@ -2590,101 +2590,47 @@
2590 # If the RPC service is down, _get_addresses() returns nothing.2590 # If the RPC service is down, _get_addresses() returns nothing.
2591 self.assertItemsEqual([], service._get_addresses())2591 self.assertItemsEqual([], service._get_addresses())
25922592
2593# NODE_GROUP_REMOVAL - blake_r - Fix.2593
2594#class TestRegionProtocol_ReportForeignDHCPServer(2594class TestRegionProtocol_ReportForeignDHCPServer(
2595# MAASTransactionServerTestCase):2595 MAASTransactionServerTestCase):
2596#2596
2597# def test_create_node_is_registered(self):2597 def test_is_registered(self):
2598# protocol = Region()
2599# responder = protocol.locateResponder(
2600# ReportForeignDHCPServer.commandName)
2601# self.assertIsNotNone(responder)
2602#
2603# @transactional
2604# def create_cluster_interface(self):
2605# cluster = factory.make_NodeGroup()
2606# return factory.make_NodeGroupInterface(cluster)
2607#
2608# @wait_for_reactor
2609# @inlineCallbacks
2610# def test_sets_foreign_dhcp_value(self):
2611# foreign_dhcp_ip = factory.make_ipv4_address()
2612# cluster_interface = yield deferToDatabase(
2613# self.create_cluster_interface)
2614# cluster = cluster_interface.nodegroup
2615#
2616# response = yield call_responder(
2617# Region(), ReportForeignDHCPServer,
2618# {
2619# 'cluster_uuid': cluster.uuid,
2620# 'interface_name': cluster_interface.name,
2621# 'foreign_dhcp_ip': foreign_dhcp_ip,
2622# })
2623#
2624# self.assertEqual({}, response)
2625# cluster_interface = yield deferToDatabase(
2626# transactional_reload_object, cluster_interface)
2627#
2628# self.assertEqual(
2629# foreign_dhcp_ip, cluster_interface.foreign_dhcp_ip)
2630#
2631# @wait_for_reactor
2632# @inlineCallbacks
2633# def test_does_not_trigger_update_signal(self):
2634# configure_dhcp = self.patch_autospec(dhcp, "configure_dhcp")
2635#
2636# foreign_dhcp_ip = factory.make_ipv4_address()
2637# cluster_interface = yield deferToDatabase(
2638# self.create_cluster_interface)
2639# cluster = cluster_interface.nodegroup
2640#
2641# response = yield call_responder(
2642# Region(), ReportForeignDHCPServer,
2643# {
2644# 'cluster_uuid': cluster.uuid,
2645# 'interface_name': cluster_interface.name,
2646# 'foreign_dhcp_ip': foreign_dhcp_ip,
2647# })
2648#
2649# self.assertEqual({}, response)
2650# self.assertThat(configure_dhcp, MockNotCalled())
2651
2652
2653class TestRegionProtocol_GetClusterInterfaces(MAASTransactionServerTestCase):
2654
2655 def test_create_node_is_registered(self):
2656 protocol = Region()2598 protocol = Region()
2657 responder = protocol.locateResponder(2599 responder = protocol.locateResponder(
2658 GetClusterInterfaces.commandName)2600 ReportForeignDHCPServer.commandName)
2659 self.assertIsNotNone(responder)2601 self.assertIsNotNone(responder)
26602602
2661 @transactional2603 @transactional
2662 def create_controller_and_interfaces(self):2604 def create_rack_interface(self):
2663 controller = factory.make_RackController()2605 rack_controller = factory.make_RackController(interface=False)
2664 for _ in range(3):2606 interface = factory.make_Interface(
2665 factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=controller)2607 INTERFACE_TYPE.PHYSICAL, node=rack_controller)
2666 interfaces = [2608 return rack_controller, interface
2609
2610 @transactional
2611 def get_vlan_for_interface(self, interface):
2612 return reload_object(interface.vlan)
2613
2614 @wait_for_reactor
2615 @inlineCallbacks
2616 def test_sets_external_dhcp_value(self):
2617 dhcp_ip = factory.make_ipv4_address()
2618 rack, interface = yield deferToDatabase(
2619 self.create_rack_interface)
2620
2621 response = yield call_responder(
2622 Region(), ReportForeignDHCPServer,
2667 {2623 {
2668 'name': interface.name,2624 'system_id': rack.system_id,
2669 'interface': interface.name,2625 'interface_name': interface.name,
2670 'ip': '',2626 'dhcp_ip': dhcp_ip,
2671 }2627 })
2672 for interface in controller.interface_set.all()]2628
2673 return controller, interfaces2629 self.assertEqual({}, response)
26742630 vlan = yield deferToDatabase(
2675 @wait_for_reactor2631 self.get_vlan_for_interface, interface)
2676 @inlineCallbacks2632 self.assertEqual(
2677 def test_returns_all_cluster_interfaces(self):2633 dhcp_ip, vlan.external_dhcp)
2678 controller, expected_interfaces = yield deferToDatabase(
2679 self.create_controller_and_interfaces)
2680
2681 response = yield call_responder(
2682 Region(), GetClusterInterfaces,
2683 {'cluster_uuid': controller.system_id})
2684
2685 self.assertIsNot(None, response)
2686 self.assertItemsEqual(
2687 expected_interfaces, response["interfaces"])
26882634
26892635
2690class TestRegionProtocol_CreateNode(MAASTransactionServerTestCase):2636class TestRegionProtocol_CreateNode(MAASTransactionServerTestCase):
26912637
=== modified file 'src/provisioningserver/dhcp/detect.py'
--- src/provisioningserver/dhcp/detect.py 2016-03-28 13:54:47 +0000
+++ src/provisioningserver/dhcp/detect.py 2016-04-07 13:22:34 +0000
@@ -1,3 +1,4 @@
1
1# Copyright 2013-2016 Canonical Ltd. This software is licensed under the2# Copyright 2013-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).3# GNU Affero General Public License version 3 (see the file LICENSE).
34
@@ -10,21 +11,10 @@
10from contextlib import contextmanager11from contextlib import contextmanager
11import errno12import errno
12import fcntl13import fcntl
13import http.client
14import json
15from random import randint14from random import randint
16import socket15import socket
17import struct16import struct
18from urllib.error import (
19 HTTPError,
20 URLError,
21)
2217
23from apiclient.maas_client import (
24 MAASClient,
25 MAASDispatcher,
26 MAASOAuth,
27)
28from provisioningserver.logger import get_maas_logger18from provisioningserver.logger import get_maas_logger
2919
3020
@@ -204,64 +194,10 @@
204 return receive_offers(transaction_id)194 return receive_offers(transaction_id)
205195
206196
207def process_request(client_func, *args, **kwargs):197def probe_interface(interface):
208 """Run a MAASClient query and check for common errors.
209
210 :return: None if there is an error, otherwise the decoded response body.
211 """
212 try:
213 response = client_func(*args, **kwargs)
214 except (HTTPError, URLError) as e:
215 maaslog.warning("Failed to contact region controller:\n%s", e)
216 return None
217 code = response.getcode()
218 if code != http.client.OK:
219 maaslog.error(
220 "Failed talking to region controller, it returned:\n%s\n%s",
221 code, response.read())
222 return None
223 try:
224 raw_data = response.read()
225 if len(raw_data) > 0:
226 data = json.loads(raw_data)
227 else:
228 return None
229 except ValueError as e:
230 maaslog.error(
231 "Failed to decode response from region controller:\n%s", e)
232 return None
233 return data
234
235
236def determine_cluster_interfaces(knowledge):
237 """Given server knowledge, determine network interfaces on this cluster.
238
239 :return: a list of tuples of (interface name, ip) for all interfaces.
240
241 :note: this uses an API call and not local probing because the
242 region controller has the definitive and final say in what does and
243 doesn't exist.
244 """
245 api_path = (
246 'api/2.0/nodegroups/%s/interfaces/' % knowledge['nodegroup_uuid'])
247 oauth = MAASOAuth(*knowledge['api_credentials'])
248 client = MAASClient(oauth, MAASDispatcher(), knowledge['maas_url'])
249 interfaces = process_request(client.get, api_path, 'list')
250 if interfaces is None:
251 return None
252
253 interface_names = sorted(
254 (interface['interface'], interface['ip'])
255 for interface in interfaces
256 if interface['interface'] != '')
257 return interface_names
258
259
260def probe_interface(interface, ip):
261 """Probe the given interface for DHCP servers.198 """Probe the given interface for DHCP servers.
262199
263 :param interface: interface as returned from determine_cluster_interfaces200 :param interface: interface name
264 :param ip: ip as returned from determine_cluster_interfaces
265 :return: A set of IP addresses of detected servers.201 :return: A set of IP addresses of detected servers.
266202
267 :note: Any servers running on the IP address of the local host are203 :note: Any servers running on the IP address of the local host are
@@ -287,63 +223,4 @@
287 "your cluster interfaces configuration.", interface)223 "your cluster interfaces configuration.", interface)
288 else:224 else:
289 raise225 raise
290 # Using servers.discard(ip) here breaks Mock in the tests, so226 return servers
291 # we're creating a copy of the set instead.
292 results = servers.difference([ip])
293 return results
294
295
296def update_region_controller(knowledge, interface, server):
297 """Update the region controller with the status of the probe.
298
299 :param knowledge: dictionary of server info
300 :param interface: name of interface, e.g. eth0
301 :param server: IP address of detected DHCP server, or None
302 """
303 api_path = 'api/2.0/nodegroups/%s/interfaces/%s/' % (
304 knowledge['nodegroup_uuid'], interface)
305 oauth = MAASOAuth(*knowledge['api_credentials'])
306 client = MAASClient(oauth, MAASDispatcher(), knowledge['maas_url'])
307 if server is None:
308 server = ''
309 process_request(
310 client.post, api_path, 'report_foreign_dhcp', foreign_dhcp_ip=server)
311
312
313def periodic_probe_task(api_knowledge):
314 """Probe for DHCP servers and set NodeGroupInterface.foriegn_dhcp.
315
316 This should be run periodically so that the database has an up-to-date
317 view of any rogue DHCP servers on the network.
318
319 NOTE: This uses blocking I/O with sequential polling of interfaces, and
320 hence doesn't scale well. It's a future improvement to make
321 to throw it in parallel threads or async I/O.
322
323 :param api_knowledge: A dict of the information needed to be able to
324 make requests to the region's REST API.
325 """
326 # Determine all the active interfaces on this cluster (nodegroup).
327 interfaces = determine_cluster_interfaces(api_knowledge)
328 if interfaces is None:
329 maaslog.info("No interfaces on cluster, not probing DHCP.")
330 return
331
332 # Iterate over interfaces and probe each one.
333 for interface, ip in interfaces:
334 try:
335 servers = probe_interface(interface, ip)
336 except socket.error:
337 maaslog.error(
338 "Failed to probe sockets; did you configure authbind as per "
339 "HACKING.txt?")
340 return
341 else:
342 if len(servers) > 0:
343 # Only send one, if it gets cleared out then the
344 # next detection pass will send a different one, if it
345 # still exists.
346 update_region_controller(
347 api_knowledge, interface, servers.pop())
348 else:
349 update_region_controller(api_knowledge, interface, None)
350227
=== removed file 'src/provisioningserver/dhcp/probe.py'
--- src/provisioningserver/dhcp/probe.py 2015-12-01 18:12:59 +0000
+++ src/provisioningserver/dhcp/probe.py 1970-01-01 00:00:00 +0000
@@ -1,41 +0,0 @@
1#!/usr/bin/env python2.7
2# Copyright 2013-2015 Canonical Ltd. This software is licensed under the
3# GNU Affero General Public License version 3 (see the file LICENSE).
4
5"""Probe network on given network interface for a DHCP server.
6
7This needs to be run as root, in order to be allowed to broadcast on the
8BOOTP port.
9
10Exit code is zero ("success") if no servers were detected, or the number of
11DHCP servers that were found.
12"""
13
14import argparse
15from sys import exit
16
17from provisioningserver.dhcp.detect import probe_dhcp
18
19
20argument_parser = argparse.ArgumentParser(description=__doc__)
21
22
23def main():
24 argument_parser.add_argument(
25 'interface',
26 help="Probe network on this network interface.")
27
28 args = argument_parser.parse_args()
29
30 servers = probe_dhcp(args.interface)
31
32 num_servers = len(servers)
33 if num_servers == 0:
34 print("No DHCP servers detected.")
35 exit(0)
36 else:
37 print("DHCP servers detected: %s" % ', '.join(sorted(servers)))
38 exit(num_servers)
39
40if __name__ == "__main__":
41 main()
420
=== modified file 'src/provisioningserver/dhcp/tests/test_detect.py'
--- src/provisioningserver/dhcp/tests/test_detect.py 2016-03-28 13:54:47 +0000
+++ src/provisioningserver/dhcp/tests/test_detect.py 2016-04-07 13:22:34 +0000
@@ -7,39 +7,25 @@
77
8import errno8import errno
9import fcntl9import fcntl
10import http.client
11import socket10import socket
12import textwrap
13import urllib.error
14import urllib.parse
15import urllib.request
1611
17from apiclient.maas_client import MAASClient
18from apiclient.testing.credentials import make_api_credentials
19from fixtures import FakeLogger
20from maastesting.factory import factory12from maastesting.factory import factory
21from maastesting.matchers import MockCalledOnceWith
22from maastesting.testcase import MAASTestCase13from maastesting.testcase import MAASTestCase
23import mock14import mock
24from mock import sentinel
25from provisioningserver.dhcp.detect import (15from provisioningserver.dhcp.detect import (
26 BOOTP_CLIENT_PORT,16 BOOTP_CLIENT_PORT,
27 BOOTP_SERVER_PORT,17 BOOTP_SERVER_PORT,
28 determine_cluster_interfaces,
29 DHCPDiscoverPacket,18 DHCPDiscoverPacket,
30 DHCPOfferPacket,19 DHCPOfferPacket,
31 get_interface_IP,20 get_interface_IP,
32 get_interface_MAC,21 get_interface_MAC,
33 make_transaction_ID,22 make_transaction_ID,
34 periodic_probe_task,
35 probe_interface,23 probe_interface,
36 receive_offers,24 receive_offers,
37 request_dhcp,25 request_dhcp,
38 udp_socket,26 udp_socket,
39 update_region_controller,
40)27)
41import provisioningserver.dhcp.detect as detect_module28import provisioningserver.dhcp.detect as detect_module
42from provisioningserver.testing.config import ClusterConfigurationFixture
43from provisioningserver.testing.testcase import PservTestCase29from provisioningserver.testing.testcase import PservTestCase
4430
4531
@@ -306,230 +292,35 @@
306 receive_offers, factory.make_bytes(4))292 receive_offers, factory.make_bytes(4))
307293
308294
309class MockResponse:
310 # This implements just enough to look lke a urllib2 response object.
311 def __init__(self, code=None, response=None):
312 if code is None:
313 code = http.client.OK
314 self.code = code
315 if response is None:
316 response = ""
317 self.response = response
318
319 def getcode(self):
320 return self.code
321
322 def read(self):
323 return self.response
324
325
326class TestPeriodicTask(PservTestCase):295class TestPeriodicTask(PservTestCase):
327296
328 def setUp(self):
329 # Initialise the knowledge cache.
330 super(TestPeriodicTask, self).setUp()
331 self.maaslog = self.useFixture(FakeLogger("maas.dhcp.detect"))
332 uuid = factory.make_UUID()
333 maas_url = 'http://%s.example.com/%s/' % (
334 factory.make_name('host'),
335 factory.make_string(),
336 )
337 api_credentials = make_api_credentials()
338 self.useFixture(ClusterConfigurationFixture(maas_url=maas_url))
339 self.knowledge = dict(
340 nodegroup_uuid=uuid,
341 api_credentials=api_credentials,
342 maas_url=maas_url)
343
344 def make_fake_interfaces_response(self, interfaces_pairs):
345 stanzas = []
346 for interfaces_pair in interfaces_pairs:
347 stanza = textwrap.dedent("""
348 {{
349 "ip_range_high": null,
350 "ip_range_low": null,
351 "broadcast_ip": null,
352 "ip": "{1}",
353 "subnet_mask": "255.255.255.0",
354 "management": 0,
355 "interface": "{0}"
356 }}""").format(*interfaces_pair)
357 stanzas.append(stanza)
358 interfaces_json = "["
359 interfaces_json += ",".join(stanzas)
360 interfaces_json += "]"
361 return interfaces_json
362
363 def patch_fake_interfaces_list(self, interfaces_pairs):
364 # Set up the api client to return a fake set of interfaces.
365 # Determine_cluster_interfaces calls the API to discover what
366 # interfaces are available, so any test code that calls it
367 # should first call this helper to set up the required fake response.
368 interfaces_json = self.make_fake_interfaces_response(interfaces_pairs)
369 self.patch(MAASClient, 'get').return_value = MockResponse(
370 http.client.OK, interfaces_json)
371
372 def test_determine_cluster_interfaces_returns_interface_names(self):
373 eth0_addr = factory.make_ipv4_address()
374 wlan0_addr = factory.make_ipv4_address()
375 self.patch_fake_interfaces_list(
376 [("eth0", eth0_addr), ("wlan0", wlan0_addr)])
377 self.assertEqual(
378 [("eth0", eth0_addr), ("wlan0", wlan0_addr)],
379 determine_cluster_interfaces(self.knowledge))
380
381 def test_probe_interface_returns_empty_set_when_nothing_detected(self):297 def test_probe_interface_returns_empty_set_when_nothing_detected(self):
382 eth0_addr = factory.make_ipv4_address()
383 self.patch_fake_interfaces_list([("eth0", eth0_addr)])
384 self.patch(detect_module, 'probe_dhcp').return_value = set()298 self.patch(detect_module, 'probe_dhcp').return_value = set()
385 interfaces = determine_cluster_interfaces(self.knowledge)299 results = probe_interface("eth0")
386 results = probe_interface(*interfaces[0])
387 self.assertEqual(set(), results)300 self.assertEqual(set(), results)
388301
389 def test_probe_interface_returns_empty_set_when_IP_missing(self):302 def test_probe_interface_returns_empty_set_when_IP_missing(self):
390 # If the interface being probed has no IP address, the303 # If the interface being probed has no IP address, the
391 # request_dhcr() method will raise IOError with errno 99. Make304 # request_dhcr() method will raise IOError with errno 99. Make
392 # sure this is caught and ignored.305 # sure this is caught and ignored.
393 eth0_addr = factory.make_ipv4_address()
394 self.patch_fake_interfaces_list([("eth0", eth0_addr)])
395 ioerror = IOError(306 ioerror = IOError(
396 errno.EADDRNOTAVAIL, "Cannot assign requested address")307 errno.EADDRNOTAVAIL, "Cannot assign requested address")
397 self.patch(fcntl, 'ioctl').side_effect = ioerror308 self.patch(fcntl, 'ioctl').side_effect = ioerror
398 interfaces = determine_cluster_interfaces(self.knowledge)309 results = probe_interface("eth0")
399 results = probe_interface(*interfaces[0])
400 self.assertEqual(set(), results)310 self.assertEqual(set(), results)
401311
402 def test_probe_interface_returns_empty_set_when_device_missing(self):312 def test_probe_interface_returns_empty_set_when_device_missing(self):
403 # If the interface being probed does not exist, the313 # If the interface being probed does not exist, the
404 # request_dhcp() method will raise IOError with errno 19. Make314 # request_dhcp() method will raise IOError with errno 19. Make
405 # sure this is caught and ignored.315 # sure this is caught and ignored.
406 eth0_addr = factory.make_ipv4_address()
407 self.patch_fake_interfaces_list([("eth0", eth0_addr)])
408 ioerror = IOError(errno.ENODEV, "No such device")316 ioerror = IOError(errno.ENODEV, "No such device")
409 self.patch(fcntl, 'ioctl').side_effect = ioerror317 self.patch(fcntl, 'ioctl').side_effect = ioerror
410 interfaces = determine_cluster_interfaces(self.knowledge)318 results = probe_interface("eth0")
411 results = probe_interface(*interfaces[0])
412 self.assertEqual(set(), results)319 self.assertEqual(set(), results)
413320
414 def test_probe_interface_returns_populated_set(self):321 def test_probe_interface_returns_populated_set(self):
415 # Test that the detected DHCP server is returned.322 # Test that the detected DHCP server is returned.
416 eth0_addr = factory.make_ipv4_address()
417 self.patch_fake_interfaces_list([("eth0", eth0_addr)])
418 self.patch(323 self.patch(
419 detect_module, 'probe_dhcp').return_value = {'10.2.2.2'}324 detect_module, 'probe_dhcp').return_value = {'10.2.2.2'}
420 interfaces = determine_cluster_interfaces(self.knowledge)325 results = probe_interface("eth0")
421 results = probe_interface(*interfaces[0])
422 self.assertEqual({'10.2.2.2'}, results)326 self.assertEqual({'10.2.2.2'}, results)
423
424 def test_probe_interface_filters_interface_own_ip(self):
425 # Test that the interface shows the detected DHCP server except
426 # if it is the same IP as the interface's.
427 eth0_addr = factory.make_ipv4_address()
428 self.patch_fake_interfaces_list([("eth0", eth0_addr)])
429 detected_dhcp = eth0_addr
430 self.patch(detect_module, 'probe_dhcp').return_value = {detected_dhcp}
431 interfaces = determine_cluster_interfaces(self.knowledge)
432 results = probe_interface(*interfaces[0])
433 self.assertEqual(set(), results)
434
435 def test_determine_cluster_interfaces_catchs_HTTPError_in_MAASClient(self):
436 self.patch(MAASClient, 'get').side_effect = urllib.error.HTTPError(
437 sentinel.url, sentinel.code, sentinel.msg, sentinel.hdrs, None)
438 determine_cluster_interfaces(self.knowledge)
439 self.assertIn(
440 "Failed to contact region controller:", self.maaslog.output)
441
442 def test_determine_cluster_interfaces_catches_URLError_in_MAASClient(self):
443 self.patch(MAASClient, 'get').side_effect = urllib.error.URLError(
444 sentinel.arg1)
445 determine_cluster_interfaces(self.knowledge)
446 self.assertIn(
447 "Failed to contact region controller:", self.maaslog.output)
448
449 def test_determine_cluster_interfaces_catches_non_OK_response(self):
450 self.patch(MAASClient, 'get').return_value = MockResponse(
451 http.client.NOT_FOUND, "error text")
452 determine_cluster_interfaces(self.knowledge)
453 self.assertIn(
454 "Failed talking to region controller, it returned:",
455 self.maaslog.output)
456
457 def test_update_region_controller_sets_detected_dhcp(self):
458 mocked_post = self.patch(MAASClient, 'post')
459 mocked_post.return_value = MockResponse()
460 detected_server = factory.make_ipv4_address()
461 update_region_controller(self.knowledge, "eth0", detected_server)
462 uuid = self.knowledge['nodegroup_uuid']
463 self.assertThat(mocked_post, MockCalledOnceWith(
464 'api/2.0/nodegroups/%s/interfaces/eth0/' % uuid,
465 'report_foreign_dhcp', foreign_dhcp_ip=detected_server))
466
467 def test_update_region_controller_clears_detected_dhcp(self):
468 mocked_post = self.patch(MAASClient, 'post')
469 mocked_post.return_value = MockResponse()
470 detected_server = None
471 update_region_controller(self.knowledge, "eth0", detected_server)
472 uuid = self.knowledge['nodegroup_uuid']
473 self.assertThat(mocked_post, MockCalledOnceWith(
474 'api/2.0/nodegroups/%s/interfaces/eth0/' % uuid,
475 'report_foreign_dhcp', foreign_dhcp_ip=''))
476
477 def test_update_region_controller_catches_HTTPError_in_MAASClient(self):
478 self.patch(MAASClient, 'post').side_effect = urllib.error.HTTPError(
479 sentinel.url, sentinel.code, sentinel.msg, sentinel.hdrs, None)
480 update_region_controller(self.knowledge, "eth0", None)
481 self.assertIn(
482 "Failed to contact region controller:", self.maaslog.output)
483
484 def test_update_region_controller_catches_URLError_in_MAASClient(self):
485 self.patch(MAASClient, 'post').side_effect = urllib.error.URLError(
486 sentinel.arg1)
487 update_region_controller(self.knowledge, "eth0", None)
488 self.assertIn(
489 "Failed to contact region controller:", self.maaslog.output)
490
491 def test_update_region_controller_catches_non_OK_response(self):
492 mock_response = MockResponse(http.client.NOT_FOUND, "error text")
493 self.patch(MAASClient, 'post').return_value = mock_response
494 update_region_controller(self.knowledge, "eth0", None)
495 self.assertIn(
496 "Failed talking to region controller, it returned:",
497 self.maaslog.output)
498
499 def test_periodic_probe_task_exits_if_no_interfaces(self):
500 mocked = self.patch(detect_module, 'probe_interface')
501 self.patch(
502 detect_module, 'determine_cluster_interfaces').return_value = None
503 periodic_probe_task(self.knowledge)
504 self.assertFalse(mocked.called)
505
506 def test_periodic_probe_task_updates_region_with_detected_server(self):
507 eth0_addr = factory.make_ipv4_address()
508 wlan0_addr = factory.make_ipv4_address()
509 detected_server = factory.make_ipv4_address()
510 self.patch_fake_interfaces_list(
511 [("eth0", eth0_addr), ("wlan0", wlan0_addr)])
512 self.patch(
513 detect_module, 'probe_dhcp').return_value = {detected_server}
514 mocked_update = self.patch(detect_module, 'update_region_controller')
515 periodic_probe_task(self.knowledge)
516 calls = [
517 mock.call(self.knowledge, 'eth0', detected_server),
518 mock.call(self.knowledge, 'wlan0', detected_server),
519 ]
520 mocked_update.assert_has_calls(calls, any_order=True)
521
522 def test_periodic_probe_task_updates_region_with_no_detected_server(self):
523 eth0_addr = factory.make_ipv4_address()
524 wlan0_addr = factory.make_ipv4_address()
525 self.patch_fake_interfaces_list(
526 [("eth0", eth0_addr), ("wlan0", wlan0_addr)])
527 self.patch(
528 detect_module, 'probe_dhcp').return_value = set()
529 mocked_update = self.patch(detect_module, 'update_region_controller')
530 periodic_probe_task(self.knowledge)
531 calls = [
532 mock.call(self.knowledge, 'eth0', None),
533 mock.call(self.knowledge, 'wlan0', None),
534 ]
535 mocked_update.assert_has_calls(calls, any_order=True)
536327
=== modified file 'src/provisioningserver/pserv_services/dhcp_probe_service.py'
--- src/provisioningserver/pserv_services/dhcp_probe_service.py 2016-03-28 13:54:47 +0000
+++ src/provisioningserver/pserv_services/dhcp_probe_service.py 2016-04-07 13:22:34 +0000
@@ -14,19 +14,14 @@
14from provisioningserver.dhcp.detect import probe_interface14from provisioningserver.dhcp.detect import probe_interface
15from provisioningserver.logger.log import get_maas_logger15from provisioningserver.logger.log import get_maas_logger
16from provisioningserver.rpc.exceptions import NoConnectionsAvailable16from provisioningserver.rpc.exceptions import NoConnectionsAvailable
17from provisioningserver.rpc.region import (17from provisioningserver.rpc.region import ReportForeignDHCPServer
18 GetClusterInterfaces,18from provisioningserver.utils.network import get_all_interfaces_definition
19 ReportForeignDHCPServer,
20)
21from provisioningserver.utils.twisted import (19from provisioningserver.utils.twisted import (
22 pause,20 pause,
23 retries,21 retries,
24)22)
25from twisted.application.internet import TimerService23from twisted.application.internet import TimerService
26from twisted.internet.defer import (24from twisted.internet.defer import inlineCallbacks
27 inlineCallbacks,
28 returnValue,
29)
30from twisted.internet.threads import deferToThread25from twisted.internet.threads import deferToThread
31from twisted.protocols.amp import UnhandledCommand26from twisted.protocols.amp import UnhandledCommand
3227
@@ -35,12 +30,11 @@
3530
3631
37class DHCPProbeService(TimerService, object):32class DHCPProbeService(TimerService, object):
38 """Service to probe for DHCP servers on this cluster's network.33 """Service to probe for DHCP servers on the rack controller interface's.
3934
40 Built on top of Twisted's `TimerService`.35 Built on top of Twisted's `TimerService`.
4136
42 :param reactor: An `IReactor` instance.37 :param reactor: An `IReactor` instance.
43 :param cluster_uuid: This cluster's UUID.
44 """38 """
4539
46 check_interval = timedelta(minutes=10).total_seconds()40 check_interval = timedelta(minutes=10).total_seconds()
@@ -52,48 +46,44 @@
52 self.clock = reactor46 self.clock = reactor
53 self.client_service = client_service47 self.client_service = client_service
5448
55 @inlineCallbacks49 def _get_interfaces(self):
56 def _get_cluster_interfaces(self, client):50 """Return the interfaces for this rack controller."""
57 """Return the interfaces for this cluster."""51 d = deferToThread(get_all_interfaces_definition)
58 try:52 d.addCallback(lambda interfaces: [
59 response = yield client(53 name
60 GetClusterInterfaces, cluster_uuid=client.localIdent)54 for name, info in interfaces.items()
61 except UnhandledCommand:55 if info["enabled"]
62 # The region hasn't been upgraded to support this method56 ])
63 # yet, so give up. Returning an empty dict means that this57 return d
64 # run will end, since there are no interfaces to check.
65 maaslog.error(
66 "Unable to query region for interfaces: Region does not "
67 "support the GetClusterInterfaces RPC method.")
68 returnValue({})
69 else:
70 returnValue(response['interfaces'])
7158
72 @inlineCallbacks59 def _inform_region_of_dhcp(self, client, name, dhcp_ip):
73 def _inform_region_of_foreign_dhcp(self, client, name,60 """Tell the region about the DHCP server.
74 foreign_dhcp_ip):
75 """Tell the region that there's a rogue DHCP server.
7661
77 :param client: The RPC client to use.62 :param client: The RPC client to use.
78 :param name: The name of the network interface where the rogue63 :param name: The name of the network interface where the rogue
79 DHCP server was found.64 DHCP server was found.
80 :param foreign_dhcp_ip: The IP address of the rogue server.65 :param dhcp_ip: The IP address of the DHCP server.
81 """66 """
82 try:67
83 yield client(68 def eb_unhandled(failure):
84 ReportForeignDHCPServer, cluster_uuid=client.localIdent,69 failure.trap(UnhandledCommand)
85 interface_name=name, foreign_dhcp_ip=foreign_dhcp_ip)
86 except UnhandledCommand:
87 # Not a lot we can do here... The region doesn't support70 # Not a lot we can do here... The region doesn't support
88 # this method yet.71 # this method yet.
89 maaslog.error(72 maaslog.error(
90 "Unable to inform region of rogue DHCP server: the region "73 "Unable to inform region of DHCP server: the region "
91 "does not yet support the ReportForeignDHCPServer RPC "74 "does not yet support the ReportForeignDHCPServer RPC "
92 "method.")75 "method.")
9376
77 d = client(
78 ReportForeignDHCPServer, system_id=client.localIdent,
79 interface_name=name, dhcp_ip=dhcp_ip)
80 d.addErrback(eb_unhandled)
81 return d
82
94 @inlineCallbacks83 @inlineCallbacks
95 def probe_dhcp(self):84 def probe_dhcp(self):
96 """Find all the interfaces on this cluster and probe for DHCP servers.85 """Find all the interfaces on this rack controller and probe for
86 DHCP servers.
97 """87 """
98 client = None88 client = None
99 for elapsed, remaining, wait in retries(15, 5, self.clock):89 for elapsed, remaining, wait in retries(15, 5, self.clock):
@@ -107,12 +97,11 @@
107 "Can't initiate DHCP probe, no RPC connection to region.")97 "Can't initiate DHCP probe, no RPC connection to region.")
108 return98 return
10999
110 cluster_interfaces = yield self._get_cluster_interfaces(client)
111 # Iterate over interfaces and probe each one.100 # Iterate over interfaces and probe each one.
112 for interface in cluster_interfaces:101 interfaces = yield self._get_interfaces()
102 for interface in interfaces:
113 try:103 try:
114 servers = yield deferToThread(104 servers = yield deferToThread(probe_interface, interface)
115 probe_interface, interface['interface'], interface['ip'])
116 except socket.error:105 except socket.error:
117 maaslog.error(106 maaslog.error(
118 "Failed to probe sockets; did you configure authbind as "107 "Failed to probe sockets; did you configure authbind as "
@@ -123,11 +112,11 @@
123 # Only send one, if it gets cleared out then the112 # Only send one, if it gets cleared out then the
124 # next detection pass will send a different one, if it113 # next detection pass will send a different one, if it
125 # still exists.114 # still exists.
126 yield self._inform_region_of_foreign_dhcp(115 yield self._inform_region_of_dhcp(
127 client, interface['name'], servers.pop())116 client, interface, servers.pop())
128 else:117 else:
129 yield self._inform_region_of_foreign_dhcp(118 yield self._inform_region_of_dhcp(
130 client, interface['name'], None)119 client, interface, None)
131120
132 @inlineCallbacks121 @inlineCallbacks
133 def try_probe_dhcp(self):122 def try_probe_dhcp(self):
@@ -136,7 +125,7 @@
136 yield self.probe_dhcp()125 yield self.probe_dhcp()
137 except Exception as error:126 except Exception as error:
138 maaslog.error(127 maaslog.error(
139 "Unable to probe for rogue DHCP servers: %s",128 "Unable to probe for DHCP servers: %s",
140 str(error))129 str(error))
141 else:130 else:
142 maaslog.debug("Finished periodic DHCP probe.")131 maaslog.debug("Finished periodic DHCP probe.")
143132
=== modified file 'src/provisioningserver/pserv_services/tests/test_dhcp_probe_service.py'
--- src/provisioningserver/pserv_services/tests/test_dhcp_probe_service.py 2016-03-28 13:54:47 +0000
+++ src/provisioningserver/pserv_services/tests/test_dhcp_probe_service.py 2016-04-07 13:22:34 +0000
@@ -11,10 +11,12 @@
11 get_mock_calls,11 get_mock_calls,
12 HasLength,12 HasLength,
13 MockCalledOnceWith,13 MockCalledOnceWith,
14 MockCallsMatch,
14 MockNotCalled,15 MockNotCalled,
15)16)
16from maastesting.testcase import MAASTwistedRunTest17from maastesting.testcase import MAASTwistedRunTest
17from mock import (18from mock import (
19 call,
18 Mock,20 Mock,
19 sentinel,21 sentinel,
20)22)
@@ -39,19 +41,9 @@
39 def patch_rpc_methods(self):41 def patch_rpc_methods(self):
40 fixture = self.useFixture(MockLiveClusterToRegionRPCFixture())42 fixture = self.useFixture(MockLiveClusterToRegionRPCFixture())
41 protocol, connecting = fixture.makeEventLoop(43 protocol, connecting = fixture.makeEventLoop(
42 region.GetClusterInterfaces, region.ReportForeignDHCPServer)44 region.ReportForeignDHCPServer)
43 return protocol, connecting45 return protocol, connecting
4446
45 def make_cluster_interface_values(self, ip=None):
46 """Return a dict describing a cluster interface."""
47 if ip is None:
48 ip = factory.make_ipv4_address()
49 return {
50 'name': factory.make_name('interface'),
51 'interface': factory.make_name('eth'),
52 'ip': ip,
53 }
54
55 def test_is_called_every_interval(self):47 def test_is_called_every_interval(self):
56 clock = Clock()48 clock = Clock()
57 service = DHCPProbeService(49 service = DHCPProbeService(
@@ -82,67 +74,47 @@
8274
83 def test_probe_is_initiated_in_new_thread(self):75 def test_probe_is_initiated_in_new_thread(self):
84 clock = Clock()76 clock = Clock()
85 interface = self.make_cluster_interface_values()77 interface_name = factory.make_name("eth")
86 rpc_service = Mock()78 interfaces = {
87 rpc_client = rpc_service.getClient.return_value79 interface_name: {
88 rpc_client.side_effect = [80 "enabled": True,
89 defer.succeed(dict(interfaces=[interface])),81 }
90 ]82 }
9183
92 # We could patch out 'periodic_probe_task' instead here but this
93 # is better because:
94 # 1. The former requires spinning the reactor again before being
95 # able to test the result.
96 # 2. This way there's no thread to clean up after the test.
97 deferToThread = self.patch(dhcp_probe_service, 'deferToThread')84 deferToThread = self.patch(dhcp_probe_service, 'deferToThread')
98 deferToThread.return_value = defer.succeed(None)85 deferToThread.side_effect = [
99 service = DHCPProbeService(86 defer.succeed(interfaces),
100 rpc_service, clock)87 defer.succeed(None),
88 ]
89 service = DHCPProbeService(Mock(), clock)
101 service.startService()90 service.startService()
102 self.assertThat(91 self.assertThat(
103 deferToThread, MockCalledOnceWith(92 deferToThread, MockCallsMatch(
104 dhcp_probe_service.probe_interface,93 call(dhcp_probe_service.get_all_interfaces_definition),
105 interface['interface'], interface['ip']))94 call(dhcp_probe_service.probe_interface, interface_name)))
106
107 @defer.inlineCallbacks
108 def test_exits_gracefully_if_cant_get_interfaces(self):
109 clock = Clock()
110 maaslog = self.patch(dhcp_probe_service, 'maaslog')
111
112 protocol, connecting = self.patch_rpc_methods()
113 self.addCleanup((yield connecting))
114
115 del protocol._commandDispatch[
116 region.GetClusterInterfaces.commandName]
117 rpc_service = Mock()
118 rpc_service.getClient.return_value = getRegionClient()
119 service = DHCPProbeService(
120 rpc_service, clock)
121 yield service.startService()
122 yield service.stopService()
123
124 self.assertThat(
125 maaslog.error, MockCalledOnceWith(
126 "Unable to query region for interfaces: Region does not "
127 "support the GetClusterInterfaces RPC method."))
12895
129 @defer.inlineCallbacks96 @defer.inlineCallbacks
130 def test_exits_gracefully_if_cant_report_foreign_dhcp_server(self):97 def test_exits_gracefully_if_cant_report_foreign_dhcp_server(self):
131 clock = Clock()98 clock = Clock()
99 interface_name = factory.make_name("eth")
100 interfaces = {
101 interface_name: {
102 "enabled": True,
103 }
104 }
105
132 maaslog = self.patch(dhcp_probe_service, 'maaslog')106 maaslog = self.patch(dhcp_probe_service, 'maaslog')
133 deferToThread = self.patch(107 deferToThread = self.patch(
134 dhcp_probe_service, 'deferToThread')108 dhcp_probe_service, 'deferToThread')
135 deferToThread.return_value = defer.succeed(['192.168.0.100'])109 deferToThread.side_effect = [
110 defer.succeed(interfaces),
111 defer.succeed(['192.168.0.100']),
112 ]
136 protocol, connecting = self.patch_rpc_methods()113 protocol, connecting = self.patch_rpc_methods()
137 self.addCleanup((yield connecting))114 self.addCleanup((yield connecting))
138115
139 del protocol._commandDispatch[116 del protocol._commandDispatch[
140 region.ReportForeignDHCPServer.commandName]117 region.ReportForeignDHCPServer.commandName]
141 protocol.GetClusterInterfaces.return_value = {
142 'interfaces': [
143 self.make_cluster_interface_values(ip='192.168.0.1'),
144 ],
145 }
146118
147 rpc_service = Mock()119 rpc_service = Mock()
148 rpc_service.getClient.return_value = getRegionClient()120 rpc_service.getClient.return_value = getRegionClient()
@@ -153,13 +125,23 @@
153125
154 self.assertThat(126 self.assertThat(
155 maaslog.error, MockCalledOnceWith(127 maaslog.error, MockCalledOnceWith(
156 "Unable to inform region of rogue DHCP server: the region "128 "Unable to inform region of DHCP server: the region "
157 "does not yet support the ReportForeignDHCPServer RPC "129 "does not yet support the ReportForeignDHCPServer RPC "
158 "method."))130 "method."))
159131
160 def test_logs_errors(self):132 def test_logs_errors(self):
161 clock = Clock()133 clock = Clock()
134 interface_name = factory.make_name("eth")
135 interfaces = {
136 interface_name: {
137 "enabled": True,
138 }
139 }
140
162 maaslog = self.patch(dhcp_probe_service, 'maaslog')141 maaslog = self.patch(dhcp_probe_service, 'maaslog')
142 mock_interfaces = self.patch(
143 dhcp_probe_service, 'get_all_interfaces_definition')
144 mock_interfaces.return_value = interfaces
163 service = DHCPProbeService(145 service = DHCPProbeService(
164 sentinel.service, clock)146 sentinel.service, clock)
165 error_message = factory.make_string()147 error_message = factory.make_string()
@@ -168,25 +150,29 @@
168 service.startService()150 service.startService()
169 self.assertThat(151 self.assertThat(
170 maaslog.error, MockCalledOnceWith(152 maaslog.error, MockCalledOnceWith(
171 "Unable to probe for rogue DHCP servers: %s",153 "Unable to probe for DHCP servers: %s",
172 error_message))154 error_message))
173155
174 @defer.inlineCallbacks156 @defer.inlineCallbacks
175 def test_reports_foreign_dhcp_servers_to_region(self):157 def test_reports_foreign_dhcp_servers_to_region(self):
176 clock = Clock()158 clock = Clock()
159 interface_name = factory.make_name("eth")
160 interfaces = {
161 interface_name: {
162 "enabled": True,
163 }
164 }
165
177 protocol, connecting = self.patch_rpc_methods()166 protocol, connecting = self.patch_rpc_methods()
178 self.addCleanup((yield connecting))167 self.addCleanup((yield connecting))
179168
180 deferToThread = self.patch(169 deferToThread = self.patch(
181 dhcp_probe_service, 'deferToThread')170 dhcp_probe_service, 'deferToThread')
182 foreign_dhcp_ip = factory.make_ipv4_address()171 foreign_dhcp_ip = factory.make_ipv4_address()
183 deferToThread.return_value = defer.succeed(172 deferToThread.side_effect = [
184 [foreign_dhcp_ip])173 defer.succeed(interfaces),
185174 defer.succeed([foreign_dhcp_ip]),
186 interface = self.make_cluster_interface_values()175 ]
187 protocol.GetClusterInterfaces.return_value = {
188 'interfaces': [interface],
189 }
190176
191 client = getRegionClient()177 client = getRegionClient()
192 rpc_service = Mock()178 rpc_service = Mock()
@@ -201,24 +187,29 @@
201 protocol.ReportForeignDHCPServer,187 protocol.ReportForeignDHCPServer,
202 MockCalledOnceWith(188 MockCalledOnceWith(
203 protocol,189 protocol,
204 cluster_uuid=client.localIdent,190 system_id=client.localIdent,
205 interface_name=interface['name'],191 interface_name=interface_name,
206 foreign_dhcp_ip=foreign_dhcp_ip))192 dhcp_ip=foreign_dhcp_ip))
207193
208 @defer.inlineCallbacks194 @defer.inlineCallbacks
209 def test_reports_lack_of_foreign_dhcp_servers_to_region(self):195 def test_reports_lack_of_foreign_dhcp_servers_to_region(self):
210 clock = Clock()196 clock = Clock()
197 interface_name = factory.make_name("eth")
198 interfaces = {
199 interface_name: {
200 "enabled": True,
201 }
202 }
203
211 protocol, connecting = self.patch_rpc_methods()204 protocol, connecting = self.patch_rpc_methods()
212 self.addCleanup((yield connecting))205 self.addCleanup((yield connecting))
213206
214 deferToThread = self.patch(207 deferToThread = self.patch(
215 dhcp_probe_service, 'deferToThread')208 dhcp_probe_service, 'deferToThread')
216 deferToThread.return_value = defer.succeed([])209 deferToThread.side_effect = [
217210 defer.succeed(interfaces),
218 interface = self.make_cluster_interface_values()211 defer.succeed([]),
219 protocol.GetClusterInterfaces.return_value = {212 ]
220 'interfaces': [interface],
221 }
222213
223 client = getRegionClient()214 client = getRegionClient()
224 rpc_service = Mock()215 rpc_service = Mock()
@@ -232,6 +223,6 @@
232 protocol.ReportForeignDHCPServer,223 protocol.ReportForeignDHCPServer,
233 MockCalledOnceWith(224 MockCalledOnceWith(
234 protocol,225 protocol,
235 cluster_uuid=client.localIdent,226 system_id=client.localIdent,
236 interface_name=interface['name'],227 interface_name=interface_name,
237 foreign_dhcp_ip=None))228 dhcp_ip=None))
238229
=== modified file 'src/provisioningserver/rpc/region.py'
--- src/provisioningserver/rpc/region.py 2016-03-28 13:54:47 +0000
+++ src/provisioningserver/rpc/region.py 2016-04-07 13:22:34 +0000
@@ -14,7 +14,6 @@
14 "GetBootConfig",14 "GetBootConfig",
15 "GetBootSources",15 "GetBootSources",
16 "GetBootSourcesV2",16 "GetBootSourcesV2",
17 "GetClusterInterfaces",
18 "GetProxies",17 "GetProxies",
19 "Identify",18 "Identify",
20 "ListNodePowerParameters",19 "ListNodePowerParameters",
@@ -337,38 +336,20 @@
337336
338337
339class ReportForeignDHCPServer(amp.Command):338class ReportForeignDHCPServer(amp.Command):
340 """Report a foreign DHCP server on the cluster's network.339 """Report a foreign DHCP server on a rack controller's interface.
341340
342 :since: 1.7341 :since: 2.0
343 """342 """
344343
345 arguments = [344 arguments = [
346 (b"cluster_uuid", amp.Unicode()),345 (b"system_id", amp.Unicode()),
347 (b"interface_name", amp.Unicode()),346 (b"interface_name", amp.Unicode()),
348 (b"foreign_dhcp_ip", amp.Unicode(optional=True)),347 (b"dhcp_ip", amp.Unicode(optional=True)),
349 ]348 ]
350 response = []349 response = []
351 errors = []350 errors = []
352351
353352
354class GetClusterInterfaces(amp.Command):
355 """Fetch the known interfaces for a cluster from the region.
356
357 :since: 1.7
358 """
359
360 arguments = [
361 (b"cluster_uuid", amp.Unicode()),
362 ]
363 response = [
364 (b"interfaces", AmpList(
365 [(b"name", amp.Unicode()),
366 (b"interface", amp.Unicode()),
367 (b"ip", amp.Unicode())]))
368 ]
369 errors = []
370
371
372class CreateNode(amp.Command):353class CreateNode(amp.Command):
373 """Create a node on a given cluster.354 """Create a node on a given cluster.
374355