Merge ~bjornt/maas:bug-1845351-2.6 into maas:2.6

Proposed by Björn Tillenius
Status: Merged
Approved by: Björn Tillenius
Approved revision: dff082251ea85543d7f335f566c493a592b0173f
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~bjornt/maas:bug-1845351-2.6
Merge into: maas:2.6
Diff against target: 243 lines (+112/-21)
4 files modified
src/apiclient/maas_client.py (+11/-1)
src/apiclient/tests/test_maas_client.py (+74/-16)
src/provisioningserver/rpc/tags.py (+6/-2)
src/provisioningserver/rpc/tests/test_tags.py (+21/-2)
Reviewer Review Type Date Requested Status
Björn Tillenius Approve
MAAS Lander unittests Pending
Review via email: mp+374267@code.launchpad.net

Commit message

LP #1845351: MAAS sometimes get in a state where machine tags aren't updated from their definitions

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.

(cherry picked from commit 4eee730d67082731ed43fe1075a4ce883fcd3823)

To post a comment you must log in.
Revision history for this message
Björn Tillenius (bjornt) wrote :

Self-approve backport.

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

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 6bd7f50..7f76d2e 100644
3--- a/src/apiclient/maas_client.py
4+++ b/src/apiclient/maas_client.py
5@@ -81,8 +81,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@@ -112,9 +118,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 5aade88..caa1f81 100644
37--- a/src/apiclient/tests/test_maas_client.py
38+++ b/src/apiclient/tests/test_maas_client.py
39@@ -8,8 +8,13 @@ __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 (
47+ ANY,
48+ MagicMock,
49+ patch,
50+)
51 import urllib.error
52 import urllib.parse
53 from urllib.parse import (
54@@ -46,6 +51,22 @@ class TestMAASOAuth(MAASTestCase):
55
56
57 class TestMAASDispatcher(MAASTestCase):
58+ def setUp(self):
59+ super().setUp()
60+ self.patch_urllib()
61+
62+ def patch_urllib(self):
63+ def patch_build_opener(*args, **kwargs):
64+ self.opener = real_build_opener(*args, **kwargs)
65+ self.orig_open_func = self.opener.open
66+ if self.open_func is not None:
67+ self.opener.open = self.open_func
68+ return self.opener
69+
70+ real_build_opener = urllib.request.build_opener
71+ build_opener_mock = self.patch(urllib.request, "build_opener")
72+ build_opener_mock.side_effect = patch_build_opener
73+ self.open_func = None
74
75 def test_dispatch_query_makes_direct_call(self):
76 contents = factory.make_string().encode("ascii")
77@@ -56,13 +77,13 @@ class TestMAASDispatcher(MAASTestCase):
78 def test_dispatch_query_encodes_string_data(self):
79 # urllib, used by MAASDispatcher, requires data encoded into bytes. We
80 # encode into utf-8 in dispatch_query if necessary.
81- request = self.patch(urllib.request.Request, '__init__')
82- urlopen = self.patch(urllib.request, 'urlopen')
83+ request = self.patch(urllib.request.Request, "__init__")
84+ self.patch_urllib()
85+ self.open_func = lambda *args: MagicMock()
86 url = factory.make_url()
87 data = factory.make_string(300, spaces=True)
88 MAASDispatcher().dispatch_query(url, {}, method="POST", data=data)
89 request.assert_called_once_with(ANY, url, bytes(data, "utf-8"), ANY)
90- urlopen.assert_called_once_with(ANY)
91
92 def test_request_from_http(self):
93 # We can't just call self.make_file because HTTPServerFixture will only
94@@ -106,12 +127,12 @@ class TestMAASDispatcher(MAASTestCase):
95 content = factory.make_string(300).encode('ascii')
96 factory.make_file(location='.', name=name, contents=content)
97 called = []
98- orig_urllib = urllib.request.urlopen
99
100- def logging_urlopen(*args, **kwargs):
101+ def logging_open(*args, **kwargs):
102 called.append((args, kwargs))
103- return orig_urllib(*args, **kwargs)
104- self.patch(urllib.request, 'urlopen', logging_urlopen)
105+ return self.orig_open_func(*args, **kwargs)
106+
107+ self.open_func = logging_open
108 with HTTPServerFixture() as httpd:
109 url = urljoin(httpd.url, name)
110 res = MAASDispatcher().dispatch_query(url, {})
111@@ -146,19 +167,18 @@ class TestMAASDispatcher(MAASTestCase):
112 factory.make_file(location='.', name=name, contents=content)
113 with HTTPServerFixture() as httpd:
114 url = urljoin(httpd.url, name)
115- original_urlopen = urllib.request.urlopen
116
117 counter = {'count': 0}
118
119- def _wrap_urlopen(*args, **kwargs):
120- if counter['count'] < 2:
121- counter['count'] += 1
122+ def _wrap_open(*args, **kwargs):
123+ if counter["count"] < 2:
124+ counter["count"] += 1
125 raise urllib.error.HTTPError(
126 url, 503, "service unavailable", {}, None)
127 else:
128- return original_urlopen(*args, **kwargs)
129+ return self.orig_open_func(*args, **kwargs)
130
131- self.patch(urllib.request, "urlopen").side_effect = _wrap_urlopen
132+ self.open_func = _wrap_open
133 response = MAASDispatcher().dispatch_query(url, {})
134 self.assertEqual(200, response.code)
135 self.assertEqual(content, response.read())
136@@ -171,16 +191,54 @@ class TestMAASDispatcher(MAASTestCase):
137 with HTTPServerFixture() as httpd:
138 url = urljoin(httpd.url, name)
139
140- def _wrap_urlopen(*args, **kwargs):
141+ def _wrap_open(*args, **kwargs):
142 raise urllib.error.HTTPError(
143 url, 503, "service unavailable", {}, None)
144
145- self.patch(urllib.request, "urlopen").side_effect = _wrap_urlopen
146+ self.open_func = _wrap_open
147 err = self.assertRaises(
148 urllib.error.HTTPError,
149 MAASDispatcher().dispatch_query, url, {})
150 self.assertEqual(503, err.code)
151
152+ def test_autodetects_proxies(self):
153+ self.open_func = lambda *args: MagicMock()
154+ url = factory.make_url()
155+ proxy_variables = {
156+ "http_proxy": "http://proxy.example.com",
157+ "https_proxy": "https://proxy.example.com",
158+ "no_proxy": "noproxy.example.com",
159+ }
160+ with patch.dict(os.environ, proxy_variables):
161+ MAASDispatcher().dispatch_query(url, {}, method="GET")
162+ for handler in self.opener.handle_open["http"]:
163+ if isinstance(handler, urllib.request.ProxyHandler):
164+ break
165+ else:
166+ raise AssertionError("No ProxyHandler installed")
167+ expected = {
168+ "http": proxy_variables["http_proxy"],
169+ "https": proxy_variables["https_proxy"],
170+ "no": proxy_variables["no_proxy"],
171+ }
172+ for key, value in expected.items():
173+ self.assertEqual(value, handler.proxies[key])
174+
175+ def test_no_autodetects_proxies(self):
176+ self.open_func = lambda *args: MagicMock()
177+ url = factory.make_url()
178+ proxy_variables = {
179+ "http_proxy": "http://proxy.example.com",
180+ "https_proxy": "https://proxy.example.com",
181+ "no_proxy": "noproxy.example.com",
182+ }
183+ with patch.dict(os.environ, proxy_variables):
184+ dispatcher = MAASDispatcher(autodetect_proxies=False)
185+ dispatcher.dispatch_query(url, {}, method="GET")
186+ for handler in self.opener.handle_open["http"]:
187+ if isinstance(handler, urllib.request.ProxyHandler):
188+ raise AssertionError("ProxyHandler shouldn't be there")
189+
190
191 def make_path():
192 """Create an arbitrary resource path."""
193diff --git a/src/provisioningserver/rpc/tags.py b/src/provisioningserver/rpc/tags.py
194index f8bc2aa..262d837 100644
195--- a/src/provisioningserver/rpc/tags.py
196+++ b/src/provisioningserver/rpc/tags.py
197@@ -30,9 +30,13 @@ def evaluate_tag(
198 :param credentials: A 3-tuple of OAuth credentials.
199 :param maas_url: URL of the MAAS API.
200 """
201+ # Turn off proxy detection, since the rack should talk directly to
202+ # the region, even if a system-wide proxy is configured.
203 client = MAASClient(
204- auth=MAASOAuth(*credentials), dispatcher=MAASDispatcher(),
205- base_url=maas_url)
206+ auth=MAASOAuth(*credentials),
207+ dispatcher=MAASDispatcher(autodetect_proxies=False),
208+ base_url=maas_url,
209+ )
210 process_node_tags(
211 rack_id=system_id, nodes=nodes,
212 tag_name=tag_name, tag_definition=tag_definition,
213diff --git a/src/provisioningserver/rpc/tests/test_tags.py b/src/provisioningserver/rpc/tests/test_tags.py
214index 9576003..40208c9 100644
215--- a/src/provisioningserver/rpc/tests/test_tags.py
216+++ b/src/provisioningserver/rpc/tests/test_tags.py
217@@ -60,5 +60,24 @@ class TestEvaluateTag(MAASTestCase):
218 self.assertEqual(self.mock_url, client.url)
219 self.assertIsInstance(client.dispatcher, MAASDispatcher)
220 self.assertIsInstance(client.auth, MAASOAuth)
221- self.assertThat(tags.MAASOAuth, MockCalledOnceWith(
222- consumer_key, resource_token, resource_secret))
223+ self.assertThat(
224+ tags.MAASOAuth,
225+ MockCalledOnceWith(consumer_key, resource_token, resource_secret),
226+ )
227+
228+ def test__constructs_client_with_no_proxies(self):
229+ credentials = "aaa", "bbb", "ccc"
230+ rack_id = factory.make_name("rack")
231+ process_node_tags = self.patch_autospec(tags, "process_node_tags")
232+ tags.evaluate_tag(
233+ rack_id,
234+ [],
235+ sentinel.tag_name,
236+ sentinel.tag_definition,
237+ sentinel.tag_nsmap,
238+ credentials,
239+ self.mock_url,
240+ )
241+
242+ client = process_node_tags.call_args[1]["client"]
243+ self.assertFalse(client.dispatcher.autodetect_proxies)

Subscribers

People subscribed via source and target branches