Merge lp:~jtv/maas/extract-compose_URL 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: 3344
Proposed branch: lp:~jtv/maas/extract-compose_URL
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 481 lines (+172/-143)
10 files modified
src/maas/development.py (+1/-1)
src/maas/settings.py (+1/-1)
src/maasserver/preseed.py (+2/-4)
src/maasserver/tests/test_dhcp.py (+1/-1)
src/provisioningserver/drivers/hardware/seamicro.py (+6/-5)
src/provisioningserver/drivers/hardware/tests/test_seamicro.py (+2/-7)
src/provisioningserver/utils/__init__.py (+0/-33)
src/provisioningserver/utils/tests/test_url.py (+107/-0)
src/provisioningserver/utils/tests/test_utils.py (+1/-91)
src/provisioningserver/utils/url.py (+51/-0)
To merge this branch: bzr merge lp:~jtv/maas/extract-compose_URL
Reviewer Review Type Date Requested Status
Graham Binns (community) Approve
Review via email: mp+241076@code.launchpad.net

Commit message

Extract provisioningserver.utils.compose_URL into its own sub-module, provisioningserver.utils.url, to make room for another URL-related helper that I'm about to add.

Description of the change

There are no substantial changes here, beyond moving code and changing some imports. The imports changes did affect some patch calls in tests, but nothing dramatic.

Jeroen

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

Easy karma indeed.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maas/development.py'
2--- src/maas/development.py 2014-10-08 11:01:44 +0000
3+++ src/maas/development.py 2014-11-07 13:40:43 +0000
4@@ -25,7 +25,7 @@
5 from maas.customise_test_db import patch_db_creation
6 from metadataserver.address import guess_server_host
7 import provisioningserver.config
8-from provisioningserver.utils import compose_URL
9+from provisioningserver.utils.url import compose_URL
10 from psycopg2.extensions import ISOLATION_LEVEL_READ_COMMITTED
11
12 # We expect the following settings to be overridden. They are mentioned here
13
14=== modified file 'src/maas/settings.py'
15--- src/maas/settings.py 2014-10-30 18:24:50 +0000
16+++ src/maas/settings.py 2014-11-07 13:40:43 +0000
17@@ -25,7 +25,7 @@
18 )
19 from maas.monkey import patch_get_script_prefix
20 from metadataserver.address import guess_server_host
21-from provisioningserver.utils import compose_URL
22+from provisioningserver.utils.url import compose_URL
23 from psycopg2.extensions import ISOLATION_LEVEL_READ_COMMITTED
24
25
26
27=== modified file 'src/maasserver/preseed.py'
28--- src/maasserver/preseed.py 2014-10-02 05:07:20 +0000
29+++ src/maasserver/preseed.py 2014-11-07 13:40:43 +0000
30@@ -61,11 +61,9 @@
31 from metadataserver.user_data.snippets import get_snippet_context
32 from netaddr import IPAddress
33 from provisioningserver.rpc.exceptions import NoConnectionsAvailable
34-from provisioningserver.utils import (
35- compose_URL,
36- locate_config,
37- )
38+from provisioningserver.utils import locate_config
39 from provisioningserver.utils.fs import read_text_file
40+from provisioningserver.utils.url import compose_URL
41 import tempita
42 import yaml
43
44
45=== modified file 'src/maasserver/tests/test_dhcp.py'
46--- src/maasserver/tests/test_dhcp.py 2014-09-19 03:12:47 +0000
47+++ src/maasserver/tests/test_dhcp.py 2014-11-07 13:40:43 +0000
48@@ -55,7 +55,7 @@
49 ConfigureDHCPv6,
50 )
51 from provisioningserver.rpc.testing import always_succeed_with
52-from provisioningserver.utils import compose_URL
53+from provisioningserver.utils.url import compose_URL
54 from testtools.matchers import (
55 AllMatch,
56 ContainsAll,
57
58=== modified file 'src/provisioningserver/drivers/hardware/seamicro.py'
59--- src/provisioningserver/drivers/hardware/seamicro.py 2014-09-10 16:20:31 +0000
60+++ src/provisioningserver/drivers/hardware/seamicro.py 2014-11-07 13:40:43 +0000
61@@ -23,7 +23,8 @@
62 import urlparse
63
64 from provisioningserver.logger import get_maas_logger
65-import provisioningserver.utils as utils
66+from provisioningserver.utils import create_node
67+from provisioningserver.utils.url import compose_URL
68 from seamicroclient import exceptions as seamicro_exceptions
69 from seamicroclient.v2 import client as seamicro_client
70
71@@ -202,7 +203,7 @@
72 :returns: api for version, None if version not supported
73 """
74 if version == 'v0.9':
75- api = SeaMicroAPIV09(utils.compose_URL('http:///v0.9/', ip))
76+ api = SeaMicroAPIV09(compose_URL('http:///v0.9/', ip))
77 try:
78 api.login(username, password)
79 except urllib2.URLError:
80@@ -210,7 +211,7 @@
81 return None
82 return api
83 elif version == 'v2.0':
84- url = utils.compose_URL('http:///v2.0', ip)
85+ url = compose_URL('http:///v2.0', ip)
86 try:
87 api = seamicro_client.Client(
88 auth_url=url, username=username, password=password)
89@@ -288,13 +289,13 @@
90 maaslog.info(
91 "Found seamicro15k node with macs %s; adding to MAAS with "
92 "params : %s", macs, params)
93- utils.create_node(macs, 'amd64', 'sm15k', params)
94+ create_node(macs, 'amd64', 'sm15k', params)
95
96
97 def power_control_seamicro15k_v09(ip, username, password, server_id,
98 power_change, retry_count=5, retry_wait=1):
99 server_id = '%s/0' % server_id
100- api = SeaMicroAPIV09(utils.compose_URL('http:///v0.9/', ip))
101+ api = SeaMicroAPIV09(compose_URL('http:///v0.9/', ip))
102
103 while retry_count > 0:
104 api.login(username, password)
105
106=== modified file 'src/provisioningserver/drivers/hardware/tests/test_seamicro.py'
107--- src/provisioningserver/drivers/hardware/tests/test_seamicro.py 2014-09-18 12:44:38 +0000
108+++ src/provisioningserver/drivers/hardware/tests/test_seamicro.py 2014-11-07 13:40:43 +0000
109@@ -41,7 +41,6 @@
110 SeaMicroError,
111 select_seamicro15k_api_version,
112 )
113-import provisioningserver.utils as utils
114
115
116 class FakeResponse:
117@@ -334,9 +333,7 @@
118 self.patch(
119 SeaMicroAPIV09, 'get',
120 Mock(return_value=result))
121- mock_create_node = self.patch(
122- utils,
123- 'create_node')
124+ mock_create_node = self.patch(seamicro, 'create_node')
125
126 probe_seamicro15k_and_enlist(
127 ip, username, password, power_control='restapi')
128@@ -418,9 +415,7 @@
129 seamicro,
130 'get_seamicro15k_api')
131 mock_get_api.return_value = fake_client
132- mock_create_node = self.patch(
133- utils,
134- 'create_node')
135+ mock_create_node = self.patch(seamicro, 'create_node')
136
137 probe_seamicro15k_and_enlist(
138 ip, username, password, power_control='restapi2')
139
140=== modified file 'src/provisioningserver/utils/__init__.py'
141--- src/provisioningserver/utils/__init__.py 2014-09-16 11:43:25 +0000
142+++ src/provisioningserver/utils/__init__.py 2014-11-07 13:40:43 +0000
143@@ -13,7 +13,6 @@
144
145 __metaclass__ = type
146 __all__ = [
147- "compose_URL",
148 "create_node",
149 "filter_dict",
150 "flatten",
151@@ -36,11 +35,6 @@
152 import re
153 import sys
154 from sys import _getframe as getframe
155-import urllib
156-from urlparse import (
157- urlparse,
158- urlunparse,
159- )
160 from warnings import warn
161
162 import bson
163@@ -381,33 +375,6 @@
164 return matched, other
165
166
167-def compose_URL(base_url, host):
168- """Produce a URL on a given hostname or IP address.
169-
170- This is straightforward if the IP address is a hostname or an IPv4
171- address; but if it's an IPv6 address, the URL must contain the IP address
172- in square brackets as per RFC 3986.
173-
174- :param base_url: URL without the host part, e.g. `http:///path'.
175- :param host: Host name or IP address to insert in the host part of the URL.
176- :return: A URL string with the host part taken from `host`, and all others
177- from `base_url`.
178- """
179- if re.match('[:.0-9a-fA-F]+(?:%.+)?$', host) and host.count(':') > 0:
180- # IPv6 address, without the brackets. Add square brackets.
181- # In case there's a zone index (introduced by a % sign), escape it.
182- netloc_host = '[%s]' % urllib.quote(host, safe=':')
183- else:
184- # IPv4 address, hostname, or IPv6 with brackets. Keep as-is.
185- netloc_host = host
186- parsed_url = urlparse(base_url)
187- if parsed_url.port is None:
188- netloc = netloc_host
189- else:
190- netloc = '%s:%d' % (netloc_host, parsed_url.port)
191- return urlunparse(parsed_url._replace(netloc=netloc))
192-
193-
194 def warn_deprecated(alternative=None):
195 """Issue a `DeprecationWarning` for the calling function.
196
197
198=== added file 'src/provisioningserver/utils/tests/test_url.py'
199--- src/provisioningserver/utils/tests/test_url.py 1970-01-01 00:00:00 +0000
200+++ src/provisioningserver/utils/tests/test_url.py 2014-11-07 13:40:43 +0000
201@@ -0,0 +1,107 @@
202+# Copyright 2014 Canonical Ltd. This software is licensed under the
203+# GNU Affero General Public License version 3 (see the file LICENSE).
204+
205+"""Test utilities for URL handling."""
206+
207+from __future__ import (
208+ absolute_import,
209+ print_function,
210+ unicode_literals,
211+ )
212+
213+str = None
214+
215+__metaclass__ = type
216+__all__ = []
217+
218+from random import randint
219+
220+from maastesting.factory import factory
221+from maastesting.testcase import MAASTestCase
222+from provisioningserver.utils.url import compose_URL
223+
224+
225+class TestComposeURL(MAASTestCase):
226+
227+ def make_path(self):
228+ """Return an arbitrary URL path part."""
229+ return '%s/%s' % (factory.make_name('root'), factory.make_name('sub'))
230+
231+ def make_network_interface(self):
232+ return 'eth%d' % randint(0, 100)
233+
234+ def test__inserts_IPv4(self):
235+ ip = factory.make_ipv4_address()
236+ path = self.make_path()
237+ self.assertEqual(
238+ 'http://%s/%s' % (ip, path),
239+ compose_URL('http:///%s' % path, ip))
240+
241+ def test__inserts_IPv6_with_brackets(self):
242+ ip = factory.make_ipv6_address()
243+ path = self.make_path()
244+ self.assertEqual(
245+ 'http://[%s]/%s' % (ip, path),
246+ compose_URL('http:///%s' % path, ip))
247+
248+ def test__escapes_IPv6_zone_index(self):
249+ ip = factory.make_ipv6_address()
250+ zone = self.make_network_interface()
251+ hostname = '%s%%%s' % (ip, zone)
252+ path = self.make_path()
253+ self.assertEqual(
254+ 'http://[%s%%25%s]/%s' % (ip, zone, path),
255+ compose_URL('http:///%s' % path, hostname))
256+
257+ def test__inserts_bracketed_IPv6_unchanged(self):
258+ ip = factory.make_ipv6_address()
259+ hostname = '[%s]' % ip
260+ path = self.make_path()
261+ self.assertEqual(
262+ 'http://%s/%s' % (hostname, path),
263+ compose_URL('http:///%s' % path, hostname))
264+
265+ def test__does_not_escape_bracketed_IPv6_zone_index(self):
266+ ip = factory.make_ipv6_address()
267+ zone = self.make_network_interface()
268+ path = self.make_path()
269+ hostname = '[%s%%25%s]' % (ip, zone)
270+ self.assertEqual(
271+ 'http://%s/%s' % (hostname, path),
272+ compose_URL('http:///%s' % path, hostname))
273+
274+ def test__inserts_hostname(self):
275+ hostname = factory.make_name('host')
276+ path = self.make_path()
277+ self.assertEqual(
278+ 'http://%s/%s' % (hostname, path),
279+ compose_URL('http:///%s' % path, hostname))
280+
281+ def test__preserves_query(self):
282+ ip = factory.make_ipv4_address()
283+ key = factory.make_name('key')
284+ value = factory.make_name('value')
285+ self.assertEqual(
286+ 'https://%s?%s=%s' % (ip, key, value),
287+ compose_URL('https://?%s=%s' % (key, value), ip))
288+
289+ def test__preserves_port_with_IPv4(self):
290+ ip = factory.make_ipv4_address()
291+ port = factory.pick_port()
292+ self.assertEqual(
293+ 'https://%s:%s/' % (ip, port),
294+ compose_URL('https://:%s/' % port, ip))
295+
296+ def test__preserves_port_with_IPv6(self):
297+ ip = factory.make_ipv6_address()
298+ port = factory.pick_port()
299+ self.assertEqual(
300+ 'https://[%s]:%s/' % (ip, port),
301+ compose_URL('https://:%s/' % port, ip))
302+
303+ def test__preserves_port_with_hostname(self):
304+ hostname = factory.make_name('host')
305+ port = factory.pick_port()
306+ self.assertEqual(
307+ 'https://%s:%s/' % (hostname, port),
308+ compose_URL('https://:%s/' % port, hostname))
309
310=== modified file 'src/provisioningserver/utils/tests/test_utils.py'
311--- src/provisioningserver/utils/tests/test_utils.py 2014-09-18 12:44:38 +0000
312+++ src/provisioningserver/utils/tests/test_utils.py 2014-11-07 13:40:43 +0000
313@@ -18,10 +18,7 @@
314 from cStringIO import StringIO
315 import json
316 import os
317-from random import (
318- choice,
319- randint,
320- )
321+from random import choice
322 from textwrap import dedent
323
324 from fixtures import EnvironmentVariableFixture
325@@ -44,7 +41,6 @@
326 import provisioningserver.utils
327 from provisioningserver.utils import (
328 classify,
329- compose_URL,
330 create_node,
331 escape_py_literal,
332 filter_dict,
333@@ -544,92 +540,6 @@
334 "exists.", macs))
335
336
337-class TestComposeURL(MAASTestCase):
338-
339- def make_path(self):
340- """Return an arbitrary URL path part."""
341- return '%s/%s' % (factory.make_name('root'), factory.make_name('sub'))
342-
343- def make_network_interface(self):
344- return 'eth%d' % randint(0, 100)
345-
346- def test__inserts_IPv4(self):
347- ip = factory.make_ipv4_address()
348- path = self.make_path()
349- self.assertEqual(
350- 'http://%s/%s' % (ip, path),
351- compose_URL('http:///%s' % path, ip))
352-
353- def test__inserts_IPv6_with_brackets(self):
354- ip = factory.make_ipv6_address()
355- path = self.make_path()
356- self.assertEqual(
357- 'http://[%s]/%s' % (ip, path),
358- compose_URL('http:///%s' % path, ip))
359-
360- def test__escapes_IPv6_zone_index(self):
361- ip = factory.make_ipv6_address()
362- zone = self.make_network_interface()
363- hostname = '%s%%%s' % (ip, zone)
364- path = self.make_path()
365- self.assertEqual(
366- 'http://[%s%%25%s]/%s' % (ip, zone, path),
367- compose_URL('http:///%s' % path, hostname))
368-
369- def test__inserts_bracketed_IPv6_unchanged(self):
370- ip = factory.make_ipv6_address()
371- hostname = '[%s]' % ip
372- path = self.make_path()
373- self.assertEqual(
374- 'http://%s/%s' % (hostname, path),
375- compose_URL('http:///%s' % path, hostname))
376-
377- def test__does_not_escape_bracketed_IPv6_zone_index(self):
378- ip = factory.make_ipv6_address()
379- zone = self.make_network_interface()
380- path = self.make_path()
381- hostname = '[%s%%25%s]' % (ip, zone)
382- self.assertEqual(
383- 'http://%s/%s' % (hostname, path),
384- compose_URL('http:///%s' % path, hostname))
385-
386- def test__inserts_hostname(self):
387- hostname = factory.make_name('host')
388- path = self.make_path()
389- self.assertEqual(
390- 'http://%s/%s' % (hostname, path),
391- compose_URL('http:///%s' % path, hostname))
392-
393- def test__preserves_query(self):
394- ip = factory.make_ipv4_address()
395- key = factory.make_name('key')
396- value = factory.make_name('value')
397- self.assertEqual(
398- 'https://%s?%s=%s' % (ip, key, value),
399- compose_URL('https://?%s=%s' % (key, value), ip))
400-
401- def test__preserves_port_with_IPv4(self):
402- ip = factory.make_ipv4_address()
403- port = factory.pick_port()
404- self.assertEqual(
405- 'https://%s:%s/' % (ip, port),
406- compose_URL('https://:%s/' % port, ip))
407-
408- def test__preserves_port_with_IPv6(self):
409- ip = factory.make_ipv6_address()
410- port = factory.pick_port()
411- self.assertEqual(
412- 'https://[%s]:%s/' % (ip, port),
413- compose_URL('https://:%s/' % port, ip))
414-
415- def test__preserves_port_with_hostname(self):
416- hostname = factory.make_name('host')
417- port = factory.pick_port()
418- self.assertEqual(
419- 'https://%s:%s/' % (hostname, port),
420- compose_URL('https://:%s/' % port, hostname))
421-
422-
423 class TestGetClusterConfig(MAASTestCase):
424 scenarios = [
425 ('Variable with quoted value', dict(
426
427=== added file 'src/provisioningserver/utils/url.py'
428--- src/provisioningserver/utils/url.py 1970-01-01 00:00:00 +0000
429+++ src/provisioningserver/utils/url.py 2014-11-07 13:40:43 +0000
430@@ -0,0 +1,51 @@
431+# Copyright 2014 Canonical Ltd. This software is licensed under the
432+# GNU Affero General Public License version 3 (see the file LICENSE).
433+
434+"""Utilities for URL handling."""
435+
436+from __future__ import (
437+ absolute_import,
438+ print_function,
439+ unicode_literals,
440+ )
441+
442+str = None
443+
444+__metaclass__ = type
445+__all__ = [
446+ 'compose_URL',
447+ ]
448+
449+import re
450+import urllib
451+from urlparse import (
452+ urlparse,
453+ urlunparse,
454+ )
455+
456+
457+def compose_URL(base_url, host):
458+ """Produce a URL on a given hostname or IP address.
459+
460+ This is straightforward if the IP address is a hostname or an IPv4
461+ address; but if it's an IPv6 address, the URL must contain the IP address
462+ in square brackets as per RFC 3986.
463+
464+ :param base_url: URL without the host part, e.g. `http:///path'.
465+ :param host: Host name or IP address to insert in the host part of the URL.
466+ :return: A URL string with the host part taken from `host`, and all others
467+ from `base_url`.
468+ """
469+ if re.match('[:.0-9a-fA-F]+(?:%.+)?$', host) and host.count(':') > 0:
470+ # IPv6 address, without the brackets. Add square brackets.
471+ # In case there's a zone index (introduced by a % sign), escape it.
472+ netloc_host = '[%s]' % urllib.quote(host, safe=':')
473+ else:
474+ # IPv4 address, hostname, or IPv6 with brackets. Keep as-is.
475+ netloc_host = host
476+ parsed_url = urlparse(base_url)
477+ if parsed_url.port is None:
478+ netloc = netloc_host
479+ else:
480+ netloc = '%s:%d' % (netloc_host, parsed_url.port)
481+ return urlunparse(parsed_url._replace(netloc=netloc))