Skip to content

Commit 2aa9727

Browse files
authored
Merge pull request ceph#65936 from tchaikov/wip-build-cephadm-with-deb
cephadm, debian/rules: Use system packages for cephadm bundled dependencies Reviewed-by: John Mulligan <[email protected]>
2 parents bcde113 + 2568002 commit 2aa9727

File tree

5 files changed

+220
-20
lines changed

5 files changed

+220
-20
lines changed

debian/control

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ Build-Depends: automake,
107107
python3-requests <pkg.ceph.check>,
108108
python3-scipy <pkg.ceph.check>,
109109
python3-onelogin-saml2 <pkg.ceph.check>,
110+
python3-jinja2,
111+
python3-markupsafe,
110112
python3-setuptools,
111113
python3-sphinx,
112114
python3-venv,

debian/rules

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ extraopts += -DWITH_CEPHFS_JAVA=ON
2828
extraopts += -DWITH_CEPHFS_SHELL=ON
2929
extraopts += -DWITH_SYSTEMD=ON -DCEPH_SYSTEMD_ENV_DIR=/etc/default
3030
extraopts += -DWITH_GRAFANA=ON
31+
extraopts += -DCEPHADM_BUNDLED_DEPENDENCIES=deb
3132
ifeq ($(DEB_HOST_ARCH), amd64)
3233
extraopts += -DWITH_RBD_RWL=ON
3334
else

src/cephadm/build.py

Lines changed: 143 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ def enabled(self):
154154
class DependencyMode(enum.Enum):
155155
pip = enum.auto()
156156
rpm = enum.auto()
157+
deb = enum.auto()
157158
none = enum.auto()
158159

159160

@@ -169,6 +170,8 @@ def __init__(self, cli_args):
169170
self._setup_pip()
170171
elif self.deps_mode == DependencyMode.rpm:
171172
self._setup_rpm()
173+
elif self.deps_mode == DependencyMode.deb:
174+
self._setup_deb()
172175

173176
def _setup_pip(self):
174177
if self._maj_min == (3, 6):
@@ -180,6 +183,9 @@ def _setup_pip(self):
180183
def _setup_rpm(self):
181184
self.requirements = [InstallSpec(**v) for v in PY_REQUIREMENTS]
182185

186+
def _setup_deb(self):
187+
self.requirements = [InstallSpec(**v) for v in PY_REQUIREMENTS]
188+
183189

184190
class DependencyInfo:
185191
"""Type for tracking bundled dependencies."""
@@ -336,7 +342,9 @@ def _install_deps(tempdir, config):
336342
return _install_pip_deps(tempdir, config)
337343
if config.deps_mode == DependencyMode.rpm:
338344
return _install_rpm_deps(tempdir, config)
339-
raise ValueError(f'unexpected deps mode: {deps.mode}')
345+
if config.deps_mode == DependencyMode.deb:
346+
return _install_deb_deps(tempdir, config)
347+
raise ValueError(f'unexpected deps mode: {config.deps_mode}')
340348

341349

342350
def _install_pip_deps(tempdir, config):
@@ -437,7 +445,26 @@ def _install_rpm_deps(tempdir, config):
437445
return dinfo
438446

439447

440-
def _gather_rpm_package_dirs(paths):
448+
def _install_deb_deps(tempdir, config):
449+
log.info("Installing dependencies using Debian packages")
450+
dinfo = DependencyInfo(config)
451+
for pkg in config.requirements:
452+
log.info(f"Looking for debian package for: {pkg.name!r}")
453+
_deps_from_deb(tempdir, config, dinfo, pkg.name)
454+
return dinfo
455+
456+
457+
def _gather_package_dirs(paths, expected_parent_dir):
458+
"""Parse package file listing to find Python package directories.
459+
460+
Args:
461+
paths: List of file paths from package listing
462+
expected_parent_dir: Expected parent directory name (e.g., 'site-packages' for RPM,
463+
'dist-packages' for Debian)
464+
465+
Returns:
466+
Tuple of (metadata_dir, package_dirs)
467+
"""
441468
# = The easy way =
442469
# the top_level.txt file can be used to determine where the python packages
443470
# actually are. We need all of those and the meta-data dir (parent of
@@ -456,7 +483,7 @@ def _gather_rpm_package_dirs(paths):
456483
# = The hard way =
457484
# loop through the directories to find the .dist-info dir (containing the
458485
# mandatory METADATA file, according to the spec) and once we know the
459-
# location of dist info we find the sibling paths from the rpm listing
486+
# location of dist info we find the sibling paths from the package listing
460487
dist_info = None
461488
ppaths = []
462489
for path in paths:
@@ -467,9 +494,9 @@ def _gather_rpm_package_dirs(paths):
467494
break
468495
if not dist_info:
469496
raise ValueError('no .dist-info METADATA found')
470-
if not dist_info.parent.name == 'site-packages':
497+
if dist_info.parent.name != expected_parent_dir:
471498
raise ValueError(
472-
'unexpected parent directory (not site-packages):'
499+
f'unexpected parent directory (not {expected_parent_dir}):'
473500
f' {dist_info.parent.name}'
474501
)
475502
siblings = [
@@ -478,6 +505,31 @@ def _gather_rpm_package_dirs(paths):
478505
return dist_info, siblings
479506

480507

508+
def _copy_package_files(tempdir, paths, expected_parent_dir):
509+
"""Copy package files to the build directory.
510+
511+
Args:
512+
tempdir: Temporary directory to copy files to
513+
paths: List of file paths from package listing
514+
expected_parent_dir: Expected parent directory name per packaging convention:
515+
- 'site-packages' for RPM-based distributions
516+
- 'dist-packages' for Debian-based distributions
517+
518+
Returns:
519+
None
520+
"""
521+
meta_dir, pkg_dirs = _gather_package_dirs(paths, expected_parent_dir)
522+
meta_dest = tempdir / meta_dir.name
523+
log.info(f"Copying {meta_dir} to {meta_dest}")
524+
# copy the meta data directory
525+
shutil.copytree(meta_dir, meta_dest, ignore=_ignore_cephadmlib)
526+
# copy all the package directories
527+
for pkg_dir in pkg_dirs:
528+
pkg_dest = tempdir / pkg_dir.name
529+
log.info(f"Copying {pkg_dir} to {pkg_dest}")
530+
shutil.copytree(pkg_dir, pkg_dest, ignore=_ignore_cephadmlib)
531+
532+
481533
def _deps_from_rpm(tempdir, config, dinfo, pkg):
482534
# first, figure out what rpm provides a particular python lib
483535
dist = f'python3.{sys.version_info.minor}dist({pkg})'.lower()
@@ -513,16 +565,92 @@ def _deps_from_rpm(tempdir, config, dinfo, pkg):
513565
['rpm', '-ql', rpmname], check=True, stdout=subprocess.PIPE
514566
)
515567
paths = [l.decode('utf8') for l in res.stdout.splitlines()]
516-
meta_dir, pkg_dirs = _gather_rpm_package_dirs(paths)
517-
meta_dest = tempdir / meta_dir.name
518-
log.info(f"Copying {meta_dir} to {meta_dest}")
519-
# copy the meta data directory
520-
shutil.copytree(meta_dir, meta_dest, ignore=_ignore_cephadmlib)
521-
# copy all the package directories
522-
for pkg_dir in pkg_dirs:
523-
pkg_dest = tempdir / pkg_dir.name
524-
log.info(f"Copying {pkg_dir} to {pkg_dest}")
525-
shutil.copytree(pkg_dir, pkg_dest, ignore=_ignore_cephadmlib)
568+
# RPM-based distributions use 'site-packages' for Python packages
569+
_copy_package_files(tempdir, paths, 'site-packages')
570+
571+
572+
def _deps_from_deb(tempdir, config, dinfo, pkg):
573+
"""Extract Python dependencies from Debian packages.
574+
575+
Args:
576+
tempdir: Temporary directory to copy package files to
577+
config: Build configuration
578+
dinfo: DependencyInfo instance to track dependencies
579+
pkg: Python package name (e.g., 'MarkupSafe', 'Jinja2', 'PyYAML')
580+
"""
581+
# Convert Python package name to Debian package name
582+
# Python packages are typically named python3-<lowercase-name>
583+
# Handle special cases: PyYAML -> python3-yaml, MarkupSafe -> python3-markupsafe
584+
pkg_lower = pkg.lower()
585+
if pkg_lower == 'pyyaml':
586+
deb_pkg_name = 'python3-yaml'
587+
else:
588+
deb_pkg_name = f'python3-{pkg_lower}'
589+
590+
# First, try to find the package using apt-cache
591+
# This helps verify the package exists before trying to list its files
592+
try:
593+
res = subprocess.run(
594+
['apt-cache', 'show', deb_pkg_name],
595+
check=True,
596+
stdout=subprocess.PIPE,
597+
stderr=subprocess.PIPE,
598+
)
599+
except subprocess.CalledProcessError:
600+
# Package not found, try alternative naming
601+
log.warning(f"Package {deb_pkg_name} not found via apt-cache, trying dpkg -S")
602+
# Try to search for files that might belong to this package
603+
# Search for the Python module in site-packages
604+
search_pattern = f'/usr/lib/python3*/dist-packages/{pkg.lower()}*'
605+
try:
606+
res = subprocess.run(
607+
['dpkg', '-S', search_pattern],
608+
check=True,
609+
stdout=subprocess.PIPE,
610+
stderr=subprocess.PIPE,
611+
)
612+
# dpkg -S output format: "package: /path/to/file"
613+
deb_pkg_name = res.stdout.decode('utf8').split(':')[0].strip()
614+
except subprocess.CalledProcessError as err:
615+
log.error(f"Could not find Debian package for {pkg}")
616+
log.error(f"Tried: {deb_pkg_name} and pattern search")
617+
sys.exit(1)
618+
619+
# Get version information using dpkg-query
620+
try:
621+
res = subprocess.run(
622+
['dpkg-query', '-W', '-f=${Version}\\n', deb_pkg_name],
623+
check=True,
624+
stdout=subprocess.PIPE,
625+
)
626+
version = res.stdout.decode('utf8').strip()
627+
except subprocess.CalledProcessError as err:
628+
log.error(f"Could not query version for package {deb_pkg_name}: {err}")
629+
sys.exit(1)
630+
631+
log.info(f"Debian Package: {deb_pkg_name} (version: {version})")
632+
dinfo.add(
633+
pkg,
634+
deb_name=deb_pkg_name,
635+
version=version,
636+
package_source='deb',
637+
)
638+
639+
# Get the list of files provided by the Debian package
640+
try:
641+
res = subprocess.run(
642+
['dpkg', '-L', deb_pkg_name],
643+
check=True,
644+
stdout=subprocess.PIPE,
645+
)
646+
except subprocess.CalledProcessError as err:
647+
log.error(f"Could not list files for package {deb_pkg_name}: {err}")
648+
sys.exit(1)
649+
650+
paths = [l.decode('utf8') for l in res.stdout.splitlines()]
651+
# Debian-based distributions use 'dist-packages' for system-managed Python packages
652+
# per Debian Python Policy: https://www.debian.org/doc/packaging-manuals/python-policy/
653+
_copy_package_files(tempdir, paths, 'dist-packages')
526654

527655

528656
def generate_version_file(versioning_vars, dest):

src/cephadm/cephadm.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1694,6 +1694,7 @@ def command_version(ctx):
16941694
# type: (CephadmContext) -> int
16951695
import importlib
16961696
import zipimport
1697+
import zipfile
16971698
import types
16981699

16991700
vmod: Optional[types.ModuleType]
@@ -1750,10 +1751,17 @@ def command_version(ctx):
17501751
out['bundled_packages'] = deps_info
17511752
except OSError:
17521753
pass
1753-
files = getattr(loader, '_files', {})
1754-
out['zip_root_entries'] = sorted(
1755-
{p.split('/')[0] for p in files.keys()}
1756-
)
1754+
# Use zipfile module to properly read the archive contents
1755+
# loader.archive contains the path to the zip file
1756+
try:
1757+
with zipfile.ZipFile(loader.archive, 'r') as zf:
1758+
files = zf.namelist()
1759+
out['zip_root_entries'] = sorted(
1760+
{p.split('/')[0] for p in files if p}
1761+
)
1762+
except (OSError, zipfile.BadZipFile):
1763+
# Fallback to empty list if we can't read the zip
1764+
out['zip_root_entries'] = []
17571765

17581766
json.dump(out, sys.stdout, indent=2)
17591767
print()

src/cephadm/tests/build/test_cephadm_build.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,16 @@
4747
'base_image': 'docker.io/library/ubuntu:24.04',
4848
'script': 'apt update && apt install -y python3-venv',
4949
},
50+
'ubuntu-22.04-plusdeps': {
51+
'name': 'cephadm-build-test:ubuntu-22-04-py3-deps',
52+
'base_image': 'docker.io/library/ubuntu:22.04',
53+
'script': 'apt update && apt install -y python3-jinja2 python3-yaml python3-markupsafe',
54+
},
55+
'ubuntu-24.04-plusdeps': {
56+
'name': 'cephadm-build-test:ubuntu-24-04-py3-deps',
57+
'base_image': 'docker.io/library/ubuntu:24.04',
58+
'script': 'apt update && apt install -y python3-jinja2 python3-yaml python3-markupsafe',
59+
},
5060
}
5161

5262
BUILD_PY = 'src/cephadm/build.py'
@@ -193,7 +203,58 @@ def test_cephadm_build_from_rpms(env, source_dir, tmp_path):
193203
assert any(e.startswith('_cephadmmeta') for e in zre)
194204

195205

206+
@pytest.mark.parametrize(
207+
'env',
208+
[
209+
'ubuntu-22.04-plusdeps',
210+
'ubuntu-24.04-plusdeps',
211+
'ubuntu-22.04',
212+
],
213+
)
214+
def test_cephadm_build_from_debs(env, source_dir, tmp_path):
215+
res = build_in(
216+
env,
217+
source_dir,
218+
tmp_path,
219+
['-Bdeb', '-SCEPH_GIT_VER=0', '-SCEPH_GIT_NICE_VER=foobar'],
220+
)
221+
if 'plusdeps' not in env:
222+
assert res.returncode != 0
223+
return
224+
binary = tmp_path / 'cephadm'
225+
assert binary.is_file()
226+
res = subprocess.run(
227+
[sys.executable, str(binary), 'version'],
228+
stdout=subprocess.PIPE,
229+
)
230+
out = res.stdout.decode('utf8')
231+
assert 'version' in out
232+
assert 'foobar' in out
233+
assert res.returncode == 0
234+
res = subprocess.run(
235+
[sys.executable, str(binary), 'version', '--verbose'],
236+
stdout=subprocess.PIPE,
237+
)
238+
data = json.loads(res.stdout)
239+
assert isinstance(data, dict)
240+
assert 'bundled_packages' in data
241+
assert all(v['package_source'] == 'deb' for v in data['bundled_packages'])
242+
assert all(
243+
v['name'] in ('Jinja2', 'MarkupSafe', 'PyYAML')
244+
for v in data['bundled_packages']
245+
)
246+
assert all('requirements_entry' in v for v in data['bundled_packages'])
247+
assert 'zip_root_entries' in data
248+
zre = data['zip_root_entries']
249+
assert any(_dist_info(e, 'Jinja2') for e in zre)
250+
assert any(_dist_info(e, 'MarkupSafe') for e in zre)
251+
assert any(e.startswith('jinja2') for e in zre)
252+
assert any(e.startswith('markupsafe') for e in zre)
253+
assert any(e.startswith('cephadmlib') for e in zre)
254+
assert any(e.startswith('_cephadmmeta') for e in zre)
255+
256+
196257
def _dist_info(entry, name):
197258
return (
198-
entry.startswith(entry) or entry.startswith(entry.lower())
259+
entry.startswith(name) or entry.startswith(name.lower())
199260
) and (entry.endswith('.dist-info') or entry.endswith('.egg-info'))

0 commit comments

Comments
 (0)