Merge lp:~cjwatson/launchpad/explicit-proxy-mirror-prober into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18712
Proposed branch: lp:~cjwatson/launchpad/explicit-proxy-mirror-prober
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/no-default-http-proxy
Diff against target: 585 lines (+167/-110)
5 files modified
cronscripts/distributionmirror-prober.py (+4/-9)
lib/lp/registry/scripts/distributionmirror_prober.py (+24/-29)
lib/lp/registry/tests/test_distributionmirror_prober.py (+81/-50)
lib/lp/services/tests/test_timeout.py (+50/-20)
lib/lp/services/timeout.py (+8/-2)
To merge this branch: bzr merge lp:~cjwatson/launchpad/explicit-proxy-mirror-prober
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+348543@code.launchpad.net

Commit message

Convert the mirror prober to use explicit proxy configuration.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'cronscripts/distributionmirror-prober.py'
--- cronscripts/distributionmirror-prober.py 2013-01-07 02:40:55 +0000
+++ cronscripts/distributionmirror-prober.py 2018-07-02 11:30:55 +0000
@@ -1,14 +1,12 @@
1#!/usr/bin/python -S1#!/usr/bin/python -S
2#2#
3# Copyright 2009-2011 Canonical Ltd. This software is licensed under the3# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
4# GNU Affero General Public License version 3 (see the file LICENSE).4# GNU Affero General Public License version 3 (see the file LICENSE).
55
6"""Script to probe distribution mirrors and check how up-to-date they are."""6"""Script to probe distribution mirrors and check how up-to-date they are."""
77
8import _pythonpath8import _pythonpath
99
10import os
11
12from lp.registry.interfaces.distributionmirror import MirrorContent10from lp.registry.interfaces.distributionmirror import MirrorContent
13from lp.registry.scripts.distributionmirror_prober import DistroMirrorProber11from lp.registry.scripts.distributionmirror_prober import DistroMirrorProber
14from lp.services.config import config12from lp.services.config import config
@@ -16,6 +14,7 @@
16 LaunchpadCronScript,14 LaunchpadCronScript,
17 LaunchpadScriptFailure,15 LaunchpadScriptFailure,
18 )16 )
17from lp.services.timeout import set_default_timeout_function
1918
2019
21class DistroMirrorProberScript(LaunchpadCronScript):20class DistroMirrorProberScript(LaunchpadCronScript):
@@ -49,12 +48,8 @@
49 'Wrong value for argument --content-type: %s'48 'Wrong value for argument --content-type: %s'
50 % self.options.content_type)49 % self.options.content_type)
5150
52 if config.distributionmirrorprober.use_proxy:51 set_default_timeout_function(
53 os.environ['http_proxy'] = config.launchpad.http_proxy52 lambda: config.distributionmirrorprober.timeout)
54 self.logger.debug("Using %s as proxy." % os.environ['http_proxy'])
55 else:
56 self.logger.debug("Not using any proxy.")
57
58 DistroMirrorProber(self.txn, self.logger).probe(53 DistroMirrorProber(self.txn, self.logger).probe(
59 content_type, self.options.no_remote_hosts, self.options.force,54 content_type, self.options.no_remote_hosts, self.options.force,
60 self.options.max_mirrors, not self.options.no_owner_notification)55 self.options.max_mirrors, not self.options.no_owner_notification)
6156
=== modified file 'lib/lp/registry/scripts/distributionmirror_prober.py'
--- lib/lp/registry/scripts/distributionmirror_prober.py 2017-05-22 09:33:45 +0000
+++ lib/lp/registry/scripts/distributionmirror_prober.py 2018-07-02 11:30:55 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2017 Canonical Ltd. This software is licensed under the1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -10,12 +10,12 @@
10import httplib10import httplib
11import itertools11import itertools
12import logging12import logging
13import os13import os.path
14from StringIO import StringIO14from StringIO import StringIO
15import urllib15import urllib
16import urllib2
17import urlparse16import urlparse
1817
18import requests
19from twisted.internet import (19from twisted.internet import (
20 defer,20 defer,
21 protocol,21 protocol,
@@ -36,6 +36,7 @@
36from lp.registry.interfaces.distroseries import IDistroSeries36from lp.registry.interfaces.distroseries import IDistroSeries
37from lp.services.config import config37from lp.services.config import config
38from lp.services.librarian.interfaces import ILibraryFileAliasSet38from lp.services.librarian.interfaces import ILibraryFileAliasSet
39from lp.services.timeout import urlfetch
39from lp.services.webapp import canonical_url40from lp.services.webapp import canonical_url
40from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries41from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
4142
@@ -202,9 +203,9 @@
202 request_path = None203 request_path = None
203204
204 # Details of the URL of the host in which we'll connect, which will only205 # Details of the URL of the host in which we'll connect, which will only
205 # be different from request_* in case we have an http_proxy environment206 # be different from request_* in case we have a configured http_proxy --
206 # variable --in that case the scheme, host and port will be the ones207 # in that case the scheme, host and port will be the ones extracted from
207 # extracted from http_proxy and the path will be self.url208 # http_proxy and the path will be self.url.
208 connect_scheme = None209 connect_scheme = None
209 connect_host = None210 connect_host = None
210 connect_port = None211 connect_port = None
@@ -279,7 +280,7 @@
279 # XXX Guilherme Salgado 2006-09-19:280 # XXX Guilherme Salgado 2006-09-19:
280 # We don't actually know how to handle FTP responses, but we281 # We don't actually know how to handle FTP responses, but we
281 # expect to be behind a squid HTTP proxy with the patch at282 # expect to be behind a squid HTTP proxy with the patch at
282 # http://www.squid-cache.org/bugs/show_bug.cgi?id=1758 applied. So, if283 # https://bugs.squid-cache.org/show_bug.cgi?id=1758 applied. So, if
283 # you encounter any problems with FTP URLs you'll probably have to nag284 # you encounter any problems with FTP URLs you'll probably have to nag
284 # the sysadmins to fix squid for you.285 # the sysadmins to fix squid for you.
285 if scheme not in ('http', 'ftp'):286 if scheme not in ('http', 'ftp'):
@@ -296,9 +297,9 @@
296 if self.request_host not in host_timeouts:297 if self.request_host not in host_timeouts:
297 host_timeouts[self.request_host] = 0298 host_timeouts[self.request_host] = 0
298299
299 # If the http_proxy variable is set, we want to use it as the host300 # If launchpad.http_proxy is set in our configuration, we want to
300 # we're going to connect to.301 # use it as the host we're going to connect to.
301 proxy = os.getenv('http_proxy')302 proxy = config.launchpad.http_proxy
302 if proxy:303 if proxy:
303 scheme, host, port, dummy = _parse(proxy)304 scheme, host, port, dummy = _parse(proxy)
304 path = url305 path = url
@@ -612,31 +613,25 @@
612 return failure613 return failure
613614
614615
615def _build_request_for_cdimage_file_list(url):
616 headers = {'Pragma': 'no-cache', 'Cache-control': 'no-cache'}
617 return urllib2.Request(url, headers=headers)
618
619
620def _get_cdimage_file_list():616def _get_cdimage_file_list():
621 url = config.distributionmirrorprober.cdimage_file_list_url617 url = config.distributionmirrorprober.cdimage_file_list_url
618 # In test environments, this may be a file: URL. Adjust it to be in a
619 # form that requests can cope with (i.e. using an absolute path).
620 parsed_url = urlparse.urlparse(url)
621 if parsed_url.scheme == 'file' and not os.path.isabs(parsed_url.path):
622 assert parsed_url.path == parsed_url[2]
623 parsed_url = list(parsed_url)
624 parsed_url[2] = os.path.join(config.root, parsed_url[2])
625 url = urlparse.urlunparse(parsed_url)
622 try:626 try:
623 return urllib2.urlopen(_build_request_for_cdimage_file_list(url))627 return urlfetch(
624 except urllib2.URLError as e:628 url, headers={'Pragma': 'no-cache', 'Cache-control': 'no-cache'},
629 trust_env=False, use_proxy=True, allow_file=True)
630 except requests.RequestException as e:
625 raise UnableToFetchCDImageFileList(631 raise UnableToFetchCDImageFileList(
626 'Unable to fetch %s: %s' % (url, e))632 'Unable to fetch %s: %s' % (url, e))
627633
628634
629def restore_http_proxy(http_proxy):
630 """Restore the http_proxy environment variable to the given value."""
631 if http_proxy is None:
632 try:
633 del os.environ['http_proxy']
634 except KeyError:
635 pass
636 else:
637 os.environ['http_proxy'] = http_proxy
638
639
640def get_expected_cdimage_paths():635def get_expected_cdimage_paths():
641 """Get all paths where we can find CD image files on a cdimage mirror.636 """Get all paths where we can find CD image files on a cdimage mirror.
642637
@@ -648,7 +643,7 @@
648 UnableToFetchCDImageFileList exception will be raised.643 UnableToFetchCDImageFileList exception will be raised.
649 """644 """
650 d = {}645 d = {}
651 for line in _get_cdimage_file_list().readlines():646 for line in _get_cdimage_file_list().iter_lines():
652 flavour, seriesname, path, size = line.split('\t')647 flavour, seriesname, path, size = line.split('\t')
653 paths = d.setdefault((flavour, seriesname), [])648 paths = d.setdefault((flavour, seriesname), [])
654 paths.append(path.lstrip('/'))649 paths.append(path.lstrip('/'))
655650
=== modified file 'lib/lp/registry/tests/test_distributionmirror_prober.py'
--- lib/lp/registry/tests/test_distributionmirror_prober.py 2018-01-02 16:10:26 +0000
+++ lib/lp/registry/tests/test_distributionmirror_prober.py 2018-07-02 11:30:55 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2016 Canonical Ltd. This software is licensed under the1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""distributionmirror-prober tests."""4"""distributionmirror-prober tests."""
@@ -13,13 +13,23 @@
13from StringIO import StringIO13from StringIO import StringIO
1414
15from lazr.uri import URI15from lazr.uri import URI
16import responses
16from sqlobject import SQLObjectNotFound17from sqlobject import SQLObjectNotFound
18from testtools.matchers import (
19 ContainsDict,
20 Equals,
21 MatchesStructure,
22 )
23from testtools.twistedsupport import (
24 assert_fails_with,
25 AsynchronousDeferredRunTest,
26 AsynchronousDeferredRunTestForBrokenTwisted,
27 )
17from twisted.internet import (28from twisted.internet import (
18 defer,29 defer,
19 reactor,30 reactor,
20 )31 )
21from twisted.python.failure import Failure32from twisted.python.failure import Failure
22from twisted.trial.unittest import TestCase as TrialTestCase
23from twisted.web import server33from twisted.web import server
24from zope.component import getUtility34from zope.component import getUtility
25from zope.security.proxy import removeSecurityProxy35from zope.security.proxy import removeSecurityProxy
@@ -29,7 +39,7 @@
29from lp.registry.model.distributionmirror import DistributionMirror39from lp.registry.model.distributionmirror import DistributionMirror
30from lp.registry.scripts import distributionmirror_prober40from lp.registry.scripts import distributionmirror_prober
31from lp.registry.scripts.distributionmirror_prober import (41from lp.registry.scripts.distributionmirror_prober import (
32 _build_request_for_cdimage_file_list,42 _get_cdimage_file_list,
33 ArchiveMirrorProberCallbacks,43 ArchiveMirrorProberCallbacks,
34 BadResponseCode,44 BadResponseCode,
35 ConnectionSkipped,45 ConnectionSkipped,
@@ -50,7 +60,6 @@
50 RedirectAwareProberProtocol,60 RedirectAwareProberProtocol,
51 RedirectToDifferentFile,61 RedirectToDifferentFile,
52 RequestManager,62 RequestManager,
53 restore_http_proxy,
54 should_skip_host,63 should_skip_host,
55 UnknownURLSchemeAfterRedirect,64 UnknownURLSchemeAfterRedirect,
56 )65 )
@@ -59,6 +68,7 @@
59 )68 )
60from lp.services.config import config69from lp.services.config import config
61from lp.services.daemons.tachandler import TacTestSetup70from lp.services.daemons.tachandler import TacTestSetup
71from lp.services.timeout import default_timeout
62from lp.testing import (72from lp.testing import (
63 clean_up_reactor,73 clean_up_reactor,
64 TestCase,74 TestCase,
@@ -93,39 +103,54 @@
93 return os.path.join(self.root, 'distributionmirror_http_server.log')103 return os.path.join(self.root, 'distributionmirror_http_server.log')
94104
95105
96class TestProberProtocolAndFactory(TrialTestCase):106class TestProberProtocolAndFactory(TestCase):
97107
98 layer = TwistedLayer108 layer = TwistedLayer
109 run_tests_with = AsynchronousDeferredRunTestForBrokenTwisted.make_factory(
110 timeout=30)
99111
100 def setUp(self):112 def setUp(self):
101 self.orig_proxy = os.getenv('http_proxy')113 super(TestProberProtocolAndFactory, self).setUp()
102 root = DistributionMirrorTestHTTPServer()114 root = DistributionMirrorTestHTTPServer()
103 site = server.Site(root)115 site = server.Site(root)
104 site.displayTracebacks = False116 site.displayTracebacks = False
105 self.listening_port = reactor.listenTCP(0, site)117 self.listening_port = reactor.listenTCP(0, site)
118 self.addCleanup(self.listening_port.stopListening)
106 self.port = self.listening_port.getHost().port119 self.port = self.listening_port.getHost().port
107 self.urls = {'timeout': u'http://localhost:%s/timeout' % self.port,120 self.urls = {'timeout': u'http://localhost:%s/timeout' % self.port,
108 '200': u'http://localhost:%s/valid-mirror' % self.port,121 '200': u'http://localhost:%s/valid-mirror' % self.port,
109 '500': u'http://localhost:%s/error' % self.port,122 '500': u'http://localhost:%s/error' % self.port,
110 '404': u'http://localhost:%s/invalid-mirror' % self.port}123 '404': u'http://localhost:%s/invalid-mirror' % self.port}
111124 self.pushConfig('launchpad', http_proxy=None)
112 def tearDown(self):
113 restore_http_proxy(self.orig_proxy)
114 return self.listening_port.stopListening()
115125
116 def _createProberAndProbe(self, url):126 def _createProberAndProbe(self, url):
117 prober = ProberFactory(url)127 prober = ProberFactory(url)
118 return prober.probe()128 return prober.probe()
119129
120 def test_environment_http_proxy_is_handled_correctly(self):130 def test_config_no_http_proxy(self):
121 os.environ['http_proxy'] = 'http://squid.internal:3128'131 prober = ProberFactory(self.urls['200'])
122 prober = ProberFactory(self.urls['200'])132 self.assertThat(prober, MatchesStructure.byEquality(
123 self.assertEqual(prober.request_host, 'localhost')133 request_scheme='http',
124 self.assertEqual(prober.request_port, self.port)134 request_host='localhost',
125 self.assertEqual(prober.request_path, '/valid-mirror')135 request_port=self.port,
126 self.assertEqual(prober.connect_host, 'squid.internal')136 request_path='/valid-mirror',
127 self.assertEqual(prober.connect_port, 3128)137 connect_scheme='http',
128 self.assertEqual(prober.connect_path, self.urls['200'])138 connect_host='localhost',
139 connect_port=self.port,
140 connect_path='/valid-mirror'))
141
142 def test_config_http_proxy(self):
143 self.pushConfig('launchpad', http_proxy='http://squid.internal:3128')
144 prober = ProberFactory(self.urls['200'])
145 self.assertThat(prober, MatchesStructure.byEquality(
146 request_scheme='http',
147 request_host='localhost',
148 request_port=self.port,
149 request_path='/valid-mirror',
150 connect_scheme='http',
151 connect_host='squid.internal',
152 connect_port=3128,
153 connect_path=self.urls['200']))
129154
130 def test_connect_cancels_existing_timeout_call(self):155 def test_connect_cancels_existing_timeout_call(self):
131 prober = ProberFactory(self.urls['200'])156 prober = ProberFactory(self.urls['200'])
@@ -161,14 +186,10 @@
161 return deferred.addCallback(restore_connect, orig_connect)186 return deferred.addCallback(restore_connect, orig_connect)
162187
163 def test_connect_to_proxy_when_http_proxy_exists(self):188 def test_connect_to_proxy_when_http_proxy_exists(self):
164 os.environ['http_proxy'] = 'http://squid.internal:3128'189 self.pushConfig('launchpad', http_proxy='http://squid.internal:3128')
165 self._test_connect_to_host(self.urls['200'], 'squid.internal')190 self._test_connect_to_host(self.urls['200'], 'squid.internal')
166191
167 def test_connect_to_host_when_http_proxy_does_not_exist(self):192 def test_connect_to_host_when_http_proxy_does_not_exist(self):
168 try:
169 del os.environ['http_proxy']
170 except KeyError:
171 pass
172 self._test_connect_to_host(self.urls['200'], 'localhost')193 self._test_connect_to_host(self.urls['200'], 'localhost')
173194
174 def test_probe_sets_up_timeout_call(self):195 def test_probe_sets_up_timeout_call(self):
@@ -197,13 +218,13 @@
197 prober = RedirectAwareProberFactory(218 prober = RedirectAwareProberFactory(
198 'http://localhost:%s/redirect-infinite-loop' % self.port)219 'http://localhost:%s/redirect-infinite-loop' % self.port)
199 deferred = prober.probe()220 deferred = prober.probe()
200 return self.assertFailure(deferred, InfiniteLoopDetected)221 return assert_fails_with(deferred, InfiniteLoopDetected)
201222
202 def test_redirectawareprober_fail_on_unknown_scheme(self):223 def test_redirectawareprober_fail_on_unknown_scheme(self):
203 prober = RedirectAwareProberFactory(224 prober = RedirectAwareProberFactory(
204 'http://localhost:%s/redirect-unknown-url-scheme' % self.port)225 'http://localhost:%s/redirect-unknown-url-scheme' % self.port)
205 deferred = prober.probe()226 deferred = prober.probe()
206 return self.assertFailure(deferred, UnknownURLSchemeAfterRedirect)227 return assert_fails_with(deferred, UnknownURLSchemeAfterRedirect)
207228
208 def test_200(self):229 def test_200(self):
209 d = self._createProberAndProbe(self.urls['200'])230 d = self._createProberAndProbe(self.urls['200'])
@@ -237,15 +258,15 @@
237258
238 def test_notfound(self):259 def test_notfound(self):
239 d = self._createProberAndProbe(self.urls['404'])260 d = self._createProberAndProbe(self.urls['404'])
240 return self.assertFailure(d, BadResponseCode)261 return assert_fails_with(d, BadResponseCode)
241262
242 def test_500(self):263 def test_500(self):
243 d = self._createProberAndProbe(self.urls['500'])264 d = self._createProberAndProbe(self.urls['500'])
244 return self.assertFailure(d, BadResponseCode)265 return assert_fails_with(d, BadResponseCode)
245266
246 def test_timeout(self):267 def test_timeout(self):
247 d = self._createProberAndProbe(self.urls['timeout'])268 d = self._createProberAndProbe(self.urls['timeout'])
248 return self.assertFailure(d, ProberTimeout)269 return assert_fails_with(d, ProberTimeout)
249270
250 def test_prober_user_agent(self):271 def test_prober_user_agent(self):
251 protocol = RedirectAwareProberProtocol()272 protocol = RedirectAwareProberProtocol()
@@ -380,7 +401,7 @@
380 self.assertFalse(should_skip_host(self.host))401 self.assertFalse(should_skip_host(self.host))
381402
382403
383class TestProberFactoryRequestTimeoutRatioWithTwisted(TrialTestCase):404class TestProberFactoryRequestTimeoutRatioWithTwisted(TestCase):
384 """Tests to ensure we stop issuing requests on a given host if the405 """Tests to ensure we stop issuing requests on a given host if the
385 requests/timeouts ratio on that host is too low.406 requests/timeouts ratio on that host is too low.
386407
@@ -392,25 +413,29 @@
392 """413 """
393414
394 layer = TwistedLayer415 layer = TwistedLayer
416 run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=30)
395417
396 def setUp(self):418 def setUp(self):
397 self.orig_host_requests = dict(419 super(TestProberFactoryRequestTimeoutRatioWithTwisted, self).setUp()
398 distributionmirror_prober.host_requests)420 orig_host_requests = dict(distributionmirror_prober.host_requests)
399 self.orig_host_timeouts = dict(421 orig_host_timeouts = dict(distributionmirror_prober.host_timeouts)
400 distributionmirror_prober.host_timeouts)
401 distributionmirror_prober.host_requests = {}422 distributionmirror_prober.host_requests = {}
402 distributionmirror_prober.host_timeouts = {}423 distributionmirror_prober.host_timeouts = {}
424
425 def restore_prober_globals():
426 # Restore the globals that our tests fiddle with.
427 distributionmirror_prober.host_requests = orig_host_requests
428 distributionmirror_prober.host_timeouts = orig_host_timeouts
429
430 self.addCleanup(restore_prober_globals)
431
403 root = DistributionMirrorTestHTTPServer()432 root = DistributionMirrorTestHTTPServer()
404 site = server.Site(root)433 site = server.Site(root)
405 site.displayTracebacks = False434 site.displayTracebacks = False
406 self.listening_port = reactor.listenTCP(0, site)435 self.listening_port = reactor.listenTCP(0, site)
436 self.addCleanup(self.listening_port.stopListening)
407 self.port = self.listening_port.getHost().port437 self.port = self.listening_port.getHost().port
408438 self.pushConfig('launchpad', http_proxy=None)
409 def tearDown(self):
410 # Restore the globals that our tests fiddle with.
411 distributionmirror_prober.host_requests = self.orig_host_requests
412 distributionmirror_prober.host_timeouts = self.orig_host_timeouts
413 return self.listening_port.stopListening()
414439
415 def _createProberAndProbe(self, url):440 def _createProberAndProbe(self, url):
416 prober = ProberFactory(url)441 prober = ProberFactory(url)
@@ -455,7 +480,7 @@
455480
456 d = self._createProberAndProbe(481 d = self._createProberAndProbe(
457 u'http://%s:%s/timeout' % (host, self.port))482 u'http://%s:%s/timeout' % (host, self.port))
458 return self.assertFailure(d, ConnectionSkipped)483 return assert_fails_with(d, ConnectionSkipped)
459484
460485
461class TestMultiLock(TestCase):486class TestMultiLock(TestCase):
@@ -842,7 +867,8 @@
842 self.assertNotEqual(0, len(mirror.cdimage_series))867 self.assertNotEqual(0, len(mirror.cdimage_series))
843 # Note that calling this function won't actually probe any mirrors; we868 # Note that calling this function won't actually probe any mirrors; we
844 # need to call reactor.run() to actually start the probing.869 # need to call reactor.run() to actually start the probing.
845 probe_cdimage_mirror(mirror, StringIO(), [], logging)870 with default_timeout(15.0):
871 probe_cdimage_mirror(mirror, StringIO(), [], logging)
846 self.assertEqual(0, len(mirror.cdimage_series))872 self.assertEqual(0, len(mirror.cdimage_series))
847873
848 def test_archive_mirror_probe_function(self):874 def test_archive_mirror_probe_function(self):
@@ -856,8 +882,9 @@
856 mirror1 = DistributionMirror.byName('releases-mirror')882 mirror1 = DistributionMirror.byName('releases-mirror')
857 mirror2 = DistributionMirror.byName('releases-mirror2')883 mirror2 = DistributionMirror.byName('releases-mirror2')
858 mirror3 = DistributionMirror.byName('canonical-releases')884 mirror3 = DistributionMirror.byName('canonical-releases')
859 self._test_one_semaphore_for_each_host(885 with default_timeout(15.0):
860 mirror1, mirror2, mirror3, probe_cdimage_mirror)886 self._test_one_semaphore_for_each_host(
887 mirror1, mirror2, mirror3, probe_cdimage_mirror)
861888
862 def _test_one_semaphore_for_each_host(889 def _test_one_semaphore_for_each_host(
863 self, mirror1, mirror2, mirror3, probe_function):890 self, mirror1, mirror2, mirror3, probe_function):
@@ -905,20 +932,24 @@
905 # When using an http_proxy, even though we'll actually connect to the932 # When using an http_proxy, even though we'll actually connect to the
906 # proxy, we'll use the mirror's host as the key to find the semaphore933 # proxy, we'll use the mirror's host as the key to find the semaphore
907 # that should be used934 # that should be used
908 orig_proxy = os.getenv('http_proxy')935 self.pushConfig('launchpad', http_proxy='http://squid.internal:3128/')
909 os.environ['http_proxy'] = 'http://squid.internal:3128/'
910 probe_function(mirror3, StringIO(), [], logging)936 probe_function(mirror3, StringIO(), [], logging)
911 self.assertEqual(len(request_manager.host_locks), 2)937 self.assertEqual(len(request_manager.host_locks), 2)
912 restore_http_proxy(orig_proxy)
913938
914939
915class TestCDImageFileListFetching(TestCase):940class TestCDImageFileListFetching(TestCase):
916941
942 @responses.activate
917 def test_no_cache(self):943 def test_no_cache(self):
918 url = 'http://releases.ubuntu.com/.manifest'944 url = 'http://releases.ubuntu.com/.manifest'
919 request = _build_request_for_cdimage_file_list(url)945 self.pushConfig('distributionmirrorprober', cdimage_file_list_url=url)
920 self.assertEqual(request.headers['Pragma'], 'no-cache')946 responses.add('GET', url)
921 self.assertEqual(request.headers['Cache-control'], 'no-cache')947 with default_timeout(1.0):
948 _get_cdimage_file_list()
949 self.assertThat(responses.calls[0].request.headers, ContainsDict({
950 'Pragma': Equals('no-cache'),
951 'Cache-control': Equals('no-cache'),
952 }))
922953
923954
924class TestLoggingMixin(TestCase):955class TestLoggingMixin(TestCase):
925956
=== modified file 'lib/lp/services/tests/test_timeout.py'
--- lib/lp/services/tests/test_timeout.py 2018-06-26 19:17:19 +0000
+++ lib/lp/services/tests/test_timeout.py 2018-07-02 11:30:55 +0000
@@ -367,26 +367,6 @@
367 MatchesStructure.byEquality(status_code=200, content='Success.'))367 MatchesStructure.byEquality(status_code=200, content='Success.'))
368 t.join()368 t.join()
369369
370 def test_urlfetch_does_not_support_ftp_urls(self):
371 """urlfetch() does not support ftp urls."""
372 set_default_timeout_function(lambda: 1)
373 self.addCleanup(set_default_timeout_function, None)
374 url = 'ftp://localhost/'
375 e = self.assertRaises(InvalidSchema, urlfetch, url)
376 self.assertEqual(
377 "No connection adapters were found for '%s'" % url, str(e))
378
379 def test_urlfetch_does_not_support_file_urls_by_default(self):
380 """urlfetch() does not support file urls by default."""
381 set_default_timeout_function(lambda: 1)
382 self.addCleanup(set_default_timeout_function, None)
383 test_path = self.useFixture(TempDir()).join('file')
384 write_file(test_path, '')
385 url = 'file://' + test_path
386 e = self.assertRaises(InvalidSchema, urlfetch, url)
387 self.assertEqual(
388 "No connection adapters were found for '%s'" % url, str(e))
389
390 def test_urlfetch_no_proxy_by_default(self):370 def test_urlfetch_no_proxy_by_default(self):
391 """urlfetch does not use a proxy by default."""371 """urlfetch does not use a proxy by default."""
392 self.pushConfig('launchpad', http_proxy='http://proxy.example:3128/')372 self.pushConfig('launchpad', http_proxy='http://proxy.example:3128/')
@@ -418,6 +398,56 @@
418 {scheme: proxy for scheme in ('http', 'https')},398 {scheme: proxy for scheme in ('http', 'https')},
419 fake_send.calls[0][1]['proxies'])399 fake_send.calls[0][1]['proxies'])
420400
401 def test_urlfetch_does_not_support_ftp_urls_by_default(self):
402 """urlfetch() does not support ftp urls by default."""
403 set_default_timeout_function(lambda: 1)
404 self.addCleanup(set_default_timeout_function, None)
405 url = 'ftp://localhost/'
406 e = self.assertRaises(InvalidSchema, urlfetch, url)
407 self.assertEqual(
408 "No connection adapters were found for '%s'" % url, str(e))
409
410 def test_urlfetch_supports_ftp_urls_if_allow_ftp(self):
411 """urlfetch() supports ftp urls via a proxy if explicitly asked."""
412 sock, http_server_url = self.make_test_socket()
413 sock.listen(1)
414
415 def success_result():
416 (client_sock, client_addr) = sock.accept()
417 # We only provide a test HTTP proxy, not anything beyond that.
418 client_sock.sendall(dedent("""\
419 HTTP/1.0 200 Ok
420 Content-Type: text/plain
421 Content-Length: 8
422
423 Success."""))
424 client_sock.close()
425
426 self.pushConfig('launchpad', http_proxy=http_server_url)
427 t = threading.Thread(target=success_result)
428 t.start()
429 set_default_timeout_function(lambda: 1)
430 self.addCleanup(set_default_timeout_function, None)
431 response = urlfetch(
432 'ftp://example.com/', trust_env=False, use_proxy=True,
433 allow_ftp=True)
434 self.assertThat(response, MatchesStructure(
435 status_code=Equals(200),
436 headers=ContainsDict({'content-length': Equals('8')}),
437 content=Equals('Success.')))
438 t.join()
439
440 def test_urlfetch_does_not_support_file_urls_by_default(self):
441 """urlfetch() does not support file urls by default."""
442 set_default_timeout_function(lambda: 1)
443 self.addCleanup(set_default_timeout_function, None)
444 test_path = self.useFixture(TempDir()).join('file')
445 write_file(test_path, '')
446 url = 'file://' + test_path
447 e = self.assertRaises(InvalidSchema, urlfetch, url)
448 self.assertEqual(
449 "No connection adapters were found for '%s'" % url, str(e))
450
421 def test_urlfetch_supports_file_urls_if_allow_file(self):451 def test_urlfetch_supports_file_urls_if_allow_file(self):
422 """urlfetch() supports file urls if explicitly asked to do so."""452 """urlfetch() supports file urls if explicitly asked to do so."""
423 set_default_timeout_function(lambda: 1)453 set_default_timeout_function(lambda: 1)
424454
=== modified file 'lib/lp/services/timeout.py'
--- lib/lp/services/timeout.py 2018-06-26 19:17:19 +0000
+++ lib/lp/services/timeout.py 2018-07-02 11:30:55 +0000
@@ -324,8 +324,8 @@
324 self.session = None324 self.session = None
325325
326 @with_timeout(cleanup='cleanup')326 @with_timeout(cleanup='cleanup')
327 def fetch(self, url, trust_env=None, use_proxy=False, allow_file=False,327 def fetch(self, url, trust_env=None, use_proxy=False, allow_ftp=False,
328 output_file=None, **request_kwargs):328 allow_file=False, output_file=None, **request_kwargs):
329 """Fetch the URL using a custom HTTP handler supporting timeout.329 """Fetch the URL using a custom HTTP handler supporting timeout.
330330
331 :param url: The URL to fetch.331 :param url: The URL to fetch.
@@ -333,6 +333,7 @@
333 to determine whether it fetches proxy configuration from the333 to determine whether it fetches proxy configuration from the
334 environment.334 environment.
335 :param use_proxy: If True, use Launchpad's configured proxy.335 :param use_proxy: If True, use Launchpad's configured proxy.
336 :param allow_ftp: If True, allow ftp:// URLs.
336 :param allow_file: If True, allow file:// URLs. (Be careful to only337 :param allow_file: If True, allow file:// URLs. (Be careful to only
337 pass this if the URL is trusted.)338 pass this if the URL is trusted.)
338 :param output_file: If not None, download the response content to339 :param output_file: If not None, download the response content to
@@ -346,6 +347,9 @@
346 # Mount our custom adapters.347 # Mount our custom adapters.
347 self.session.mount("https://", CleanableHTTPAdapter())348 self.session.mount("https://", CleanableHTTPAdapter())
348 self.session.mount("http://", CleanableHTTPAdapter())349 self.session.mount("http://", CleanableHTTPAdapter())
350 # We can do FTP, but currently only via an HTTP proxy.
351 if allow_ftp and use_proxy:
352 self.session.mount("ftp://", CleanableHTTPAdapter())
349 if allow_file:353 if allow_file:
350 self.session.mount("file://", FileAdapter())354 self.session.mount("file://", FileAdapter())
351355
@@ -354,6 +358,8 @@
354 request_kwargs.setdefault("proxies", {})358 request_kwargs.setdefault("proxies", {})
355 request_kwargs["proxies"]["http"] = config.launchpad.http_proxy359 request_kwargs["proxies"]["http"] = config.launchpad.http_proxy
356 request_kwargs["proxies"]["https"] = config.launchpad.http_proxy360 request_kwargs["proxies"]["https"] = config.launchpad.http_proxy
361 if allow_ftp:
362 request_kwargs["proxies"]["ftp"] = config.launchpad.http_proxy
357 if output_file is not None:363 if output_file is not None:
358 request_kwargs["stream"] = True364 request_kwargs["stream"] = True
359 response = self.session.request(url=url, **request_kwargs)365 response = self.session.request(url=url, **request_kwargs)