Merge ~bjornt/maas:bug-1845351-update-tags-no-proxy into maas:master

Proposed by Björn Tillenius
Status: Merged
Approved by: Björn Tillenius
Approved revision: 226898c23ee945f9d843ea888ca5b9cc3a0932e2
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~bjornt/maas:bug-1845351-update-tags-no-proxy
Merge into: maas:master
Diff against target: 231 lines (+98/-15)
4 files modified
src/apiclient/maas_client.py (+11/-1)
src/apiclient/tests/test_maas_client.py (+67/-13)
src/provisioningserver/rpc/tags.py (+3/-1)
src/provisioningserver/rpc/tests/test_tags.py (+17/-0)
Reviewer Review Type Date Requested Status
MAAS Lander Needs Fixing
Alberto Donato (community) Approve
Review via email: mp+374214@code.launchpad.net

Commit message

Don't use a proxy for the rack-region API communication.

We require the rack to have direct access to the region without going
through a proxy.

Using a proxy for the rack-region communication (HTTP API only) has
partly worked, but been broken.

This fixes the test_check_tag_applied_to_all_machines() failure that
fails intermittently in CI.

To post a comment you must log in.
Revision history for this message
Alberto Donato (ack) wrote :

LGTM, +1

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b bug-1845351-update-tags-no-proxy lp:~bjornt/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/6771/console
COMMIT: 1523767a1851eca9202edbea70d553679a429d51

review: Needs Fixing
e9296dc... by Björn Tillenius

Fix test failure.

Revision history for this message
Björn Tillenius (bjornt) :
226898c... by Björn Tillenius

Typo.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/apiclient/maas_client.py b/src/apiclient/maas_client.py
2index d0607e7..95d98e1 100644
3--- a/src/apiclient/maas_client.py
4+++ b/src/apiclient/maas_client.py
5@@ -87,8 +87,14 @@ class MAASDispatcher:
6 Be careful when changing its API: this class is designed so that it
7 can be replaced with a Twisted-enabled alternative. See the MAAS
8 provider in Juju for the code this would require.
9+
10+ @ivar autodetect_proxies: Extract proxy information from the
11+ environment variables (http_proxy, no_proxy). Default True
12 """
13
14+ def __init__(self, autodetect_proxies=True):
15+ self.autodetect_proxies = autodetect_proxies
16+
17 def dispatch_query(self, request_url, headers, method="GET", data=None):
18 """Synchronously dispatch an OAuth-signed request to L{request_url}.
19
20@@ -118,9 +124,13 @@ class MAASDispatcher:
21 data = bytes(data, "utf-8")
22 req = RequestWithMethod(request_url, data, headers, method=method)
23 # Retry the request maximum of 3 times.
24+ handlers = []
25+ if not self.autodetect_proxies:
26+ handlers.append(urllib.request.ProxyHandler({}))
27 for try_count in range(3):
28+ opener = urllib.request.build_opener(*handlers)
29 try:
30- res = urllib.request.urlopen(req)
31+ res = opener.open(req)
32 except urllib.error.HTTPError as exc:
33 if exc.code == 503:
34 # HTTP 503 Service Unavailable - MAAS might still be
35diff --git a/src/apiclient/tests/test_maas_client.py b/src/apiclient/tests/test_maas_client.py
36index 36701d1..6e06581 100644
37--- a/src/apiclient/tests/test_maas_client.py
38+++ b/src/apiclient/tests/test_maas_client.py
39@@ -8,8 +8,9 @@ __all__ = []
40 import gzip
41 from io import BytesIO
42 import json
43+import os
44 from random import randint
45-from unittest.mock import ANY
46+from unittest.mock import ANY, MagicMock, patch
47 import urllib.error
48 import urllib.parse
49 from urllib.parse import parse_qs, urljoin, urlparse
50@@ -33,6 +34,23 @@ class TestMAASOAuth(MAASTestCase):
51
52
53 class TestMAASDispatcher(MAASTestCase):
54+ def setUp(self):
55+ super().setUp()
56+ self.patch_urllib()
57+
58+ def patch_urllib(self):
59+ def patch_build_opener(*args, **kwargs):
60+ self.opener = real_build_opener(*args, **kwargs)
61+ self.orig_open_func = self.opener.open
62+ if self.open_func is not None:
63+ self.opener.open = self.open_func
64+ return self.opener
65+
66+ real_build_opener = urllib.request.build_opener
67+ build_opener_mock = self.patch(urllib.request, "build_opener")
68+ build_opener_mock.side_effect = patch_build_opener
69+ self.open_func = None
70+
71 def test_dispatch_query_makes_direct_call(self):
72 contents = factory.make_string().encode("ascii")
73 url = "file://%s" % self.make_file(contents=contents)
74@@ -44,12 +62,12 @@ class TestMAASDispatcher(MAASTestCase):
75 # urllib, used by MAASDispatcher, requires data encoded into bytes. We
76 # encode into utf-8 in dispatch_query if necessary.
77 request = self.patch(urllib.request.Request, "__init__")
78- urlopen = self.patch(urllib.request, "urlopen")
79+ self.patch_urllib()
80+ self.open_func = lambda *args: MagicMock()
81 url = factory.make_url()
82 data = factory.make_string(300, spaces=True)
83 MAASDispatcher().dispatch_query(url, {}, method="POST", data=data)
84 request.assert_called_once_with(ANY, url, bytes(data, "utf-8"), ANY)
85- urlopen.assert_called_once_with(ANY)
86
87 def test_request_from_http(self):
88 # We can't just call self.make_file because HTTPServerFixture will only
89@@ -97,13 +115,12 @@ class TestMAASDispatcher(MAASTestCase):
90 content = factory.make_string(300).encode("ascii")
91 factory.make_file(location=".", name=name, contents=content)
92 called = []
93- orig_urllib = urllib.request.urlopen
94
95- def logging_urlopen(*args, **kwargs):
96+ def logging_open(*args, **kwargs):
97 called.append((args, kwargs))
98- return orig_urllib(*args, **kwargs)
99+ return self.orig_open_func(*args, **kwargs)
100
101- self.patch(urllib.request, "urlopen", logging_urlopen)
102+ self.open_func = logging_open
103 with HTTPServerFixture() as httpd:
104 url = urljoin(httpd.url, name)
105 res = MAASDispatcher().dispatch_query(url, {})
106@@ -139,20 +156,19 @@ class TestMAASDispatcher(MAASTestCase):
107 factory.make_file(location=".", name=name, contents=content)
108 with HTTPServerFixture() as httpd:
109 url = urljoin(httpd.url, name)
110- original_urlopen = urllib.request.urlopen
111
112 counter = {"count": 0}
113
114- def _wrap_urlopen(*args, **kwargs):
115+ def _wrap_open(*args, **kwargs):
116 if counter["count"] < 2:
117 counter["count"] += 1
118 raise urllib.error.HTTPError(
119 url, 503, "service unavailable", {}, None
120 )
121 else:
122- return original_urlopen(*args, **kwargs)
123+ return self.orig_open_func(*args, **kwargs)
124
125- self.patch(urllib.request, "urlopen").side_effect = _wrap_urlopen
126+ self.open_func = _wrap_open
127 response = MAASDispatcher().dispatch_query(url, {})
128 self.assertEqual(200, response.code)
129 self.assertEqual(content, response.read())
130@@ -165,12 +181,12 @@ class TestMAASDispatcher(MAASTestCase):
131 with HTTPServerFixture() as httpd:
132 url = urljoin(httpd.url, name)
133
134- def _wrap_urlopen(*args, **kwargs):
135+ def _wrap_open(*args, **kwargs):
136 raise urllib.error.HTTPError(
137 url, 503, "service unavailable", {}, None
138 )
139
140- self.patch(urllib.request, "urlopen").side_effect = _wrap_urlopen
141+ self.open_func = _wrap_open
142 err = self.assertRaises(
143 urllib.error.HTTPError,
144 MAASDispatcher().dispatch_query,
145@@ -179,6 +195,44 @@ class TestMAASDispatcher(MAASTestCase):
146 )
147 self.assertEqual(503, err.code)
148
149+ def test_autodetects_proxies(self):
150+ self.open_func = lambda *args: MagicMock()
151+ url = factory.make_url()
152+ proxy_variables = {
153+ "http_proxy": "http://proxy.example.com",
154+ "https_proxy": "https://proxy.example.com",
155+ "no_proxy": "noproxy.example.com",
156+ }
157+ with patch.dict(os.environ, proxy_variables):
158+ MAASDispatcher().dispatch_query(url, {}, method="GET")
159+ for handler in self.opener.handle_open["http"]:
160+ if isinstance(handler, urllib.request.ProxyHandler):
161+ break
162+ else:
163+ raise AssertionError("No ProxyHandler installed")
164+ expected = {
165+ "http": proxy_variables["http_proxy"],
166+ "https": proxy_variables["https_proxy"],
167+ "no": proxy_variables["no_proxy"],
168+ }
169+ for key, value in expected.items():
170+ self.assertEqual(value, handler.proxies[key])
171+
172+ def test_no_autodetects_proxies(self):
173+ self.open_func = lambda *args: MagicMock()
174+ url = factory.make_url()
175+ proxy_variables = {
176+ "http_proxy": "http://proxy.example.com",
177+ "https_proxy": "https://proxy.example.com",
178+ "no_proxy": "noproxy.example.com",
179+ }
180+ with patch.dict(os.environ, proxy_variables):
181+ dispatcher = MAASDispatcher(autodetect_proxies=False)
182+ dispatcher.dispatch_query(url, {}, method="GET")
183+ for handler in self.opener.handle_open["http"]:
184+ if isinstance(handler, urllib.request.ProxyHandler):
185+ raise AssertionError("ProxyHandler shouldn't be there")
186+
187
188 def make_path():
189 """Create an arbitrary resource path."""
190diff --git a/src/provisioningserver/rpc/tags.py b/src/provisioningserver/rpc/tags.py
191index 3b436f6..653e777 100644
192--- a/src/provisioningserver/rpc/tags.py
193+++ b/src/provisioningserver/rpc/tags.py
194@@ -30,9 +30,11 @@ def evaluate_tag(
195 :param credentials: A 3-tuple of OAuth credentials.
196 :param maas_url: URL of the MAAS API.
197 """
198+ # Turn off proxy detection, since the rack should talk directly to
199+ # the region, even if a system-wide proxy is configured.
200 client = MAASClient(
201 auth=MAASOAuth(*credentials),
202- dispatcher=MAASDispatcher(),
203+ dispatcher=MAASDispatcher(autodetect_proxies=False),
204 base_url=maas_url,
205 )
206 process_node_tags(
207diff --git a/src/provisioningserver/rpc/tests/test_tags.py b/src/provisioningserver/rpc/tests/test_tags.py
208index 7a7d0b3..a3493e5 100644
209--- a/src/provisioningserver/rpc/tests/test_tags.py
210+++ b/src/provisioningserver/rpc/tests/test_tags.py
211@@ -73,3 +73,20 @@ class TestEvaluateTag(MAASTestCase):
212 tags.MAASOAuth,
213 MockCalledOnceWith(consumer_key, resource_token, resource_secret),
214 )
215+
216+ def test__constructs_client_with_no_proxies(self):
217+ credentials = "aaa", "bbb", "ccc"
218+ rack_id = factory.make_name("rack")
219+ process_node_tags = self.patch_autospec(tags, "process_node_tags")
220+ tags.evaluate_tag(
221+ rack_id,
222+ [],
223+ sentinel.tag_name,
224+ sentinel.tag_definition,
225+ sentinel.tag_nsmap,
226+ credentials,
227+ self.mock_url,
228+ )
229+
230+ client = process_node_tags.call_args[1]["client"]
231+ self.assertFalse(client.dispatcher.autodetect_proxies)

Subscribers

People subscribed via source and target branches