Merge lp:~allenap/maas/fix-1384464 into lp:~maas-committers/maas/trunk

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 3309
Proposed branch: lp:~allenap/maas/fix-1384464
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 465 lines (+152/-29)
11 files modified
src/maasserver/api/tests/test_nodegroup.py (+7/-1)
src/maasserver/models/nodegroup.py (+8/-1)
src/maasserver/models/tests/test_nodegroup.py (+12/-2)
src/provisioningserver/boot/utils.py (+8/-1)
src/provisioningserver/pserv_services/image_download_service.py (+11/-1)
src/provisioningserver/pserv_services/tests/test_image_download_service.py (+8/-2)
src/provisioningserver/rpc/boot_images.py (+24/-3)
src/provisioningserver/rpc/cluster.py (+2/-0)
src/provisioningserver/rpc/clusterservice.py (+5/-2)
src/provisioningserver/rpc/tests/test_boot_images.py (+47/-15)
src/provisioningserver/rpc/tests/test_clusterservice.py (+20/-1)
To merge this branch: bzr merge lp:~allenap/maas/fix-1384464
Reviewer Review Type Date Requested Status
Christian Reis (community) Approve
Graham Binns (community) Approve
Review via email: mp+240053@code.launchpad.net

Commit message

Use a configured HTTP proxy to import boot loaders to the cluster, but do not use a proxy when importing boot images from the region.

Previously the cluster used the proxy to download boot images. However, boot images must now be retrieved from the region and not the Internet, whereas boot loaders still must be retrieved via the Internet.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Many many thanks to Narinder Gupta for exhaustively testing this.

Revision history for this message
Andres Rodriguez (andreserl) wrote :

So tests were successful? Have we ensured that there are no regressions?

Revision history for this message
Andres Rodriguez (andreserl) wrote :

No regressions when not having a proxy?

Revision history for this message
Gavin Panella (allenap) wrote :

Andres, I'll give it a go locally.

Revision history for this message
Graham Binns (gmb) wrote :

My usual critique: Commit message lacks a wherefore (mentioning the bug would be great). Otherwise, this looks good.

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

FTR, most of the work in this branch was done by Blake, and first appeared in https://code.launchpad.net/~blake-rouse/maas/fix-1384464/+merge/239440.

Revision history for this message
Gavin Panella (allenap) wrote :

Andres, I've just imported Trusty i386 to my NUCs at home without proxy and it was fine.

Revision history for this message
Christian Reis (kiko) wrote :

I have a question about the first proxy piece, where it seems we're only using http_proxy, not https_proxy.

review: Needs Fixing
Revision history for this message
Christian Reis (kiko) wrote :

I think I understand now; filed bug 1387381. Can you land this for 1.7 as well?

review: Approve
Revision history for this message
Gavin Panella (allenap) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api/tests/test_nodegroup.py'
2--- src/maasserver/api/tests/test_nodegroup.py 2014-10-10 15:00:47 +0000
3+++ src/maasserver/api/tests/test_nodegroup.py 2014-10-29 20:22:20 +0000
4@@ -17,6 +17,7 @@
5 import httplib
6 import json
7 from textwrap import dedent
8+from urlparse import urlparse
9
10 import bson
11 from django.contrib.auth.models import AnonymousUser
12@@ -27,6 +28,7 @@
13 NODEGROUP_STATUS_CHOICES,
14 )
15 from maasserver.models import (
16+ Config,
17 DownloadProgress,
18 NodeGroup,
19 nodegroup as nodegroup_module,
20@@ -472,6 +474,8 @@
21 self.assertItemsEqual([node.system_id], parsed_result)
22
23 def test_nodegroup_import_boot_images_calls_ImportBootImages(self):
24+ proxy = 'http://%s.example.com' % factory.make_name('proxy')
25+ Config.objects.set_config('http_proxy', proxy)
26 sources = [get_simplestream_endpoint()]
27 fake_client = Mock()
28 mock_getClientFor = self.patch(nodegroup_module, 'getClientFor')
29@@ -488,7 +492,9 @@
30 explain_unexpected_response(httplib.OK, response))
31 self.assertThat(
32 fake_client,
33- MockCalledOnceWith(ImportBootImages, sources=sources))
34+ MockCalledOnceWith(
35+ ImportBootImages, sources=sources,
36+ http_proxy=urlparse(proxy), https_proxy=urlparse(proxy)))
37
38 def test_nodegroup_import_boot_images_denied_if_not_admin(self):
39 nodegroup = factory.make_NodeGroup()
40
41=== modified file 'src/maasserver/models/nodegroup.py'
42--- src/maasserver/models/nodegroup.py 2014-10-10 15:00:47 +0000
43+++ src/maasserver/models/nodegroup.py 2014-10-29 20:22:20 +0000
44@@ -17,6 +17,7 @@
45 'NODEGROUP_CLUSTER_NAME_TEMPLATE',
46 ]
47
48+from urlparse import urlparse
49
50 from apiclient.creds import convert_tuple_to_string
51 from crochet import TimeoutError
52@@ -295,6 +296,7 @@
53 the images that are exposed there.
54 """
55 # Avoid circular imports
56+ from maasserver.models import Config
57 from maasserver.bootresources import get_simplestream_endpoint
58 try:
59 client = getClientFor(self.uuid, timeout=1)
60@@ -303,7 +305,12 @@
61 # the cluster is down, it will do an import on start up.
62 return
63 sources = [get_simplestream_endpoint()]
64- return client(ImportBootImages, sources=sources)
65+ http_proxy = Config.objects.get_config("http_proxy")
66+ if http_proxy is not None:
67+ http_proxy = urlparse(http_proxy)
68+ return client(
69+ ImportBootImages, sources=sources,
70+ http_proxy=http_proxy, https_proxy=http_proxy)
71
72 def add_seamicro15k(self, mac, username, password, power_control=None):
73 """ Add all of the specified cards the Seamicro SM15000 chassis at the
74
75=== modified file 'src/maasserver/models/tests/test_nodegroup.py'
76--- src/maasserver/models/tests/test_nodegroup.py 2014-09-30 18:13:21 +0000
77+++ src/maasserver/models/tests/test_nodegroup.py 2014-10-29 20:22:20 +0000
78@@ -14,6 +14,7 @@
79 __metaclass__ = type
80 __all__ = []
81
82+from urlparse import urlparse
83
84 from apiclient.creds import convert_string_to_tuple
85 from django.db.models.signals import post_save
86@@ -26,6 +27,7 @@
87 NODEGROUPINTERFACE_MANAGEMENT,
88 )
89 from maasserver.models import (
90+ Config,
91 NodeGroup,
92 nodegroup as nodegroup_module,
93 )
94@@ -381,6 +383,8 @@
95 mock_getClientFor, MockCalledOnceWith(nodegroup.uuid, timeout=1))
96
97 def test_import_boot_images_calls_client_with_resource_endpoint(self):
98+ proxy = 'http://%s.example.com' % factory.make_name('proxy')
99+ Config.objects.set_config('http_proxy', proxy)
100 sources = [get_simplestream_endpoint()]
101 fake_client = Mock()
102 mock_getClientFor = self.patch(nodegroup_module, 'getClientFor')
103@@ -389,7 +393,9 @@
104 nodegroup.import_boot_images()
105 self.assertThat(
106 fake_client,
107- MockCalledOnceWith(ImportBootImages, sources=sources))
108+ MockCalledOnceWith(
109+ ImportBootImages, sources=sources,
110+ http_proxy=urlparse(proxy), https_proxy=urlparse(proxy)))
111
112 def test_import_boot_images_does_nothing_if_no_connection_to_cluster(self):
113 mock_getClientFor = self.patch(nodegroup_module, 'getClientFor')
114@@ -401,6 +407,8 @@
115 self.assertThat(mock_get_simplestreams_endpoint, MockNotCalled())
116
117 def test_import_boot_images_end_to_end(self):
118+ proxy = 'http://%s.example.com' % factory.make_name('proxy')
119+ Config.objects.set_config('http_proxy', proxy)
120 nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ACCEPTED)
121
122 self.useFixture(RegionEventLoopFixture("rpc"))
123@@ -413,7 +421,9 @@
124
125 self.assertThat(
126 protocol.ImportBootImages,
127- MockCalledOnceWith(ANY, sources=[get_simplestream_endpoint()]))
128+ MockCalledOnceWith(
129+ ANY, sources=[get_simplestream_endpoint()],
130+ http_proxy=urlparse(proxy), https_proxy=urlparse(proxy)))
131
132 def test_is_connected_returns_true_if_connection(self):
133 mock_getClientFor = self.patch(nodegroup_module, 'getClientFor')
134
135=== modified file 'src/provisioningserver/boot/utils.py'
136--- src/provisioningserver/boot/utils.py 2014-08-13 21:49:35 +0000
137+++ src/provisioningserver/boot/utils.py 2014-10-29 20:22:20 +0000
138@@ -46,7 +46,14 @@
139 :param url: URL to download file
140 :returns: File data, or None
141 """
142- response = urllib2.urlopen(url)
143+ from urllib import getproxies
144+ from twisted.python import log
145+ log.msg("Proxies: %s" % getproxies())
146+
147+ # Build a new opener so that the environment is checked for proxy
148+ # URLs. Using urllib2.urlopen() means that we'd only be using the
149+ # proxies as defined when urlopen() was called the first time.
150+ response = urllib2.build_opener().open(url)
151 return response.read()
152
153
154
155=== modified file 'src/provisioningserver/pserv_services/image_download_service.py'
156--- src/provisioningserver/pserv_services/image_download_service.py 2014-10-02 11:53:59 +0000
157+++ src/provisioningserver/pserv_services/image_download_service.py 2014-10-29 20:22:20 +0000
158@@ -26,6 +26,7 @@
159 from provisioningserver.rpc.region import (
160 GetBootSources,
161 GetBootSourcesV2,
162+ GetProxies,
163 )
164 from provisioningserver.utils.twisted import (
165 pause,
166@@ -108,7 +109,16 @@
167
168 # Get sources from region
169 sources = yield self._get_boot_sources(client)
170- yield import_boot_images(sources.get("sources"))
171+ # Get http proxy from region
172+ proxies = yield client(GetProxies)
173+
174+ def get_proxy_url(scheme):
175+ url = proxies.get(scheme) # url is a ParsedResult.
176+ return None if url is None else url.geturl()
177+
178+ yield import_boot_images(
179+ sources.get("sources"), get_proxy_url("http"),
180+ get_proxy_url("https"))
181
182 @inlineCallbacks
183 def maybe_start_download(self):
184
185=== modified file 'src/provisioningserver/pserv_services/tests/test_image_download_service.py'
186--- src/provisioningserver/pserv_services/tests/test_image_download_service.py 2014-10-02 11:53:59 +0000
187+++ src/provisioningserver/pserv_services/tests/test_image_download_service.py 2014-10-29 20:22:20 +0000
188@@ -14,8 +14,8 @@
189 __metaclass__ = type
190 __all__ = []
191
192-
193 from datetime import timedelta
194+from urlparse import urlparse
195
196 from fixtures import FakeLogger
197 from maastesting.factory import factory
198@@ -139,10 +139,15 @@
199 tftppath, 'maas_meta_last_modified')
200 one_week = timedelta(minutes=15).total_seconds()
201 maas_meta_last_modified.return_value = clock.seconds() - one_week
202+ http_proxy = factory.make_simple_http_url()
203+ https_proxy = factory.make_simple_http_url()
204 rpc_client = Mock()
205 client_call = Mock()
206 client_call.side_effect = [
207 defer.succeed(dict(sources=sentinel.sources)),
208+ defer.succeed(dict(
209+ http=urlparse(http_proxy),
210+ https=urlparse(https_proxy))),
211 ]
212 rpc_client.getClient.return_value = client_call
213
214@@ -158,7 +163,8 @@
215 service.startService()
216 self.assertThat(
217 deferToThread, MockCalledOnceWith(
218- _run_import, sentinel.sources))
219+ _run_import, sentinel.sources, http_proxy=http_proxy,
220+ https_proxy=https_proxy))
221
222 def test_no_download_if_no_rpc_connections(self):
223 rpc_client = Mock()
224
225=== modified file 'src/provisioningserver/rpc/boot_images.py'
226--- src/provisioningserver/rpc/boot_images.py 2014-09-30 10:05:55 +0000
227+++ src/provisioningserver/rpc/boot_images.py 2014-10-29 20:22:20 +0000
228@@ -18,6 +18,7 @@
229 "is_import_boot_images_running",
230 ]
231
232+from urlparse import urlparse
233
234 from provisioningserver import concurrency
235 from provisioningserver.auth import get_maas_user_gpghome
236@@ -35,8 +36,18 @@
237 Config.load_from_cache()['tftp']['resource_root'])
238
239
240+def get_hosts_from_sources(sources):
241+ """Return set of hosts that are contained in the given sources."""
242+ hosts = set()
243+ for source in sources:
244+ url = urlparse(source['url'])
245+ if url.hostname is not None:
246+ hosts.add(url.hostname)
247+ return hosts
248+
249+
250 @synchronous
251-def _run_import(sources):
252+def _run_import(sources, http_proxy=None, https_proxy=None):
253 """Run the import.
254
255 This is function is synchronous so it must be called with deferToThread.
256@@ -44,15 +55,25 @@
257 variables = {
258 'GNUPGHOME': get_maas_user_gpghome(),
259 }
260+ if http_proxy is not None:
261+ variables['http_proxy'] = http_proxy
262+ if https_proxy is not None:
263+ variables['https_proxy'] = https_proxy
264+ # Communication to the sources and loopback should not go through proxy.
265+ no_proxy_hosts = ["localhost", "127.0.0.1", "::1"]
266+ no_proxy_hosts += list(get_hosts_from_sources(sources))
267+ variables['no_proxy'] = ','.join(no_proxy_hosts)
268 with environment_variables(variables):
269 boot_resources.import_images(sources)
270
271
272-def import_boot_images(sources):
273+def import_boot_images(sources, http_proxy=None, https_proxy=None):
274 """Imports the boot images from the given sources."""
275 lock = concurrency.boot_images
276 if not lock.locked:
277- return lock.run(deferToThread, _run_import, sources)
278+ return lock.run(
279+ deferToThread, _run_import, sources,
280+ http_proxy=http_proxy, https_proxy=https_proxy)
281
282
283 def is_import_boot_images_running():
284
285=== modified file 'src/provisioningserver/rpc/cluster.py'
286--- src/provisioningserver/rpc/cluster.py 2014-10-02 16:21:43 +0000
287+++ src/provisioningserver/rpc/cluster.py 2014-10-29 20:22:20 +0000
288@@ -353,6 +353,8 @@
289 (b"arches", amp.ListOf(amp.Unicode())),
290 (b"subarches", amp.ListOf(amp.Unicode())),
291 (b"labels", amp.ListOf(amp.Unicode()))]))])),
292+ (b"http_proxy", ParsedURL(optional=True)),
293+ (b"https_proxy", ParsedURL(optional=True)),
294 ]
295 response = []
296 errors = []
297
298=== modified file 'src/provisioningserver/rpc/clusterservice.py'
299--- src/provisioningserver/rpc/clusterservice.py 2014-10-23 21:11:15 +0000
300+++ src/provisioningserver/rpc/clusterservice.py 2014-10-29 20:22:20 +0000
301@@ -138,13 +138,16 @@
302 return {"images": list_boot_images()}
303
304 @cluster.ImportBootImages.responder
305- def import_boot_images(self, sources):
306+ def import_boot_images(self, sources, http_proxy=None, https_proxy=None):
307 """import_boot_images()
308
309 Implementation of
310 :py:class:`~provisioningserver.rpc.cluster.ImportBootImages`.
311 """
312- import_boot_images(sources)
313+ get_proxy_url = lambda url: None if url is None else url.geturl()
314+ import_boot_images(
315+ sources, http_proxy=get_proxy_url(http_proxy),
316+ https_proxy=get_proxy_url(https_proxy))
317 return {}
318
319 @cluster.IsImportBootImagesRunning.responder
320
321=== modified file 'src/provisioningserver/rpc/tests/test_boot_images.py'
322--- src/provisioningserver/rpc/tests/test_boot_images.py 2014-09-30 10:05:55 +0000
323+++ src/provisioningserver/rpc/tests/test_boot_images.py 2014-10-29 20:22:20 +0000
324@@ -15,6 +15,7 @@
325 __all__ = []
326
327 import os
328+from random import randint
329
330 from maastesting.factory import factory
331 from maastesting.matchers import (
332@@ -30,6 +31,7 @@
333 from provisioningserver.rpc import boot_images
334 from provisioningserver.rpc.boot_images import (
335 _run_import,
336+ get_hosts_from_sources,
337 import_boot_images,
338 is_import_boot_images_running,
339 list_boot_images,
340@@ -37,10 +39,25 @@
341 from provisioningserver.testing.config import BootSourcesFixture
342 from provisioningserver.testing.testcase import PservTestCase
343 from provisioningserver.utils.twisted import pause
344+from testtools.matchers import Equals
345 from twisted.internet import defer
346 from twisted.internet.task import Clock
347
348
349+def make_sources():
350+ hosts = [factory.make_name('host').lower() for _ in range(3)]
351+ urls = [
352+ 'http://%s:%s/images-stream/streams/v1/index.json' % (
353+ host, randint(1, 1000))
354+ for host in hosts
355+ ]
356+ sources = [
357+ {'url': url, 'selections': []}
358+ for url in urls
359+ ]
360+ return sources, hosts
361+
362+
363 class TestListBootImages(PservTestCase):
364
365 def test__calls_list_boot_images_with_resource_root(self):
366@@ -56,6 +73,13 @@
367 MockCalledOnceWith(sentinel.resource_root))
368
369
370+class TestGetHostsFromSources(PservTestCase):
371+
372+ def test__returns_set_of_hosts_from_sources(self):
373+ sources, hosts = make_sources()
374+ self.assertItemsEqual(hosts, get_hosts_from_sources(sources))
375+
376+
377 class TestRunImport(PservTestCase):
378
379 def make_archive_url(self, name=None):
380@@ -95,22 +119,29 @@
381 _run_import(sources=[])
382 self.assertEqual(home, fake.env['GNUPGHOME'])
383
384+ def test__run_import_sets_proxy_if_given(self):
385+ proxy = 'http://%s.example.com' % factory.make_name('proxy')
386+ fake = self.patch_boot_resources_function()
387+ _run_import(sources=[], http_proxy=proxy, https_proxy=proxy)
388+ self.expectThat(fake.env['http_proxy'], Equals(proxy))
389+ self.expectThat(fake.env['https_proxy'], Equals(proxy))
390+
391+ def test__run_import_sets_proxy_for_loopback(self):
392+ fake = self.patch_boot_resources_function()
393+ _run_import(sources=[])
394+ self.assertEqual(fake.env['no_proxy'], "localhost,127.0.0.1,::1")
395+
396+ def test__run_import_sets_proxy_for_source_host(self):
397+ sources, hosts = make_sources()
398+ fake = self.patch_boot_resources_function()
399+ _run_import(sources=sources)
400+ self.assertItemsEqual(
401+ fake.env['no_proxy'].split(','),
402+ ["localhost", "127.0.0.1", "::1"] + hosts)
403+
404 def test__run_import_accepts_sources_parameter(self):
405 fake = self.patch(boot_resources, 'import_images')
406- sources = [
407- {
408- 'path': "http://example.com",
409- 'selections': [
410- {
411- 'os': "ubuntu",
412- 'release': "trusty",
413- 'arches': ["amd64"],
414- 'subarches': ["generic"],
415- 'labels': ["release"]
416- },
417- ],
418- },
419- ]
420+ sources, _ = make_sources()
421 _run_import(sources=sources)
422 self.assertThat(fake, MockCalledOnceWith(sources))
423
424@@ -136,7 +167,8 @@
425 yield import_boot_images(sentinel.sources)
426 self.assertThat(
427 deferToThread, MockCalledOnceWith(
428- _run_import, sentinel.sources))
429+ _run_import, sentinel.sources,
430+ http_proxy=None, https_proxy=None))
431
432 def test__takes_lock_when_running(self):
433 clock = Clock()
434
435=== modified file 'src/provisioningserver/rpc/tests/test_clusterservice.py'
436--- src/provisioningserver/rpc/tests/test_clusterservice.py 2014-10-14 16:30:50 +0000
437+++ src/provisioningserver/rpc/tests/test_clusterservice.py 2014-10-29 20:22:20 +0000
438@@ -323,7 +323,26 @@
439
440 self.assertThat(
441 import_boot_images,
442- MockCalledOnceWith(sources))
443+ MockCalledOnceWith(sources, http_proxy=None, https_proxy=None))
444+
445+ @inlineCallbacks
446+ def test_import_boot_images_calls_import_boot_images_with_proxies(self):
447+ import_boot_images = self.patch(clusterservice, "import_boot_images")
448+
449+ proxy = 'http://%s.example.com' % factory.make_name('proxy')
450+ parsed_proxy = urlparse(proxy)
451+
452+ yield call_responder(
453+ Cluster(), cluster.ImportBootImages, {
454+ 'sources': [],
455+ 'http_proxy': parsed_proxy,
456+ 'https_proxy': parsed_proxy,
457+ })
458+
459+ self.assertThat(
460+ import_boot_images,
461+ MockCalledOnceWith(
462+ [], http_proxy=proxy, https_proxy=proxy))
463
464
465 class TestClusterProtocol_IsImportBootImagesRunning(MAASTestCase):