Merge lp:~jtv/maas/resolve_hostname into lp:~maas-committers/maas/trunk

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 2833
Proposed branch: lp:~jtv/maas/resolve_hostname
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 169 lines (+125/-1)
2 files modified
src/provisioningserver/utils/network.py (+44/-0)
src/provisioningserver/utils/tests/test_network.py (+81/-1)
To merge this branch: bzr merge lp:~jtv/maas/resolve_hostname
Reviewer Review Type Date Requested Status
Julian Edwards (community) Approve
Review via email: mp+232372@code.launchpad.net

Commit message

New networking helper: resolve_hostname.

Description of the change

This is preparation for an IPv6 branch that is about to go up for separate review. In order to support sending DNS server addresses to DHCPv6 clients I had to pry apart the implementation of get_maas_facing_server_address. The separation of concerns also simplifies the testing a bit — there are a lot fewer combinations to go through.

Is it time to introduce an enum for IPv4/IPv6? It'd have to go into provisioningserver.enum, which does not currently exist, so I've been reluctant. Or should I pass AF_INET/AF_INET6? I didn't want to expose that level of implementation detail in an otherwise nicely abstract API.

Jeroen

To post a comment you must log in.
Revision history for this message
Julian Edwards (julian-edwards) wrote :

> provisioningserver.enum

I know I keep saying it, but a common third library would be awesome :/

AF_INET[6] sounds fine enough for me though, it's pretty generic.

review: Approve
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Yes, we absolutely want a common package! I've been splitting utility functions out of __init__.py modules, and I'm sure some of the resulting modules could simply move into a common section. Easier to see now what the dependencies are, too.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/provisioningserver/utils/network.py'
--- src/provisioningserver/utils/network.py 2014-08-13 21:49:35 +0000
+++ src/provisioningserver/utils/network.py 2014-08-28 06:29:58 +0000
@@ -19,9 +19,19 @@
19 'get_all_addresses_for_interface',19 'get_all_addresses_for_interface',
20 'get_all_interface_addresses',20 'get_all_interface_addresses',
21 'make_network',21 'make_network',
22 'resolve_hostname',
22 ]23 ]
2324
2425
26from socket import (
27 AF_INET,
28 AF_INET6,
29 EAI_NODATA,
30 EAI_NONAME,
31 gaierror,
32 getaddrinfo,
33 )
34
25from netaddr import (35from netaddr import (
26 IPAddress,36 IPAddress,
27 IPNetwork,37 IPNetwork,
@@ -152,3 +162,37 @@
152 for interface in netifaces.interfaces():162 for interface in netifaces.interfaces():
153 for address in get_all_addresses_for_interface(interface):163 for address in get_all_addresses_for_interface(interface):
154 yield address164 yield address
165
166
167def resolve_hostname(hostname, ip_version=4):
168 """Wrapper around `getaddrinfo`: return addresses for `hostname`.
169
170 :param hostname: Host name (or IP address).
171 :param ip_version: Look for addresses of this IP version only: 4 for IPv4,
172 or 6 for IPv6.
173 :return: A set of `IPAddress`. Empty if `hostname` does not resolve for
174 the requested IP version.
175 """
176 addr_families = {
177 4: AF_INET,
178 6: AF_INET6,
179 }
180 assert ip_version in addr_families
181 # Arbitrary non-privileged port, on which we can call getaddrinfo.
182 port = 33360
183 try:
184 address_info = getaddrinfo(hostname, port, addr_families[ip_version])
185 except gaierror as e:
186 if e.errno in (EAI_NONAME, EAI_NODATA):
187 # Name does not resolve.
188 address_info = []
189 else:
190 raise
191
192 # The contents of sockaddr differ for IPv6 and IPv4, but the
193 # first element is always the address, and that's all we care
194 # about.
195 return {
196 IPAddress(sockaddr[0])
197 for family, socktype, proto, canonname, sockaddr in address_info
198 }
155199
=== modified file 'src/provisioningserver/utils/tests/test_network.py'
--- src/provisioningserver/utils/tests/test_network.py 2014-08-13 21:49:35 +0000
+++ src/provisioningserver/utils/tests/test_network.py 2014-08-28 06:29:58 +0000
@@ -14,11 +14,21 @@
14__metaclass__ = type14__metaclass__ = type
15__all__ = []15__all__ = []
1616
17from socket import (
18 EAI_BADFLAGS,
19 EAI_NODATA,
20 EAI_NONAME,
21 gaierror,
22 )
23
17from maastesting.factory import factory24from maastesting.factory import factory
18from maastesting.matchers import MockCalledOnceWith25from maastesting.matchers import MockCalledOnceWith
19from maastesting.testcase import MAASTestCase26from maastesting.testcase import MAASTestCase
20import mock27import mock
21from netaddr import IPNetwork28from netaddr import (
29 IPAddress,
30 IPNetwork,
31 )
22import netifaces32import netifaces
23from netifaces import (33from netifaces import (
24 AF_LINK,34 AF_LINK,
@@ -34,6 +44,7 @@
34 get_all_addresses_for_interface,44 get_all_addresses_for_interface,
35 get_all_interface_addresses,45 get_all_interface_addresses,
36 make_network,46 make_network,
47 resolve_hostname,
37 )48 )
3849
3950
@@ -345,3 +356,72 @@
345 self.assertEqual(356 self.assertEqual(
346 ip,357 ip,
347 clean_up_netifaces_address('%s%%%s' % (ip, interface), interface))358 clean_up_netifaces_address('%s%%%s' % (ip, interface), interface))
359
360
361class TestResolveHostname(MAASTestCase):
362 """Tests for `resolve_hostname`."""
363
364 def patch_getaddrinfo(self, *addrs):
365 fake = self.patch(network_module, 'getaddrinfo')
366 fake.return_value = [
367 (None, None, None, None, (unicode(address), None))
368 for address in addrs
369 ]
370 return fake
371
372 def patch_getaddrinfo_fail(self, exception):
373 fake = self.patch(network_module, 'getaddrinfo')
374 fake.side_effect = exception
375 return fake
376
377 def test__rejects_weird_IP_version(self):
378 self.assertRaises(
379 AssertionError,
380 resolve_hostname, factory.make_hostname(), ip_version=5)
381
382 def test__integrates_with_getaddrinfo(self):
383 result = resolve_hostname('localhost', 4)
384 self.assertIsInstance(result, set)
385 [localhost] = result
386 self.assertIsInstance(localhost, IPAddress)
387 self.assertIn(localhost, IPNetwork('127.0.0.0/8'))
388
389 def test__resolves_IPv4_address(self):
390 ip = factory.getRandomIPAddress()
391 fake = self.patch_getaddrinfo(ip)
392 hostname = factory.make_hostname()
393 result = resolve_hostname(hostname, 4)
394 self.assertIsInstance(result, set)
395 self.assertEqual({IPAddress(ip)}, result)
396 self.assertThat(fake, MockCalledOnceWith(hostname, mock.ANY, AF_INET))
397
398 def test__resolves_IPv6_address(self):
399 ip = factory.make_ipv6_address()
400 fake = self.patch_getaddrinfo(ip)
401 hostname = factory.make_hostname()
402 result = resolve_hostname(hostname, 6)
403 self.assertIsInstance(result, set)
404 self.assertEqual({IPAddress(ip)}, result)
405 self.assertThat(fake, MockCalledOnceWith(hostname, mock.ANY, AF_INET6))
406
407 def test__returns_empty_if_address_does_not_resolve(self):
408 self.patch_getaddrinfo_fail(
409 gaierror(EAI_NONAME, "Name or service not known"))
410 self.assertEqual(set(), resolve_hostname(factory.make_hostname(), 4))
411
412 def test__returns_empty_if_address_resolves_to_no_data(self):
413 self.patch_getaddrinfo_fail(
414 gaierror(EAI_NODATA, "No data returned"))
415 self.assertEqual(set(), resolve_hostname(factory.make_hostname(), 4))
416
417 def test__propagates_other_gaierrors(self):
418 self.patch_getaddrinfo_fail(gaierror(EAI_BADFLAGS, "Bad parameters"))
419 self.assertRaises(
420 gaierror,
421 resolve_hostname, factory.make_hostname(), 4)
422
423 def test__propagates_unexpected_errors(self):
424 self.patch_getaddrinfo_fail(KeyError("Huh what?"))
425 self.assertRaises(
426 KeyError,
427 resolve_hostname, factory.make_hostname(), 4)