Merge lp:~jtv/maas-test/tolerate-addressless-interface into lp:maas-test

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Raphaël Badin
Approved revision: 95
Merged at revision: 94
Proposed branch: lp:~jtv/maas-test/tolerate-addressless-interface
Merge into: lp:maas-test
Diff against target: 237 lines (+98/-32)
2 files modified
maastest/detect_dhcp.py (+21/-2)
maastest/tests/test_detect_dhcp.py (+77/-30)
To merge this branch: bzr merge lp:~jtv/maas-test/tolerate-addressless-interface
Reviewer Review Type Date Requested Status
Raphaël Badin (community) Approve
Review via email: mp+198693@code.launchpad.net

Commit message

Continue past DHCP server check if testing network interface has no IP address.

Description of the change

I apologise for the brevity of this merge proposal. It was written in a hurry. See bug for details; the symptom of the problem is an IOError with errno = 99 (EADDRNOTAVAIL). We need to bind to that IP address, or invite problems if a dhclient is running on another interface.

On the bright side, if the interface has no IP address, that probably means that there's no DHCP server running anyway!

Jeroen

To post a comment you must log in.
Revision history for this message
Raphaël Badin (rvb) wrote :

Nice!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'maastest/detect_dhcp.py'
2--- maastest/detect_dhcp.py 2013-12-06 07:25:45 +0000
3+++ maastest/detect_dhcp.py 2013-12-12 09:57:33 +0000
4@@ -21,6 +21,7 @@
5
6
7 from contextlib import contextmanager
8+from errno import EADDRNOTAVAIL
9 import fcntl
10 import logging
11 from random import randint
12@@ -130,7 +131,14 @@
13 ifreq = struct.pack(
14 b'16sH14s', interface.encode('ascii')[:15],
15 socket.AF_INET, b'\x00' * 14)
16- info = fcntl.ioctl(sock, SIOCGIFADDR, ifreq)
17+ try:
18+ info = fcntl.ioctl(sock, SIOCGIFADDR, ifreq)
19+ except IOError as e:
20+ if e.errno == EADDRNOTAVAIL:
21+ # Interface has no IP address.
22+ return None
23+ else:
24+ raise
25 ip = struct.unpack(b'16sH2x4s8x', info)[2]
26 return socket.inet_ntoa(ip)
27
28@@ -148,11 +156,17 @@
29
30
31 def request_dhcp(interface):
32- """Broadcast a DHCP discovery request. Return DHCP transaction ID."""
33+ """Broadcast a DHCP discovery request.
34+
35+ Returns DHCP transaction ID, or `None` in the special case where the
36+ network interface has no IP address.
37+ """
38 with udp_socket() as sock:
39 sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
40 mac = get_interface_MAC(sock, interface)
41 bind_address = get_interface_IP(sock, interface)
42+ if bind_address is None:
43+ return None
44 discover = DHCPDiscoverPacket(mac)
45 sock.bind((bind_address, BOOTP_CLIENT_PORT))
46 sock.sendto(discover.packet, ('<broadcast>', BOOTP_SERVER_PORT))
47@@ -197,4 +211,9 @@
48 # UDP is not reliable at any rate. If detection is important, we should
49 # send out repeated requests.
50 transaction_id = request_dhcp(interface)
51+ if transaction_id is None:
52+ logging.info(
53+ "Network interface %s has no IP address. "
54+ "Can't scan for DHCP servers.", interface)
55+ return set()
56 return receive_offers(transaction_id)
57
58=== modified file 'maastest/tests/test_detect_dhcp.py'
59--- maastest/tests/test_detect_dhcp.py 2013-12-06 07:02:08 +0000
60+++ maastest/tests/test_detect_dhcp.py 2013-12-12 09:57:33 +0000
61@@ -14,6 +14,8 @@
62 __metaclass__ = type
63 __all__ = []
64
65+from errno import EADDRNOTAVAIL
66+import fcntl
67 import os
68 from random import randint
69 import socket
70@@ -28,6 +30,7 @@
71 get_interface_IP,
72 get_interface_MAC,
73 make_transaction_ID,
74+ probe_dhcp,
75 receive_offers,
76 request_dhcp,
77 udp_socket,
78@@ -71,7 +74,7 @@
79 return os.urandom(length)
80
81
82-class MakeTransactionID(TestCase):
83+class TestMakeTransactionID(TestCase):
84 """Tests for `make_transaction_ID`."""
85
86 def test_produces_well_formed_ID(self):
87@@ -159,6 +162,12 @@
88 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
89 self.assertEqual('127.0.0.1', get_interface_IP(sock, 'lo'))
90
91+ def test_returns_None_if_no_address(self):
92+ failure = IOError(EADDRNOTAVAIL, "Interface has no address")
93+ self.patch(fcntl, 'ioctl', mock.MagicMock(side_effect=failure))
94+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
95+ self.assertIsNone(get_interface_IP(sock, make_name('itf')))
96+
97
98 def patch_socket(testcase):
99 """Patch `socket.socket` to return a mock."""
100@@ -222,6 +231,15 @@
101 mock.MagicMock(return_value=transaction_id))
102 return transaction_id
103
104+ def test_returns_None_if_no_IP_address(self):
105+ self.patch(
106+ detect_module, 'get_interface_MAC',
107+ mock.MagicMock(return_value=make_MAC()))
108+ self.patch(
109+ detect_module, 'get_interface_IP',
110+ mock.MagicMock(return_value=None))
111+ self.assertIsNone(request_dhcp(make_name('itf')))
112+
113 def test_sends_discover_packet(self):
114 sock = patch_socket(self)
115 self.patch_interface_MAC()
116@@ -266,34 +284,36 @@
117 return self.packets.pop(0)
118
119
120+def patch_recv(testcase, sock, num_packets=0):
121+ """Patch up socket's `recv` to return `num_packets` arbitrary packets.
122+
123+ After that, further calls to `recv` will raise a timeout.
124+ """
125+ packets = [make_bytes() for _ in range(num_packets)]
126+ receiver = FakePacketReceiver(packets)
127+ testcase.patch(sock, 'recv', receiver)
128+ return receiver
129+
130+
131+def patch_offer_packet(testcase):
132+ """Patch a mock `DHCPOfferPacket`."""
133+ transaction_id = make_bytes(4)
134+ packet = mock.MagicMock()
135+ packet.transaction_ID = transaction_id
136+ packet.dhcp_server_ID = make_IP()
137+ testcase.patch(
138+ detect_module, 'DHCPOfferPacket',
139+ mock.MagicMock(return_value=packet))
140+ return packet
141+
142+
143 class TestReceiveOffers(TestCase):
144 """Tests for `receive_offers`."""
145
146- def patch_recv(self, sock, num_packets=0):
147- """Patch up socket's `recv` to return `num_packets` arbitrary packets.
148-
149- After that, further calls to `recv` will raise a timeout.
150- """
151- packets = [make_bytes() for _ in range(num_packets)]
152- receiver = FakePacketReceiver(packets)
153- self.patch(sock, 'recv', receiver)
154- return receiver
155-
156- def patch_offer_packet(self):
157- """Patch a mock `DHCPOfferPacket`."""
158- transaction_id = make_bytes(4)
159- packet = mock.MagicMock()
160- packet.transaction_ID = transaction_id
161- packet.dhcp_server_ID = make_IP()
162- self.patch(
163- detect_module, 'DHCPOfferPacket',
164- mock.MagicMock(return_value=packet))
165- return packet
166-
167 def test_receives_from_socket(self):
168 sock = patch_socket(self)
169- receiver = self.patch_recv(sock)
170- transaction_id = self.patch_offer_packet().transaction_ID
171+ receiver = patch_recv(self, sock)
172+ transaction_id = patch_offer_packet(self).transaction_ID
173
174 receive_offers(transaction_id)
175
176@@ -307,15 +327,15 @@
177
178 def test_returns_empty_if_nothing_received(self):
179 sock = patch_socket(self)
180- self.patch_recv(sock)
181- transaction_id = self.patch_offer_packet().transaction_ID
182+ patch_recv(self, sock)
183+ transaction_id = patch_offer_packet(self).transaction_ID
184
185 self.assertEqual(set(), receive_offers(transaction_id))
186
187 def test_processes_offer(self):
188 sock = patch_socket(self)
189- self.patch_recv(sock, 1)
190- packet = self.patch_offer_packet()
191+ patch_recv(self, sock, 1)
192+ packet = patch_offer_packet(self)
193
194 self.assertEqual(
195 {packet.dhcp_server_ID},
196@@ -323,8 +343,8 @@
197
198 def test_ignores_other_transactions(self):
199 sock = patch_socket(self)
200- self.patch_recv(sock, 1)
201- self.patch_offer_packet()
202+ patch_recv(self, sock, 1)
203+ patch_offer_packet(self)
204 other_transaction_id = make_bytes(4)
205
206 self.assertEqual(set(), receive_offers(other_transaction_id))
207@@ -339,3 +359,30 @@
208 self.assertRaises(
209 InducedError,
210 receive_offers, make_bytes(4))
211+
212+
213+class TestProbeDHCP(TestCase):
214+
215+ def test_detects_dhcp_servers(self):
216+ sock = patch_socket(self)
217+ patch_recv(self, sock, 1)
218+ packet = patch_offer_packet(self)
219+ transaction_id = packet.transaction_ID
220+ server = packet.dhcp_server_ID
221+ self.patch(
222+ detect_module, 'request_dhcp',
223+ mock.MagicMock(return_value=transaction_id))
224+
225+ servers = probe_dhcp(make_name('itf'))
226+
227+ self.assertEqual({server}, servers)
228+
229+ def test_tolerates_interface_without_address(self):
230+ self.patch(
231+ detect_module, 'get_interface_MAC',
232+ mock.MagicMock(return_value=make_MAC()))
233+ self.patch(
234+ detect_module, 'get_interface_IP',
235+ mock.MagicMock(return_value=None))
236+ servers = probe_dhcp(make_name('itf'))
237+ self.assertEqual(set(), servers)

Subscribers

People subscribed via source and target branches