Merge ~jfguedez/charm-ubuntu-advantage:bug/1944591 into charm-ubuntu-advantage:master

Proposed by Jose Guedez
Status: Merged
Approved by: Tom Haddon
Approved revision: e8265de8062093dbb6764748172e236ab43ccdc9
Merged at revision: 2ff56b61e24664d01cb2ddd843474706b8ce2e31
Proposed branch: ~jfguedez/charm-ubuntu-advantage:bug/1944591
Merge into: charm-ubuntu-advantage:master
Diff against target: 357 lines (+127/-44)
2 files modified
src/charm.py (+44/-6)
tests/test_charm.py (+83/-38)
Reviewer Review Type Date Requested Status
Tom Haddon Approve
Canonical IS Reviewers Pending
Review via email: mp+415946@code.launchpad.net

Commit message

Add support for proxy settings from the model

Description of the change

Supersedes this MP: https://code.launchpad.net/~mastier1/charm-ubuntu-advantage/+git/charm-ubuntu-advantage/+merge/412774

Includes the original code/commit and additionally:
* Fixes the broken unit tests
* Add a new test for the new functionality
* Removes the proxy variables for the apt calls, as it's already supported by apt-http[s]-proxy model variables
* Adds the proxy config to the UA client itself, instead of installing it with proxy environment variables. This ensures it's functional after the charm installs it.
* Keeps the environment variables for the `add-apt-repository` calls.

Tested in Focal/Bionic/Xenial with and without proxy config in the juju model.

To post a comment you must log in.
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

This merge proposal is being monitored by mergebot. Change the status to Approved to merge.

Revision history for this message
Tom Haddon (mthaddon) wrote :

A few comments/questions inline, otherwise looks good to me. Thanks.

review: Needs Fixing
Revision history for this message
Jose Guedez (jfguedez) wrote :

Thanks for the review. I have replied to your comments and hopefully addressed the issues in comments/logging. Please take a look again when you have a chance.

Revision history for this message
Tom Haddon (mthaddon) wrote :

I'm still a bit confused as to why we're setting this in `_setup_proxy_env` if UA client doesn't have an option for that:

self.env['no_proxy'] = self.env.get('JUJU_CHARM_NO_PROXY', '')

Can you clarify?

Revision history for this message
Jose Guedez (jfguedez) wrote (last edit ):

@Tom, happy to clarify:

The charm needs connectivity via proxy for 4 different things:

1-To install the ppa (function `install_ppa`), which relies on the command `add-apt-repository`. This command supports using proxies via environment variables, which are mapped from the standard model-config variables juju-*-proxy variables:

* juju-http-proxy => JUJU_CHARM_HTTP_PROXY => http_proxy
* juju-https-proxy => JUJU_CHARM_HTTPs_PROXY => https_proxy
* juju-no-proxy => JUJU_CHARM_NO_PROXY => no_proxy

the values extracted from the environment in `_setup_proxy_env`

2-To install packages (function `install_packages`). The original MP used environment variables for this as well. I reverted this, as it should rely on model config apt-http[s]-proxy, that handles this case already. No changes in this MP to support this.

3-To install snaps (e.g. canonical-livepatch, as part of the UA installation). The original MP did not contain changes for this. The charm should rely on model config snap-http[s]-proxy, that handles this case already. No changes in this MP to support this either.

4-To configure the UA client (functions `detach_subscription`, `attach_subscription`, `get_status_output` + tool itself). The original MP passed environment variables as in #1 for this, and didn't configure ubuntu-advantage itself. This MP sets the proxy configuration of ubuntu-advantage itself via the CLI (which makes it's way to /etc/ubuntu-advantage/uaclient.conf), in the helper `_configure_ua_proxy`. It re-uses the values extracted from the environment in `_setup_proxy_env`, but not no_proxy which is not used in the UA tool. I guess it's not needed because the tool already knows which URLs it's interested in, and uses the proxy for them (no need to include/exclude)

* juju-http-proxy => JUJU_CHARM_HTTP_PROXY => http_proxy
* juju-https-proxy => JUJU_CHARM_HTTPs_PROXY => https_proxy

Hope that clarifies the different requirements of connectivity within the charm as I understand it. In the end it's trying to use the settings in the juju model, and either pass them to the functionality that is proxy aware: apt/snap are built into the model, UA has it's own configuration, and the PPA tool requires the information via the standard environment variables.

Revision history for this message
Tom Haddon (mthaddon) wrote :

Thanks for the explanation.

Could you add a comment in _setup_proxy_env to explain that the http_proxy and https_proxy env variables will be used for package installs and ua, while no_proxy will only be used be used for package installs as it's not a supported variable of ua itself?

Revision history for this message
Jose Guedez (jfguedez) wrote :

Thanks for the review. Added the comment as requested.

Revision history for this message
Tom Haddon (mthaddon) wrote :

LGTM, thx

review: Approve
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change successfully merged at revision 2ff56b61e24664d01cb2ddd843474706b8ce2e31

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/charm.py b/src/charm.py
2index 25d7d0a..34878c0 100755
3--- a/src/charm.py
4+++ b/src/charm.py
5@@ -6,6 +6,7 @@
6 import hashlib
7 import json
8 import logging
9+import os
10 import subprocess
11 import yaml
12
13@@ -18,14 +19,14 @@ from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus
14 logger = logging.getLogger(__name__)
15
16
17-def install_ppa(ppa):
18+def install_ppa(ppa, env):
19 """Install specified ppa"""
20- subprocess.check_call(["add-apt-repository", "--yes", ppa])
21+ subprocess.check_call(["add-apt-repository", "--yes", ppa], env=env)
22
23
24-def remove_ppa(ppa):
25+def remove_ppa(ppa, env):
26 """Remove specified ppa"""
27- subprocess.check_call(["add-apt-repository", "--remove", "--yes", ppa])
28+ subprocess.check_call(["add-apt-repository", "--remove", "--yes", ppa], env=env)
29
30
31 def install_package(package):
32@@ -74,13 +75,33 @@ class UbuntuAdvantageCharm(CharmBase):
33
34 def __init__(self, *args):
35 super().__init__(*args)
36+ self._setup_proxy_env()
37 self._state.set_default(
38 contract_url=None,
39 hashed_token=None,
40 package_needs_installing=True,
41 ppa=None)
42+
43 self.framework.observe(self.on.config_changed, self.config_changed)
44
45+ def _setup_proxy_env(self):
46+ """Setup proxy variables from model"""
47+ self.env = dict(os.environ)
48+ self.env['http_proxy'] = self.env.get('JUJU_CHARM_HTTP_PROXY', '')
49+ self.env['https_proxy'] = self.env.get('JUJU_CHARM_HTTPS_PROXY', '')
50+ self.env['no_proxy'] = self.env.get('JUJU_CHARM_NO_PROXY', '')
51+
52+ # The values for 'http_proxy' and 'https_proxy' will be used for the
53+ # PPA install/remove operations (passed as environment variables), as
54+ # well as for configuring the UA client.
55+ # The value for 'no_proxy' is only used by the PPA install/remove
56+ # operations (passed as an environment variable).
57+
58+ # log proxy environment variables for debugging
59+ for envvar, value in self.env.items():
60+ if envvar.endswith("proxy".lower()):
61+ logger.debug("Envvar '%s' => '%s'", envvar, value)
62+
63 def config_changed(self, event):
64 """Install and configure ubuntu-advantage tools and attachment"""
65 logger.info("Beginning config_changed")
66@@ -100,14 +121,14 @@ class UbuntuAdvantageCharm(CharmBase):
67
68 if old_ppa and old_ppa != ppa:
69 logger.info("Removing previously installed ppa (%s)", old_ppa)
70- remove_ppa(old_ppa)
71+ remove_ppa(old_ppa, self.env)
72 self._state.ppa = None
73 # If ppa is changed, want to remove the previous version of the package for consistency
74 self._state.package_needs_installing = True
75
76 if ppa and ppa != old_ppa:
77 logger.info("Installing ppa: %s", ppa)
78- install_ppa(ppa)
79+ install_ppa(ppa, self.env)
80 self._state.ppa = ppa
81 # If ppa is changed, want to force an install of the package for potential updates
82 self._state.package_needs_installing = True
83@@ -132,6 +153,11 @@ class UbuntuAdvantageCharm(CharmBase):
84 old_contract_url = self._state.contract_url
85 config_changed = contract_url != old_contract_url
86
87+ # Add the proxy configuration to the UA client.
88+ # This is needed for attach/detach/status commands used by the charm,
89+ # as well as regular tool operations.
90+ self._configure_ua_proxy()
91+
92 status = get_status_output()
93 if status["attached"] and (config_changed or token_changed):
94 logger.info("Detaching ubuntu-advantage subscription")
95@@ -165,6 +191,18 @@ class UbuntuAdvantageCharm(CharmBase):
96 message = "Attached (" + ",".join(services) + ")"
97 self.unit.status = ActiveStatus(message)
98
99+ def _configure_ua_proxy(self):
100+ """Configure the proxy options for the ubuntu-advantage client"""
101+ for config_key in ("http_proxy", "https_proxy"):
102+ subprocess.check_call(
103+ [
104+ "ubuntu-advantage",
105+ "config",
106+ "set",
107+ "{}={}".format(config_key, self.env[config_key]),
108+ ]
109+ )
110+
111
112 if __name__ == "__main__": # pragma: nocover
113 main(UbuntuAdvantageCharm)
114diff --git a/tests/test_charm.py b/tests/test_charm.py
115index 1415f7d..f55d844 100644
116--- a/tests/test_charm.py
117+++ b/tests/test_charm.py
118@@ -63,6 +63,9 @@ log_level: debug
119 log_file: /var/log/ubuntu-advantage.log
120 """
121
122+TEST_PROXY_URL = "http://squid.internal:3128"
123+TEST_NO_PROXY = "127.0.0.1,localhost,::1"
124+
125
126 def _written(handle):
127 contents = "".join(["".join(a.args) for a in handle.write.call_args_list])
128@@ -76,7 +79,8 @@ class TestCharm(TestCase):
129 "call": patch("subprocess.call").start(),
130 "check_call": patch("subprocess.check_call").start(),
131 "check_output": patch("subprocess.check_output").start(),
132- "open": patch("builtins.open").start()
133+ "open": patch("builtins.open").start(),
134+ "environ": patch.dict("os.environ", clear=True).start()
135 }
136 self.mocks["check_output"].side_effect = [
137 STATUS_DETACHED
138@@ -86,6 +90,7 @@ class TestCharm(TestCase):
139 self.harness = Harness(UbuntuAdvantageCharm)
140 self.addCleanup(self.harness.cleanup)
141 self.harness.begin()
142+ self.env = self.harness.charm.env
143
144 def test_config_defaults(self):
145 self.assertEqual(self.harness.charm.config.get("contract_url"),
146@@ -95,25 +100,25 @@ class TestCharm(TestCase):
147
148 def test_config_changed_ppa_new(self):
149 self.harness.update_config({"ppa": "ppa:ua-client/stable"})
150- self.assertEqual(self.mocks["check_call"].call_count, 4)
151- self.mocks["check_call"].assert_has_calls([
152- call(["add-apt-repository", "--yes", "ppa:ua-client/stable"]),
153+ self.assertEqual(self.mocks["check_call"].call_count, 6)
154+ self.mocks["check_call"].assert_has_calls(self._add_ua_proxy_setup_calls([
155+ call(["add-apt-repository", "--yes", "ppa:ua-client/stable"], env=self.env),
156 call(["apt", "remove", "--yes", "--quiet", "ubuntu-advantage-tools"]),
157 call(["apt", "update"]),
158 call(["apt", "install", "--yes", "--quiet", "ubuntu-advantage-tools"])
159- ])
160+ ]))
161 self.assertEqual(self.harness.charm._state.ppa, "ppa:ua-client/stable")
162 self.assertFalse(self.harness.charm._state.package_needs_installing)
163
164 def test_config_changed_ppa_updated(self):
165 self.harness.update_config({"ppa": "ppa:ua-client/stable"})
166- self.assertEqual(self.mocks["check_call"].call_count, 4)
167- self.mocks["check_call"].assert_has_calls([
168- call(["add-apt-repository", "--yes", "ppa:ua-client/stable"]),
169+ self.assertEqual(self.mocks["check_call"].call_count, 6)
170+ self.mocks["check_call"].assert_has_calls(self._add_ua_proxy_setup_calls([
171+ call(["add-apt-repository", "--yes", "ppa:ua-client/stable"], env=self.env),
172 call(["apt", "remove", "--yes", "--quiet", "ubuntu-advantage-tools"]),
173 call(["apt", "update"]),
174 call(["apt", "install", "--yes", "--quiet", "ubuntu-advantage-tools"])
175- ])
176+ ]))
177 self.assertEqual(self.harness.charm._state.ppa, "ppa:ua-client/stable")
178 self.assertFalse(self.harness.charm._state.package_needs_installing)
179
180@@ -122,26 +127,28 @@ class TestCharm(TestCase):
181 ]
182 self.mocks["check_call"].reset_mock()
183 self.harness.update_config({"ppa": "ppa:different-client/unstable"})
184- self.assertEqual(self.mocks["check_call"].call_count, 5)
185- self.mocks["check_call"].assert_has_calls([
186- call(["add-apt-repository", "--remove", "--yes", "ppa:ua-client/stable"]),
187- call(["add-apt-repository", "--yes", "ppa:different-client/unstable"]),
188+ self.assertEqual(self.mocks["check_call"].call_count, 7)
189+ self.mocks["check_call"].assert_has_calls(self._add_ua_proxy_setup_calls([
190+ call(["add-apt-repository", "--remove", "--yes", "ppa:ua-client/stable"],
191+ env=self.env),
192+ call(["add-apt-repository", "--yes", "ppa:different-client/unstable"],
193+ env=self.env),
194 call(["apt", "remove", "--yes", "--quiet", "ubuntu-advantage-tools"]),
195 call(["apt", "update"]),
196 call(["apt", "install", "--yes", "--quiet", "ubuntu-advantage-tools"])
197- ])
198+ ]))
199 self.assertEqual(self.harness.charm._state.ppa, "ppa:different-client/unstable")
200 self.assertFalse(self.harness.charm._state.package_needs_installing)
201
202 def test_config_changed_ppa_unmodified(self):
203 self.harness.update_config({"ppa": "ppa:ua-client/stable"})
204- self.assertEqual(self.mocks["check_call"].call_count, 4)
205- self.mocks["check_call"].assert_has_calls([
206- call(["add-apt-repository", "--yes", "ppa:ua-client/stable"]),
207+ self.assertEqual(self.mocks["check_call"].call_count, 6)
208+ self.mocks["check_call"].assert_has_calls(self._add_ua_proxy_setup_calls([
209+ call(["add-apt-repository", "--yes", "ppa:ua-client/stable"], env=self.env),
210 call(["apt", "remove", "--yes", "--quiet", "ubuntu-advantage-tools"]),
211 call(["apt", "update"]),
212 call(["apt", "install", "--yes", "--quiet", "ubuntu-advantage-tools"])
213- ])
214+ ]))
215 self.assertEqual(self.harness.charm._state.ppa, "ppa:ua-client/stable")
216 self.assertFalse(self.harness.charm._state.package_needs_installing)
217
218@@ -151,19 +158,20 @@ class TestCharm(TestCase):
219 STATUS_DETACHED
220 ]
221 self.harness.update_config({"ppa": "ppa:ua-client/stable"})
222- self.assertEqual(self.mocks["check_call"].call_count, 0)
223+ self.assertEqual(self.mocks["check_call"].call_count, 2)
224+ self.mocks["check_call"].assert_has_calls(self._add_ua_proxy_setup_calls([]))
225 self.assertEqual(self.harness.charm._state.ppa, "ppa:ua-client/stable")
226 self.assertFalse(self.harness.charm._state.package_needs_installing)
227
228 def test_config_changed_ppa_unset(self):
229 self.harness.update_config({"ppa": "ppa:ua-client/stable"})
230- self.assertEqual(self.mocks["check_call"].call_count, 4)
231- self.mocks["check_call"].assert_has_calls([
232- call(["add-apt-repository", "--yes", "ppa:ua-client/stable"]),
233+ self.assertEqual(self.mocks["check_call"].call_count, 6)
234+ self.mocks["check_call"].assert_has_calls(self._add_ua_proxy_setup_calls([
235+ call(["add-apt-repository", "--yes", "ppa:ua-client/stable"], env=self.env),
236 call(["apt", "remove", "--yes", "--quiet", "ubuntu-advantage-tools"]),
237 call(["apt", "update"]),
238 call(["apt", "install", "--yes", "--quiet", "ubuntu-advantage-tools"])
239- ])
240+ ]))
241 self.assertEqual(self.harness.charm._state.ppa, "ppa:ua-client/stable")
242 self.assertFalse(self.harness.charm._state.package_needs_installing)
243
244@@ -173,13 +181,14 @@ class TestCharm(TestCase):
245 STATUS_DETACHED
246 ]
247 self.harness.update_config({"ppa": ""})
248- self.assertEqual(self.mocks["check_call"].call_count, 4)
249- self.mocks["check_call"].assert_has_calls([
250- call(["add-apt-repository", "--remove", "--yes", "ppa:ua-client/stable"]),
251+ self.assertEqual(self.mocks["check_call"].call_count, 6)
252+ self.mocks["check_call"].assert_has_calls(self._add_ua_proxy_setup_calls([
253+ call(["add-apt-repository", "--remove", "--yes", "ppa:ua-client/stable"],
254+ env=self.env),
255 call(["apt", "remove", "--yes", "--quiet", "ubuntu-advantage-tools"]),
256 call(["apt", "update"]),
257 call(["apt", "install", "--yes", "--quiet", "ubuntu-advantage-tools"])
258- ])
259+ ]))
260 self.assertIsNone(self.harness.charm._state.ppa)
261 self.assertFalse(self.harness.charm._state.package_needs_installing)
262
263@@ -223,13 +232,13 @@ class TestCharm(TestCase):
264 STATUS_ATTACHED
265 ]
266 self.harness.update_config({"token": "test-token"})
267- self.assertEqual(self.mocks["check_call"].call_count, 4)
268- self.mocks["check_call"].assert_has_calls([
269- call(["add-apt-repository", "--yes", "ppa:ua-client/stable"]),
270+ self.assertEqual(self.mocks["check_call"].call_count, 6)
271+ self.mocks["check_call"].assert_has_calls(self._add_ua_proxy_setup_calls([
272+ call(["add-apt-repository", "--yes", "ppa:ua-client/stable"], env=self.env),
273 call(["apt", "remove", "--yes", "--quiet", "ubuntu-advantage-tools"]),
274 call(["apt", "update"]),
275 call(["apt", "install", "--yes", "--quiet", "ubuntu-advantage-tools"])
276- ])
277+ ]))
278 self.mocks["open"].assert_called_with("/etc/ubuntu-advantage/uaclient.conf", "r+")
279 handle = self.mocks["open"]()
280 expected = dedent("""\
281@@ -255,10 +264,10 @@ class TestCharm(TestCase):
282 STATUS_ATTACHED
283 ]
284 self.harness.update_config({"token": "test-token-2"})
285- self.assertEqual(self.mocks["check_call"].call_count, 1)
286- self.mocks["check_call"].assert_has_calls([
287+ self.assertEqual(self.mocks["check_call"].call_count, 3)
288+ self.mocks["check_call"].assert_has_calls(self._add_ua_proxy_setup_calls([
289 call(["ubuntu-advantage", "detach", "--assume-yes"])
290- ])
291+ ], append=False))
292 self.assertEqual(self.mocks["call"].call_count, 1)
293 self.mocks["call"].assert_has_calls([
294 call(["ubuntu-advantage", "attach", "test-token-2"])
295@@ -295,10 +304,10 @@ class TestCharm(TestCase):
296 STATUS_DETACHED
297 ]
298 self.harness.update_config({"token": ""})
299- self.assertEqual(self.mocks["check_call"].call_count, 1)
300- self.mocks["check_call"].assert_has_calls([
301+ self.assertEqual(self.mocks["check_call"].call_count, 3)
302+ self.mocks["check_call"].assert_has_calls(self._add_ua_proxy_setup_calls([
303 call(["ubuntu-advantage", "detach", "--assume-yes"])
304- ])
305+ ], append=False))
306 self.assertEqual(self.mocks["call"].call_count, 0)
307 self.assertIsNone(self.harness.charm._state.hashed_token)
308 self.assertEqual(self.harness.model.unit.status, BlockedStatus("No token configured"))
309@@ -335,7 +344,7 @@ class TestCharm(TestCase):
310 def test_config_changed_ppa_contains_newline(self):
311 self.harness.update_config({"ppa": "ppa:ua-client/stable\n"})
312 self.mocks["check_call"].assert_has_calls([
313- call(["add-apt-repository", "--yes", "ppa:ua-client/stable"]),
314+ call(["add-apt-repository", "--yes", "ppa:ua-client/stable"], env=self.env),
315 ])
316 self.assertEqual(self.harness.charm._state.ppa, "ppa:ua-client/stable")
317
318@@ -460,3 +469,39 @@ class TestCharm(TestCase):
319 handle.truncate.assert_called_once()
320 self.mocks["call"].assert_not_called()
321 self.assertEqual(self.harness.model.unit.status, BlockedStatus("No token configured"))
322+
323+ def test_setup_proxy_env(self):
324+ self.mocks["environ"].update(
325+ {
326+ "JUJU_CHARM_HTTP_PROXY": TEST_PROXY_URL,
327+ "JUJU_CHARM_HTTPS_PROXY": TEST_PROXY_URL,
328+ "JUJU_CHARM_NO_PROXY": TEST_NO_PROXY,
329+ }
330+ )
331+
332+ self.harness.charm._setup_proxy_env()
333+ self.assertEqual(self.harness.charm.env["http_proxy"], TEST_PROXY_URL)
334+ self.assertEqual(self.harness.charm.env["https_proxy"], TEST_PROXY_URL)
335+ self.assertEqual(self.harness.charm.env["no_proxy"], TEST_NO_PROXY)
336+
337+ def _add_ua_proxy_setup_calls(self, call_list, append=True):
338+ """Helper to generate the calls used for UA proxy setup"""
339+ proxy_calls = [
340+ call(
341+ [
342+ "ubuntu-advantage",
343+ "config",
344+ "set",
345+ "http_proxy={}".format(self.env["http_proxy"]),
346+ ]
347+ ),
348+ call(
349+ [
350+ "ubuntu-advantage",
351+ "config",
352+ "set",
353+ "https_proxy={}".format(self.env["https_proxy"]),
354+ ]
355+ ),
356+ ]
357+ return call_list + proxy_calls if append else proxy_calls + call_list

Subscribers

People subscribed via source and target branches