Merge lp:~allenap/maas/fix-1384464 into lp:~maas-committers/maas/trunk
- fix-1384464
- Merge into trunk
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 | ||||
Related bugs: |
|
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.
Description of the change
Gavin Panella (allenap) wrote : | # |
Andres Rodriguez (andreserl) wrote : | # |
So tests were successful? Have we ensured that there are no regressions?
Andres Rodriguez (andreserl) wrote : | # |
No regressions when not having a proxy?
Gavin Panella (allenap) wrote : | # |
Andres, I'll give it a go locally.
Graham Binns (gmb) wrote : | # |
My usual critique: Commit message lacks a wherefore (mentioning the bug would be great). Otherwise, this looks good.
Gavin Panella (allenap) wrote : | # |
FTR, most of the work in this branch was done by Blake, and first appeared in https:/
Gavin Panella (allenap) wrote : | # |
Andres, I've just imported Trusty i386 to my NUCs at home without proxy and it was fine.
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.
Christian Reis (kiko) wrote : | # |
I think I understand now; filed bug 1387381. Can you land this for 1.7 as well?
Gavin Panella (allenap) : | # |
Preview Diff
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): |
Many many thanks to Narinder Gupta for exhaustively testing this.