Skip to content

Commit 8319a57

Browse files
authored
Merge pull request #26 from canonical/proxy-settings
Update charm to handle PS7 proxy egress
2 parents 82b5772 + 2170210 commit 8319a57

7 files changed

Lines changed: 269 additions & 26 deletions

File tree

src/charm.py

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
"""
1414

1515
import logging
16+
import os
17+
import socket
1618
from pathlib import Path
17-
from socket import getfqdn
1819

1920
import ops
2021

22+
import environment as env
2123
import importer_node as node
2224
import launchpad as lp
2325
import package_installation as pkgs
@@ -33,7 +35,7 @@
3335
GIT_UBUNTU_GIT_USER_NAME = "Ubuntu Git Importer"
3436
GIT_UBUNTU_GIT_EMAIL = "usd-importer-do-not-mail@canonical.com"
3537
GIT_UBUNTU_USER_HOME_DIR = "/var/local/git-ubuntu"
36-
GIT_UBUNTU_SOURCE_BASE_URL = "git.launchpad.net/git-ubuntu"
38+
GIT_UBUNTU_SOURCE_URL = "https://git.launchpad.net/git-ubuntu"
3739
GIT_UBUNTU_KEYRING_FOLDER = Path(__file__).parent.parent / "keyring"
3840

3941

@@ -138,20 +140,6 @@ def _lpuser_lp_key(self) -> str | None:
138140

139141
return None
140142

141-
@property
142-
def _git_ubuntu_source_url(self) -> str:
143-
"""Get the git-ubuntu source URL based on config.
144-
145-
If an SSH private key is provided, get a git+ssh URL, otherwise use https.
146-
147-
Returns:
148-
The git-ubuntu source URL.
149-
"""
150-
if self._lpuser_ssh_key is not None:
151-
return f"git+ssh://{self._lp_username}@{GIT_UBUNTU_SOURCE_BASE_URL}"
152-
153-
return f"https://{GIT_UBUNTU_SOURCE_BASE_URL}"
154-
155143
@property
156144
def _git_ubuntu_primary_relation(self) -> ops.Relation | None:
157145
"""Get the peer relation that contains the primary node IP.
@@ -195,7 +183,7 @@ def _set_peer_primary_node_address(self) -> bool:
195183
relation = self._git_ubuntu_primary_relation
196184

197185
if relation:
198-
new_primary_address = getfqdn()
186+
new_primary_address = socket.gethostbyname(socket.gethostname())
199187
relation.data[self.app]["primary_address"] = new_primary_address
200188
logger.info("Updated primary node address to %s", new_primary_address)
201189
return True
@@ -267,16 +255,44 @@ def _refresh_git_ubuntu_source(self) -> bool:
267255
"""
268256
self.unit.status = ops.MaintenanceStatus("Refreshing git-ubuntu source.")
269257

258+
# Set https proxy environment variable if available.
259+
https_proxy = env.get_juju_https_proxy_url()
260+
261+
if https_proxy != "":
262+
logger.info("Using https proxy %s for git-ubuntu source refresh.", https_proxy)
263+
os.environ["https_proxy"] = https_proxy
264+
265+
# Run clone or pull of git-ubuntu source.
270266
if not usr.refresh_git_ubuntu_source(
271267
GIT_UBUNTU_SYSTEM_USER_USERNAME,
272268
GIT_UBUNTU_USER_HOME_DIR,
273-
self._git_ubuntu_source_url,
269+
GIT_UBUNTU_SOURCE_URL,
274270
):
275271
self.unit.status = ops.BlockedStatus("Failed to refresh git-ubuntu source.")
276272
return False
277273

278274
return True
279275

276+
def _refresh_ssh_config(self) -> bool:
277+
"""Refresh the SSH config for the git-ubuntu user.
278+
279+
Returns:
280+
True if the config was updated successfully, False otherwise.
281+
"""
282+
self.unit.status = ops.MaintenanceStatus("Refreshing SSH config.")
283+
284+
if not usr.update_ssh_config(
285+
GIT_UBUNTU_SYSTEM_USER_USERNAME,
286+
GIT_UBUNTU_USER_HOME_DIR,
287+
env.get_juju_http_proxy_url(),
288+
):
289+
self.unit.status = ops.BlockedStatus(
290+
"Failed to update SSH config for git-ubuntu user."
291+
)
292+
return False
293+
294+
return True
295+
280296
def _refresh_importer_node(self) -> None:
281297
"""Remove old and install new git-ubuntu services."""
282298
self.unit.status = ops.MaintenanceStatus("Refreshing git-ubuntu services.")
@@ -290,6 +306,8 @@ def _refresh_importer_node(self) -> None:
290306
GIT_UBUNTU_USER_HOME_DIR,
291307
GIT_UBUNTU_SYSTEM_USER_USERNAME,
292308
self._controller_port,
309+
env.get_juju_http_proxy_url(),
310+
env.get_juju_https_proxy_url(),
293311
):
294312
self.unit.status = ops.BlockedStatus("Failed to install git-ubuntu services.")
295313
return
@@ -309,6 +327,7 @@ def _refresh_importer_node(self) -> None:
309327
self._is_publishing_active,
310328
self._controller_port,
311329
primary_ip,
330+
env.get_juju_https_proxy_url(),
312331
):
313332
self.unit.status = ops.BlockedStatus("Failed to install git-ubuntu services.")
314333
return
@@ -423,6 +442,7 @@ def _on_config_changed(self, _: ops.ConfigChangedEvent) -> None:
423442
and self._update_git_ubuntu_snap()
424443
and self._open_controller_port()
425444
and self._refresh_secret_keys()
445+
and self._refresh_ssh_config()
426446
and self._refresh_git_ubuntu_source()
427447
):
428448
# Initialize or re-install git-ubuntu services as needed.

src/environment.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2025 Canonical Ltd.
3+
# See LICENSE file for licensing details.
4+
5+
"""Environment information extraction."""
6+
7+
import os
8+
9+
10+
def get_juju_http_proxy_url() -> str:
11+
"""Get the Juju-managed http proxy URL if available.
12+
13+
Returns:
14+
The proxy URL or an empty string if it does not exist.
15+
"""
16+
env = os.environ.copy()
17+
http_proxy = env.get("JUJU_CHARM_HTTP_PROXY")
18+
19+
if http_proxy:
20+
return http_proxy
21+
22+
return ""
23+
24+
25+
def get_juju_https_proxy_url() -> str:
26+
"""Get the Juju-managed https proxy URL if available.
27+
28+
Returns:
29+
The proxy URL or an empty string if it does not exist.
30+
"""
31+
env = os.environ.copy()
32+
https_proxy = env.get("JUJU_CHARM_HTTPS_PROXY")
33+
34+
if https_proxy:
35+
return https_proxy
36+
37+
return ""

src/git_ubuntu.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,8 @@ def setup_poller_service(
178178
user: str,
179179
group: str,
180180
denylist: str,
181-
proxy: str = "",
181+
http_proxy: str = "",
182+
https_proxy: str = "",
182183
) -> bool:
183184
"""Set up poller systemd service file.
184185
@@ -187,7 +188,8 @@ def setup_poller_service(
187188
user: The user to run the service as.
188189
group: The permissions group to run the service as.
189190
denylist: The location of the package denylist.
190-
proxy: Optional proxy url.
191+
http_proxy: Optional HTTP proxy url.
192+
https_proxy: Optional HTTPS proxy url.
191193
192194
Returns:
193195
True if setup succeeded, False otherwise.
@@ -197,8 +199,11 @@ def setup_poller_service(
197199

198200
environment = "PYTHONUNBUFFERED=1"
199201

200-
if proxy:
201-
environment = f"http_proxy={proxy} " + environment
202+
if http_proxy:
203+
environment = f"http_proxy={http_proxy} " + environment
204+
205+
if https_proxy:
206+
environment = f"https_proxy={https_proxy} " + environment
202207

203208
service_string = generate_systemd_service_string(
204209
"git-ubuntu importer service poller",
@@ -225,6 +230,7 @@ def setup_worker_service(
225230
push_to_lp: bool = True,
226231
broker_ip: str = "127.0.0.1",
227232
broker_port: int = 1692,
233+
https_proxy: str = "",
228234
) -> bool:
229235
"""Set up worker systemd file with designated worker name.
230236
@@ -236,6 +242,7 @@ def setup_worker_service(
236242
push_to_lp: True if publishing repositories to Launchpad.
237243
broker_ip: The IP address of the broker process' node.
238244
broker_port: The network port that the broker provides tasks on.
245+
https_proxy: Optional HTTPS proxy url.
239246
240247
Returns:
241248
True if setup succeeded, False otherwise.
@@ -246,6 +253,11 @@ def setup_worker_service(
246253
broker_url = f"tcp://{broker_ip}:{broker_port}"
247254
exec_start = f"/snap/bin/git-ubuntu importer-service-worker{publish_arg} %i {broker_url}"
248255

256+
environment = "PYTHONUNBUFFERED=1"
257+
258+
if https_proxy:
259+
environment = f"https_proxy={https_proxy} " + environment
260+
249261
service_string = generate_systemd_service_string(
250262
"git-ubuntu importer service worker",
251263
user,
@@ -258,7 +270,7 @@ def setup_worker_service(
258270
timeout_abort_sec=600,
259271
watchdog_signal="SIGINT",
260272
private_tmp=True,
261-
environment="PYTHONUNBUFFERED=1",
273+
environment=environment,
262274
wanted_by="multi-user.target",
263275
)
264276

src/importer_node.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def setup_secondary_node(
2121
push_to_lp: bool,
2222
primary_port: int,
2323
primary_ip: str,
24+
https_proxy: str = "",
2425
) -> bool:
2526
"""Set up necessary services for a worker-only git-ubuntu importer node.
2627
@@ -32,6 +33,7 @@ def setup_secondary_node(
3233
push_to_lp: True if publishing repositories to Launchpad.
3334
primary_port: The network port used for worker assignments.
3435
primary_ip: The IP or network location of the primary node.
36+
https_proxy: URL for the environment's https proxy if required.
3537
3638
Returns:
3739
True if installation succeeded, False otherwise.
@@ -48,6 +50,7 @@ def setup_secondary_node(
4850
push_to_lp,
4951
primary_ip,
5052
primary_port,
53+
https_proxy,
5154
):
5255
logger.error("Failed to setup worker %s service.", worker_name)
5356
return False
@@ -59,13 +62,17 @@ def setup_primary_node(
5962
git_ubuntu_user_home: str,
6063
system_user: str,
6164
primary_port: int,
65+
http_proxy: str = "",
66+
https_proxy: str = "",
6267
) -> bool:
6368
"""Set up poller and broker services to create a primary git-ubuntu importer node.
6469
6570
Args:
6671
git_ubuntu_user_home: The home directory of the git-ubuntu user.
6772
system_user: The user + group to run the services as.
6873
primary_port: The network port used for worker assignments.
74+
http_proxy: URL for the environment's http proxy if required.
75+
https_proxy: URL for the environment's https proxy if required.
6976
7077
Returns:
7178
True if installation succeeded, False otherwise.
@@ -93,6 +100,8 @@ def setup_primary_node(
93100
system_user,
94101
system_user,
95102
denylist.as_posix(),
103+
http_proxy,
104+
https_proxy,
96105
):
97106
logger.error("Failed to setup poller service.")
98107
return False

src/user_management.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,61 @@ def update_launchpad_keyring_secret(user: str, home_dir: str, lp_key_data: str)
258258
return key_success
259259

260260

261+
def update_ssh_config(user: str, home_dir: str, http_proxy: str = "") -> bool:
262+
"""Create or refresh the .ssh/config file for git.launchpad.net handling.
263+
264+
Args:
265+
user: The git-ubuntu user.
266+
home_dir: The home directory for the user.
267+
http_proxy: The http proxy URL if required.
268+
269+
Returns:
270+
True if directory and file creation succeeded, False otherwise.
271+
"""
272+
ssh_config_file = pathops.LocalPath(home_dir, ".ssh/config")
273+
274+
parent_dir = ssh_config_file.parent
275+
276+
if not _mkdir_for_user_with_error_checking(parent_dir, user, 0o700):
277+
return False
278+
279+
config_success = False
280+
281+
ssh_config_content: str = (
282+
"Host git.launchpad.net\n"
283+
+ " HostName git.launchpad.net\n"
284+
+ " Port 22\n"
285+
+ " IdentityFile ~/.ssh/id\n"
286+
)
287+
288+
# Use socat to proxy ssh data if needed.
289+
if http_proxy != "":
290+
proxy_base_url = http_proxy.replace("http://", "").split(":")[0]
291+
proxy_port = (
292+
http_proxy.replace("http://", "").split(":")[1] if ":" in http_proxy else "3128"
293+
)
294+
ssh_config_content += (
295+
" ProxyCommand /usr/bin/socat - "
296+
+ f"PROXY:{proxy_base_url}:%h:%p,proxyport={proxy_port}\n"
297+
)
298+
299+
try:
300+
ssh_config_file.write_text(
301+
ssh_config_content,
302+
mode=0o664,
303+
user=user,
304+
group=user,
305+
)
306+
config_success = True
307+
except (FileNotFoundError, NotADirectoryError) as e:
308+
logger.error("Failed to create ssh config due to directory issues: %s", str(e))
309+
except LookupError as e:
310+
logger.error("Failed to create ssh config due to issues with root user: %s", str(e))
311+
except PermissionError as e:
312+
logger.error("Failed to create ssh config due to permission issues: %s", str(e))
313+
return config_success
314+
315+
261316
def set_snap_homedirs(home_dir: str) -> bool:
262317
"""Allow snaps to run for a user with a given home directory.
263318

tests/unit/test_environment.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Copyright 2025 Canonical Ltd.
2+
# See LICENSE file for licensing details.
3+
#
4+
# Learn more about testing at: https://juju.is/docs/sdk/testing
5+
6+
"""Unit tests for launchpad tools."""
7+
8+
import os
9+
10+
import environment as env
11+
12+
13+
def test_get_juju_http_proxy_url():
14+
"""Test getting Juju http proxy URL when not set."""
15+
proxy_var_original = os.environ.get("JUJU_CHARM_HTTP_PROXY")
16+
17+
if proxy_var_original is not None:
18+
del os.environ["JUJU_CHARM_HTTP_PROXY"]
19+
20+
proxy_url = env.get_juju_http_proxy_url()
21+
assert proxy_url == ""
22+
23+
test_url = "http://proxy.internal:1234"
24+
os.environ["JUJU_CHARM_HTTP_PROXY"] = test_url
25+
proxy_url = env.get_juju_http_proxy_url()
26+
assert proxy_url == test_url
27+
28+
del os.environ["JUJU_CHARM_HTTP_PROXY"]
29+
30+
if proxy_var_original is not None:
31+
os.environ["JUJU_CHARM_HTTP_PROXY"] = proxy_var_original
32+
33+
34+
def test_get_juju_https_proxy_url():
35+
"""Test getting Juju https proxy URL when not set."""
36+
proxy_var_original = os.environ.get("JUJU_CHARM_HTTPS_PROXY")
37+
38+
if proxy_var_original is not None:
39+
del os.environ["JUJU_CHARM_HTTPS_PROXY"]
40+
41+
proxy_url = env.get_juju_https_proxy_url()
42+
assert proxy_url == ""
43+
44+
test_url = "http://httpsproxy.internal:1234"
45+
os.environ["JUJU_CHARM_HTTPS_PROXY"] = test_url
46+
proxy_url = env.get_juju_https_proxy_url()
47+
assert proxy_url == test_url
48+
49+
del os.environ["JUJU_CHARM_HTTPS_PROXY"]
50+
51+
if proxy_var_original is not None:
52+
os.environ["JUJU_CHARM_HTTPS_PROXY"] = proxy_var_original

0 commit comments

Comments
 (0)