Skip to content

Commit cc0d0ed

Browse files
committed
common: add shared FLS binary download utilities
Add a shared module for FLS (Flasher) binary download utilities that can be used by multiple drivers. Update flashers driver to use the shared get_fls_github_url() function. Signed-off-by: Benny Zlotnik <bzlotnik@redhat.com> Assisted-by: claude-opus-4.5
1 parent 1e279c7 commit cc0d0ed

5 files changed

Lines changed: 221 additions & 4 deletions

File tree

python/Dockerfile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,17 @@ RUN dnf install -y make git && \
77
COPY --from=uv /uv /uvx /bin/
88

99
FROM fedora:43 AS product
10-
RUN dnf install -y python3 ustreamer libusb1 android-tools python3-libgpiod && \
10+
RUN dnf install -y python3 ustreamer libusb1 android-tools python3-libgpiod curl && \
1111
dnf clean all && \
1212
rm -rf /var/cache/dnf
1313
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
1414

15+
# Install FLS binary for OCI flashing support
16+
ARG FLS_VERSION=0.1.9
17+
RUN curl -L "https://github.com/jumpstarter-dev/fls/releases/download/${FLS_VERSION}/fls-$(uname -m)-linux" \
18+
-o /usr/local/bin/fls && \
19+
chmod +x /usr/local/bin/fls
20+
1521
FROM builder AS wheels
1622
ADD . /src
1723
RUN make -C /src/python build

python/packages/jumpstarter-driver-flashers/jumpstarter_driver_flashers/client.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
from jumpstarter.client.decorators import driver_click_group
2727
from jumpstarter.common.exceptions import ArgumentError, JumpstarterException
28+
from jumpstarter.common.fls import get_fls_github_url
2829

2930

3031
class FlashError(JumpstarterException):
@@ -533,8 +534,8 @@ def _flash_with_fls(
533534
self._download_fls_binary(console, prompt, fls_binary_url, f"Failed to download FLS from {fls_binary_url}")
534535
elif fls_version != "":
535536
self.logger.info(f"Downloading FLS version {fls_version} from GitHub releases")
536-
# Download fls binary to the target device (until it is available on the target device)
537-
fls_url = f"https://github.com/jumpstarter-dev/fls/releases/download/{fls_version}/fls-aarch64-linux"
537+
# Download fls binary to the target device (always aarch64 for target devices)
538+
fls_url = get_fls_github_url(fls_version, arch="aarch64")
538539
self._download_fls_binary(console, prompt, fls_url, f"Failed to download FLS from {fls_url}")
539540

540541
# Flash the image
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1+
from .fls import download_fls, get_fls_binary, get_fls_github_url
12
from .metadata import Metadata
23
from .tempfile import TemporarySocket, TemporaryTcpListener, TemporaryUnixListener
34

4-
__all__ = ["Metadata", "TemporarySocket", "TemporaryUnixListener", "TemporaryTcpListener"]
5+
__all__ = [
6+
"Metadata",
7+
"TemporarySocket",
8+
"TemporaryUnixListener",
9+
"TemporaryTcpListener",
10+
"download_fls",
11+
"get_fls_binary",
12+
"get_fls_github_url",
13+
]
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
"""FLS (Flasher) binary utilities.
2+
3+
This module provides functions for locating the FLS binary tool
4+
used for flashing devices via fastboot with OCI image support.
5+
6+
FLS is pre-installed in the container image for security and reliability,
7+
with optional configuration-based overrides for testing and flexibility.
8+
"""
9+
10+
import logging
11+
import os
12+
import platform
13+
import tempfile
14+
import urllib.request
15+
from pathlib import Path
16+
17+
logger = logging.getLogger(__name__)
18+
19+
FLS_GITHUB_REPO = "jumpstarter-dev/fls"
20+
21+
22+
def get_fls_github_url(version: str, arch: str | None = None) -> str:
23+
"""Get GitHub release URL for FLS version.
24+
25+
Args:
26+
version: FLS version (e.g., "0.1.9")
27+
arch: Target architecture (e.g., "aarch64", "x86_64"). If None,
28+
auto-detects from current platform.
29+
30+
Returns:
31+
Download URL for the architecture-appropriate binary
32+
"""
33+
if arch is None:
34+
arch = platform.machine().lower()
35+
else:
36+
arch = arch.lower()
37+
if arch in ("aarch64", "arm64"):
38+
binary_name = "fls-aarch64-linux"
39+
elif arch in ("x86_64", "amd64"):
40+
binary_name = "fls-x86_64-linux"
41+
else:
42+
binary_name = "fls-aarch64-linux" # Default to aarch64
43+
44+
return f"https://github.com/{FLS_GITHUB_REPO}/releases/download/{version}/{binary_name}"
45+
46+
47+
def download_fls(url: str) -> str:
48+
"""Download FLS binary from URL to a temp file.
49+
50+
Args:
51+
url: URL to download FLS binary from
52+
53+
Returns:
54+
Path to the downloaded binary
55+
56+
Raises:
57+
RuntimeError: If download fails
58+
"""
59+
fd, binary_path = tempfile.mkstemp(prefix="fls-")
60+
os.close(fd)
61+
try:
62+
logger.info(f"Downloading FLS binary from: {url}")
63+
urllib.request.urlretrieve(url, binary_path)
64+
Path(binary_path).chmod(0o755)
65+
logger.info(f"FLS binary downloaded to: {binary_path}")
66+
return binary_path
67+
except Exception as e:
68+
Path(binary_path).unlink(missing_ok=True)
69+
logger.error(f"Failed to download FLS from {url}: {e}")
70+
raise RuntimeError(f"Failed to download FLS from {url}: {e}") from e
71+
72+
73+
def get_fls_binary(
74+
fls_version: str | None = None,
75+
fls_binary_url: str | None = None,
76+
allow_custom_binaries: bool = False,
77+
) -> str:
78+
"""Get path to FLS binary with configuration-based overrides.
79+
80+
Args:
81+
fls_version: Optional FLS version to download from GitHub releases
82+
fls_binary_url: Custom URL to download FLS binary from
83+
allow_custom_binaries: Whether custom binary URLs are allowed
84+
85+
Returns:
86+
Path to the FLS binary
87+
88+
Raises:
89+
RuntimeError: If custom binary URL provided but not allowed
90+
91+
Note:
92+
Priority order:
93+
1. fls_binary_url (if allow_custom_binaries=True)
94+
2. fls_version from GitHub releases
95+
3. Pre-installed system binary
96+
"""
97+
if fls_binary_url:
98+
if not allow_custom_binaries:
99+
raise RuntimeError(
100+
"Custom FLS binary URLs are disabled for security. "
101+
"Set allow_custom_binaries=True in driver configuration to enable."
102+
)
103+
logger.warning(
104+
f"⚠️ SECURITY: Downloading custom FLS binary from {fls_binary_url}. "
105+
"Ensure this URL is trusted and secure."
106+
)
107+
return download_fls(fls_binary_url)
108+
109+
if fls_version:
110+
github_url = get_fls_github_url(fls_version)
111+
logger.warning(f"Downloading FLS version {fls_version} from GitHub: {github_url}")
112+
return download_fls(github_url)
113+
114+
logger.debug("Using pre-installed FLS from system PATH")
115+
return "fls"
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from unittest.mock import patch
2+
3+
import pytest
4+
5+
from .fls import download_fls, get_fls_binary, get_fls_github_url
6+
7+
8+
@pytest.mark.parametrize(
9+
"arch,version,expected_binary",
10+
[
11+
("aarch64", "0.1.9", "fls-aarch64-linux"),
12+
("arm64", "0.1.9", "fls-aarch64-linux"),
13+
("x86_64", "0.2.0", "fls-x86_64-linux"),
14+
("amd64", "0.2.0", "fls-x86_64-linux"),
15+
("unknown", "0.1.9", "fls-aarch64-linux"), # defaults to aarch64
16+
],
17+
)
18+
def test_get_fls_github_url_auto_detect(arch, version, expected_binary):
19+
"""Test architecture auto-detection from platform.machine()"""
20+
with patch("platform.machine", return_value=arch):
21+
url = get_fls_github_url(version)
22+
assert url == f"https://github.com/jumpstarter-dev/fls/releases/download/{version}/{expected_binary}"
23+
24+
25+
@pytest.mark.parametrize(
26+
"arch,version,expected_binary",
27+
[
28+
("aarch64", "0.1.9", "fls-aarch64-linux"),
29+
("AARCH64", "0.1.9", "fls-aarch64-linux"), # case insensitive
30+
("x86_64", "0.2.0", "fls-x86_64-linux"),
31+
],
32+
)
33+
def test_get_fls_github_url_explicit_arch(arch, version, expected_binary):
34+
"""Test explicit architecture parameter (used by flashers for target device)"""
35+
url = get_fls_github_url(version, arch=arch)
36+
assert url == f"https://github.com/jumpstarter-dev/fls/releases/download/{version}/{expected_binary}"
37+
38+
39+
def test_get_fls_binary_with_custom_url():
40+
with patch("jumpstarter.common.fls.download_fls", return_value="/tmp/custom-fls") as mock_download:
41+
result = get_fls_binary(fls_binary_url="https://example.com/fls", allow_custom_binaries=True)
42+
43+
mock_download.assert_called_once_with("https://example.com/fls")
44+
assert result == "/tmp/custom-fls"
45+
46+
47+
def test_get_fls_binary_custom_url_security_check():
48+
"""Test that custom URLs are blocked when allow_custom_binaries=False."""
49+
with pytest.raises(RuntimeError, match="Custom FLS binary URLs are disabled for security"):
50+
get_fls_binary(fls_binary_url="https://example.com/fls", allow_custom_binaries=False)
51+
52+
53+
def test_get_fls_binary_with_version():
54+
with patch("jumpstarter.common.fls.download_fls", return_value="/tmp/fls-0.1.9") as mock_download:
55+
with patch("jumpstarter.common.fls.get_fls_github_url", return_value="https://github.com/...") as mock_url:
56+
result = get_fls_binary(fls_version="0.1.9")
57+
58+
mock_url.assert_called_once_with("0.1.9")
59+
mock_download.assert_called_once()
60+
assert result == "/tmp/fls-0.1.9"
61+
62+
63+
def test_get_fls_binary_falls_back_to_path():
64+
result = get_fls_binary()
65+
assert result == "fls"
66+
67+
68+
def test_download_fls_success():
69+
with patch("urllib.request.urlretrieve") as mock_retrieve:
70+
with patch("tempfile.mkstemp", return_value=(99, "/tmp/fls-test")):
71+
with patch("os.close") as mock_close:
72+
with patch("pathlib.Path.chmod"):
73+
result = download_fls("https://example.com/fls")
74+
75+
mock_close.assert_called_once_with(99)
76+
mock_retrieve.assert_called_once_with("https://example.com/fls", "/tmp/fls-test")
77+
assert result == "/tmp/fls-test"
78+
79+
80+
def test_download_fls_failure():
81+
with patch("urllib.request.urlretrieve", side_effect=Exception("Network error")):
82+
with patch("tempfile.mkstemp", return_value=(99, "/tmp/fls-test")):
83+
with patch("os.close"):
84+
with patch("pathlib.Path.unlink"):
85+
with pytest.raises(RuntimeError, match="Failed to download FLS"):
86+
download_fls("https://example.com/fls")

0 commit comments

Comments
 (0)