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
1=== modified file 'src/provisioningserver/utils/network.py'
2--- src/provisioningserver/utils/network.py 2014-08-13 21:49:35 +0000
3+++ src/provisioningserver/utils/network.py 2014-08-28 06:29:58 +0000
4@@ -19,9 +19,19 @@
5 'get_all_addresses_for_interface',
6 'get_all_interface_addresses',
7 'make_network',
8+ 'resolve_hostname',
9 ]
10
11
12+from socket import (
13+ AF_INET,
14+ AF_INET6,
15+ EAI_NODATA,
16+ EAI_NONAME,
17+ gaierror,
18+ getaddrinfo,
19+ )
20+
21 from netaddr import (
22 IPAddress,
23 IPNetwork,
24@@ -152,3 +162,37 @@
25 for interface in netifaces.interfaces():
26 for address in get_all_addresses_for_interface(interface):
27 yield address
28+
29+
30+def resolve_hostname(hostname, ip_version=4):
31+ """Wrapper around `getaddrinfo`: return addresses for `hostname`.
32+
33+ :param hostname: Host name (or IP address).
34+ :param ip_version: Look for addresses of this IP version only: 4 for IPv4,
35+ or 6 for IPv6.
36+ :return: A set of `IPAddress`. Empty if `hostname` does not resolve for
37+ the requested IP version.
38+ """
39+ addr_families = {
40+ 4: AF_INET,
41+ 6: AF_INET6,
42+ }
43+ assert ip_version in addr_families
44+ # Arbitrary non-privileged port, on which we can call getaddrinfo.
45+ port = 33360
46+ try:
47+ address_info = getaddrinfo(hostname, port, addr_families[ip_version])
48+ except gaierror as e:
49+ if e.errno in (EAI_NONAME, EAI_NODATA):
50+ # Name does not resolve.
51+ address_info = []
52+ else:
53+ raise
54+
55+ # The contents of sockaddr differ for IPv6 and IPv4, but the
56+ # first element is always the address, and that's all we care
57+ # about.
58+ return {
59+ IPAddress(sockaddr[0])
60+ for family, socktype, proto, canonname, sockaddr in address_info
61+ }
62
63=== modified file 'src/provisioningserver/utils/tests/test_network.py'
64--- src/provisioningserver/utils/tests/test_network.py 2014-08-13 21:49:35 +0000
65+++ src/provisioningserver/utils/tests/test_network.py 2014-08-28 06:29:58 +0000
66@@ -14,11 +14,21 @@
67 __metaclass__ = type
68 __all__ = []
69
70+from socket import (
71+ EAI_BADFLAGS,
72+ EAI_NODATA,
73+ EAI_NONAME,
74+ gaierror,
75+ )
76+
77 from maastesting.factory import factory
78 from maastesting.matchers import MockCalledOnceWith
79 from maastesting.testcase import MAASTestCase
80 import mock
81-from netaddr import IPNetwork
82+from netaddr import (
83+ IPAddress,
84+ IPNetwork,
85+ )
86 import netifaces
87 from netifaces import (
88 AF_LINK,
89@@ -34,6 +44,7 @@
90 get_all_addresses_for_interface,
91 get_all_interface_addresses,
92 make_network,
93+ resolve_hostname,
94 )
95
96
97@@ -345,3 +356,72 @@
98 self.assertEqual(
99 ip,
100 clean_up_netifaces_address('%s%%%s' % (ip, interface), interface))
101+
102+
103+class TestResolveHostname(MAASTestCase):
104+ """Tests for `resolve_hostname`."""
105+
106+ def patch_getaddrinfo(self, *addrs):
107+ fake = self.patch(network_module, 'getaddrinfo')
108+ fake.return_value = [
109+ (None, None, None, None, (unicode(address), None))
110+ for address in addrs
111+ ]
112+ return fake
113+
114+ def patch_getaddrinfo_fail(self, exception):
115+ fake = self.patch(network_module, 'getaddrinfo')
116+ fake.side_effect = exception
117+ return fake
118+
119+ def test__rejects_weird_IP_version(self):
120+ self.assertRaises(
121+ AssertionError,
122+ resolve_hostname, factory.make_hostname(), ip_version=5)
123+
124+ def test__integrates_with_getaddrinfo(self):
125+ result = resolve_hostname('localhost', 4)
126+ self.assertIsInstance(result, set)
127+ [localhost] = result
128+ self.assertIsInstance(localhost, IPAddress)
129+ self.assertIn(localhost, IPNetwork('127.0.0.0/8'))
130+
131+ def test__resolves_IPv4_address(self):
132+ ip = factory.getRandomIPAddress()
133+ fake = self.patch_getaddrinfo(ip)
134+ hostname = factory.make_hostname()
135+ result = resolve_hostname(hostname, 4)
136+ self.assertIsInstance(result, set)
137+ self.assertEqual({IPAddress(ip)}, result)
138+ self.assertThat(fake, MockCalledOnceWith(hostname, mock.ANY, AF_INET))
139+
140+ def test__resolves_IPv6_address(self):
141+ ip = factory.make_ipv6_address()
142+ fake = self.patch_getaddrinfo(ip)
143+ hostname = factory.make_hostname()
144+ result = resolve_hostname(hostname, 6)
145+ self.assertIsInstance(result, set)
146+ self.assertEqual({IPAddress(ip)}, result)
147+ self.assertThat(fake, MockCalledOnceWith(hostname, mock.ANY, AF_INET6))
148+
149+ def test__returns_empty_if_address_does_not_resolve(self):
150+ self.patch_getaddrinfo_fail(
151+ gaierror(EAI_NONAME, "Name or service not known"))
152+ self.assertEqual(set(), resolve_hostname(factory.make_hostname(), 4))
153+
154+ def test__returns_empty_if_address_resolves_to_no_data(self):
155+ self.patch_getaddrinfo_fail(
156+ gaierror(EAI_NODATA, "No data returned"))
157+ self.assertEqual(set(), resolve_hostname(factory.make_hostname(), 4))
158+
159+ def test__propagates_other_gaierrors(self):
160+ self.patch_getaddrinfo_fail(gaierror(EAI_BADFLAGS, "Bad parameters"))
161+ self.assertRaises(
162+ gaierror,
163+ resolve_hostname, factory.make_hostname(), 4)
164+
165+ def test__propagates_unexpected_errors(self):
166+ self.patch_getaddrinfo_fail(KeyError("Huh what?"))
167+ self.assertRaises(
168+ KeyError,
169+ resolve_hostname, factory.make_hostname(), 4)