Merge lp:~mpontillo/maas/dhcp-probe-log-spam--bug-1633717 into lp:~maas-committers/maas/trunk

Proposed by Mike Pontillo
Status: Merged
Approved by: Mike Pontillo
Approved revision: no longer in the source branch.
Merged at revision: 5503
Proposed branch: lp:~mpontillo/maas/dhcp-probe-log-spam--bug-1633717
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 321 lines (+183/-11)
4 files modified
src/provisioningserver/rackdservices/dhcp_probe_service.py (+18/-5)
src/provisioningserver/rackdservices/tests/test_dhcp_probe_service.py (+104/-6)
src/provisioningserver/utils/network.py (+17/-0)
src/provisioningserver/utils/tests/test_network.py (+44/-0)
To merge this branch: bzr merge lp:~mpontillo/maas/dhcp-probe-log-spam--bug-1633717
Reviewer Review Type Date Requested Status
Lee Trager (community) Approve
Review via email: mp+309054@code.launchpad.net

Commit message

Fix extraneous logging in external DHCP detection.

 * No longer attempt to probe any interfaces without an IPv4
   address assigned (it will always fail anyway).
 * Add utility to determine if there is an IPv4 address on a specified
   interface, given the interfaces definition dictionary.
 * Fix error surfacing to include traceback.

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

LGTM!

Revision history for this message
Lee Trager (ltrager) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/provisioningserver/rackdservices/dhcp_probe_service.py'
2--- src/provisioningserver/rackdservices/dhcp_probe_service.py 2016-10-01 01:25:00 +0000
3+++ src/provisioningserver/rackdservices/dhcp_probe_service.py 2016-10-21 19:00:29 +0000
4@@ -15,7 +15,10 @@
5 from provisioningserver.logger.log import get_maas_logger
6 from provisioningserver.rpc.exceptions import NoConnectionsAvailable
7 from provisioningserver.rpc.region import ReportForeignDHCPServer
8-from provisioningserver.utils.network import get_all_interfaces_definition
9+from provisioningserver.utils.network import (
10+ get_all_interfaces_definition,
11+ has_ipv4_address,
12+)
13 from provisioningserver.utils.twisted import (
14 pause,
15 retries,
16@@ -50,8 +53,11 @@
17 self.clock = reactor
18 self.client_service = client_service
19
20- def log(self, string):
21- log.msg(string, system=type(self).__name__)
22+ def log(self, *args, **kwargs):
23+ log.msg(*args, **kwargs, system=type(self).__name__)
24+
25+ def err(self, *args, **kwargs):
26+ log.err(*args, **kwargs, system=type(self).__name__)
27
28 def _get_interfaces(self):
29 """Return the interfaces for this rack controller."""
30@@ -59,7 +65,13 @@
31 d.addCallback(lambda interfaces: [
32 name
33 for name, info in interfaces.items()
34- if info["enabled"]
35+ # No IPv4 address (unfortunately) means that MAAS cannot probe
36+ # this interface. This is because ultimately the DHCP probe
37+ # mechanism must send out a unicast UDP packet from the
38+ # interface in order to receive a response, and the TCP/IP
39+ # stack will drop packets coming back from the server if an
40+ # unexpected address is used.
41+ if info["enabled"] and has_ipv4_address(interfaces, name)
42 ])
43 return d
44
45@@ -123,7 +135,7 @@
46 "'%s'." % interface)
47 if is_dev_environment():
48 error += " (Did you configure authbind per HACKING.txt?)"
49- log.err(e, error, system=type(self).__name__)
50+ self.err(e, error)
51 continue
52 else:
53 if len(servers) > 0:
54@@ -146,5 +158,6 @@
55 maaslog.error(
56 "Unable to probe for DHCP servers: %s",
57 str(error))
58+ self.err(error, "Unable to probe for DHCP servers.")
59 else:
60 maaslog.debug("Finished periodic DHCP probe.")
61
62=== modified file 'src/provisioningserver/rackdservices/tests/test_dhcp_probe_service.py'
63--- src/provisioningserver/rackdservices/tests/test_dhcp_probe_service.py 2016-10-18 11:55:44 +0000
64+++ src/provisioningserver/rackdservices/tests/test_dhcp_probe_service.py 2016-10-21 19:00:29 +0000
65@@ -5,7 +5,6 @@
66
67 __all__ = []
68
69-
70 from unittest.mock import (
71 Mock,
72 sentinel,
73@@ -13,8 +12,10 @@
74
75 from maastesting.factory import factory
76 from maastesting.matchers import (
77+ DocTestMatches,
78 get_mock_calls,
79 HasLength,
80+ MockCalledOnce,
81 MockCalledOnceWith,
82 MockNotCalled,
83 )
84@@ -22,6 +23,7 @@
85 MAASTestCase,
86 MAASTwistedRunTest,
87 )
88+from maastesting.twisted import TwistedLoggerFixture
89 from provisioningserver.rackdservices import dhcp_probe_service
90 from provisioningserver.rackdservices.dhcp_probe_service import (
91 DHCPProbeService,
92@@ -81,6 +83,7 @@
93 interfaces = {
94 interface_name: {
95 "enabled": True,
96+ "links": [{"address": "10.0.0.1/24"}]
97 }
98 }
99
100@@ -118,6 +121,7 @@
101 interfaces = {
102 interface_name: {
103 "enabled": True,
104+ "links": [{"address": "10.0.0.1/24"}]
105 }
106 }
107
108@@ -130,11 +134,103 @@
109 error_message = factory.make_string()
110 self.patch(service, 'probe_dhcp').side_effect = Exception(
111 error_message)
112- service.startService()
113- self.assertThat(
114- maaslog.error, MockCalledOnceWith(
115- "Unable to probe for DHCP servers: %s",
116- error_message))
117+ with TwistedLoggerFixture() as logger:
118+ service.startService()
119+ self.assertThat(
120+ maaslog.error, MockCalledOnceWith(
121+ "Unable to probe for DHCP servers: %s",
122+ error_message))
123+ self.assertThat(logger.output, DocTestMatches(
124+ "Unable to probe for DHCP servers.\n..."
125+ "Traceback... "))
126+
127+ @inlineCallbacks
128+ def test_skips_disabled_interfaces(self):
129+ clock = Clock()
130+ interface_name = factory.make_name("eth")
131+ interfaces = {
132+ interface_name: {
133+ "enabled": False,
134+ "links": [{"address": "10.0.0.1/24"}]
135+ }
136+ }
137+ mock_interfaces = self.patch(
138+ dhcp_probe_service, 'get_all_interfaces_definition')
139+ mock_interfaces.return_value = interfaces
140+ service = DHCPProbeService(
141+ sentinel.service, clock)
142+ try_get_client = self.patch(service, '_tryGetClient')
143+ try_get_client.getClient = Mock()
144+ probe_interface = self.patch(dhcp_probe_service, 'probe_interface')
145+ yield service.startService()
146+ yield service.stopService()
147+ self.assertThat(probe_interface, MockNotCalled())
148+
149+ @inlineCallbacks
150+ def test_probes_ipv4_interfaces(self):
151+ clock = Clock()
152+ interface_name = factory.make_name("eth")
153+ interfaces = {
154+ interface_name: {
155+ "enabled": True,
156+ "links": [{"address": "10.0.0.1/24"}]
157+ }
158+ }
159+ mock_interfaces = self.patch(
160+ dhcp_probe_service, 'get_all_interfaces_definition')
161+ mock_interfaces.return_value = interfaces
162+ service = DHCPProbeService(
163+ sentinel.service, clock)
164+ try_get_client = self.patch(service, '_tryGetClient')
165+ try_get_client.getClient = Mock()
166+ probe_interface = self.patch(dhcp_probe_service, 'probe_interface')
167+ yield service.startService()
168+ yield service.stopService()
169+ self.assertThat(probe_interface, MockCalledOnce())
170+
171+ @inlineCallbacks
172+ def test_skips_ipv6_interfaces(self):
173+ clock = Clock()
174+ interface_name = factory.make_name("eth")
175+ interfaces = {
176+ interface_name: {
177+ "enabled": True,
178+ "links": [{"address": "2001:db8::1/64"}]
179+ }
180+ }
181+ mock_interfaces = self.patch(
182+ dhcp_probe_service, 'get_all_interfaces_definition')
183+ mock_interfaces.return_value = interfaces
184+ service = DHCPProbeService(
185+ sentinel.service, clock)
186+ try_get_client = self.patch(service, '_tryGetClient')
187+ try_get_client.getClient = Mock()
188+ probe_interface = self.patch(dhcp_probe_service, 'probe_interface')
189+ yield service.startService()
190+ yield service.stopService()
191+ self.assertThat(probe_interface, MockNotCalled())
192+
193+ @inlineCallbacks
194+ def test_skips_unconfigured_interfaces(self):
195+ clock = Clock()
196+ interface_name = factory.make_name("eth")
197+ interfaces = {
198+ interface_name: {
199+ "enabled": True,
200+ "links": []
201+ }
202+ }
203+ mock_interfaces = self.patch(
204+ dhcp_probe_service, 'get_all_interfaces_definition')
205+ mock_interfaces.return_value = interfaces
206+ service = DHCPProbeService(
207+ sentinel.service, clock)
208+ try_get_client = self.patch(service, '_tryGetClient')
209+ try_get_client.getClient = Mock()
210+ probe_interface = self.patch(dhcp_probe_service, 'probe_interface')
211+ yield service.startService()
212+ yield service.stopService()
213+ self.assertThat(probe_interface, MockNotCalled())
214
215 @inlineCallbacks
216 def test_reports_foreign_dhcp_servers_to_region(self):
217@@ -143,6 +239,7 @@
218 interfaces = {
219 interface_name: {
220 "enabled": True,
221+ "links": [{"address": "10.0.0.1/24"}]
222 }
223 }
224
225@@ -182,6 +279,7 @@
226 interfaces = {
227 interface_name: {
228 "enabled": True,
229+ "links": [{"address": "10.0.0.1/24"}]
230 }
231 }
232
233
234=== modified file 'src/provisioningserver/utils/network.py'
235--- src/provisioningserver/utils/network.py 2016-10-17 23:56:12 +0000
236+++ src/provisioningserver/utils/network.py 2016-10-21 19:00:29 +0000
237@@ -1171,6 +1171,23 @@
238 return interfaces
239
240
241+def has_ipv4_address(interfaces: dict, interface: str) -> bool:
242+ """Returns True if the specified interface has an IPv4 address assigned.
243+
244+ If no addresses are assigned, or only addresses with other address families
245+ are assigned (IPv6), returns False.
246+
247+ :param interfaces: The output of `get_all_interfaces_definition()`.
248+ :param interface: The interface name to check.
249+ """
250+ links = interfaces[interface]["links"]
251+ address_families = {
252+ IPAddress(link['address'].split('/')[0]).version
253+ for link in links
254+ }
255+ return 4 in address_families
256+
257+
258 def is_loopback_address(hostname):
259 """Determine if the given hostname appears to be a loopback address.
260
261
262=== modified file 'src/provisioningserver/utils/tests/test_network.py'
263--- src/provisioningserver/utils/tests/test_network.py 2016-10-17 17:34:28 +0000
264+++ src/provisioningserver/utils/tests/test_network.py 2016-10-21 19:00:29 +0000
265@@ -51,6 +51,7 @@
266 get_eui_organization,
267 get_interface_children,
268 get_mac_organization,
269+ has_ipv4_address,
270 hex_str_to_bytes,
271 inet_ntop,
272 interface_children,
273@@ -1472,6 +1473,49 @@
274 self.assertInterfacesResult(ip_addr, iproute_info, {}, expected_result)
275
276
277+class TestHasIPv4Address(MAASTestCase):
278+ """Tests for `has_ipv4_address()`."""
279+
280+ def make_interfaces(self, name, address=None):
281+ links = []
282+ if address is not None:
283+ links.append({
284+ 'address': address,
285+ 'mode': 'static'
286+ })
287+ return {
288+ name: {
289+ 'enabled': True,
290+ 'links': links,
291+ 'mac_address': '52:54:00:e7:d9:2c',
292+ 'monitored': True,
293+ 'parents': [],
294+ 'source': 'ipaddr',
295+ 'type': 'physical'
296+ }
297+ }
298+
299+ def test__returns_false_for_no_ip_address(self):
300+ ifname = factory.make_name('eth')
301+ self.assertThat(
302+ has_ipv4_address(self.make_interfaces(ifname), ifname),
303+ Equals(False))
304+
305+ def test__returns_false_for_ipv6_address(self):
306+ ifname = factory.make_name('eth')
307+ address = "%s/64" % factory.make_ipv6_address()
308+ self.assertThat(
309+ has_ipv4_address(self.make_interfaces(ifname, address), ifname),
310+ Equals(False))
311+
312+ def test__returns_true_for_ipv4_address(self):
313+ ifname = factory.make_name('eth')
314+ address = "%s/24" % factory.make_ipv4_address()
315+ self.assertThat(
316+ has_ipv4_address(self.make_interfaces(ifname, address), ifname),
317+ Equals(True))
318+
319+
320 class TestGetInterfaceChildren(MAASTestCase):
321 """Tests for `get_interface_children()`."""
322