mirror of
https://github.com/xcat2/confluent.git
synced 2026-08-05 10:17:51 +00:00
Add cross-architecture image build support to imgutil
Allow building EL and Ubuntu diskless images for a foreign architecture (e.g. aarch64 on an x86_64 host) by leveraging qemu-user-static. The target architecture is detected automatically from a -s source tree (for EL), or may be requested explicitly with the new --arch option. When the target differs from the host, dnf/debootstrap is invoked with --forcearch/--arch and the presence of an enabled binfmt_misc handler with the F (fix-binary) flag is verified up front, so emulation keeps working inside the installroot chroot and a missing setup yields an actionable error instead of a confusing exec failure mid-build. The image architecture is recorded in confluentimg.buildinfo so that pack selects the initramfs addons for the image architecture rather than the build host, and exec of a foreign-arch root performs the same binfmt check.
This commit is contained in:
@@ -43,6 +43,16 @@
|
||||
* `-s`, `--source` <directory>:
|
||||
Directory to pull installation from, typically a subdirectory of `/var/lib/confluent/distributions`. By default, the repositories for the build system are used. For Ubuntu, this is not supported; the build system repositories are always used.
|
||||
|
||||
* `--arch` <architecture>:
|
||||
Target architecture to build for (`x86_64` or `aarch64`). For Ubuntu, the
|
||||
Debian-style names `amd64` and `arm64` are accepted as aliases. Building
|
||||
for a foreign architecture is supported on EL and Ubuntu build hosts and
|
||||
requires a statically linked qemu-user emulator registered in binfmt_misc
|
||||
with the F (fix-binary) flag; on Ubuntu this is provided by the
|
||||
qemu-user-static package (qemu-user-binfmt as of Ubuntu 26.04). For EL,
|
||||
the architecture is also detected automatically when building from a `-s`
|
||||
source tree.
|
||||
|
||||
* `-y`, `--non-interactive`:
|
||||
Avoid prompting for confirmation.
|
||||
|
||||
@@ -99,6 +109,15 @@ Build a diskless image from a distribution:
|
||||
|
||||
imgutil build -s alma-9.6-x86_64 /tmp/myimage
|
||||
|
||||
Build an aarch64 EL diskless image on an x86_64 host (the architecture is
|
||||
detected from the source tree):
|
||||
|
||||
imgutil build -s alma-9.6-aarch64 /tmp/myimage
|
||||
|
||||
Build an aarch64 Ubuntu diskless image on an amd64 Ubuntu host:
|
||||
|
||||
imgutil build --arch aarch64 /tmp/myimage
|
||||
|
||||
Execute a shell in an unpacked image:
|
||||
|
||||
imgutil exec /tmp/myimage
|
||||
|
||||
+154
-8
@@ -15,7 +15,6 @@ import glob
|
||||
import json
|
||||
import argparse
|
||||
import os
|
||||
import platform
|
||||
import pwd
|
||||
import re
|
||||
import shutil
|
||||
@@ -259,7 +258,7 @@ async def capture_remote(args):
|
||||
os.symlink('/var/lib/confluent/public/site/initramfs.cpio',
|
||||
os.path.join(outdir, 'boot/initramfs/site.cpio'))
|
||||
confdir = '/opt/confluent/lib/osdeploy/{}-diskless'.format(oscat)
|
||||
archaddon = '/opt/confluent/lib/osdeploy/{}-diskless/initramfs/{}/addons.cpio'.format(oscat, platform.machine())
|
||||
archaddon = '/opt/confluent/lib/osdeploy/{}-diskless/initramfs/{}/addons.cpio'.format(oscat, os.uname().machine)
|
||||
if os.path.exists(archaddon):
|
||||
os.symlink(archaddon, os.path.join(outdir, 'boot/initramfs/addons.cpio'))
|
||||
else:
|
||||
@@ -462,6 +461,8 @@ def get_mydir(oscategory):
|
||||
return gencopy
|
||||
|
||||
class OsHandler(object):
|
||||
crossbuild = False
|
||||
|
||||
def __init__(self, name, version, arch, args):
|
||||
self.name = name
|
||||
self._interactive = True
|
||||
@@ -496,6 +497,10 @@ class OsHandler(object):
|
||||
def set_interactive(self, shouldbeinteractive):
|
||||
self._interactive = shouldbeinteractive
|
||||
|
||||
def set_arch(self, arch):
|
||||
self.arch = arch
|
||||
self.osname = '{}-{}-{}'.format(self.name, self.version, arch)
|
||||
|
||||
def get_json(self):
|
||||
odata = [self.oscategory, self.version, self.arch, self.name]
|
||||
for idx in range(len(odata)):
|
||||
@@ -653,7 +658,22 @@ class SuseHandler(OsHandler):
|
||||
run_constrainedx(fancy_chroot, (args, self.targpath))
|
||||
|
||||
|
||||
deb_arch_map = {
|
||||
'aarch64': 'arm64',
|
||||
'x86_64': 'amd64',
|
||||
}
|
||||
uname_arch_map = {
|
||||
'arm64': 'aarch64',
|
||||
'amd64': 'x86_64',
|
||||
}
|
||||
|
||||
|
||||
class DebHandler(OsHandler):
|
||||
crossbuild = True
|
||||
|
||||
def set_arch(self, arch):
|
||||
super().set_arch(uname_arch_map.get(arch, arch))
|
||||
|
||||
def __init__(self, name, version, arch, args, codename, hostpath):
|
||||
self.includepkgs = []
|
||||
self.targpath = None
|
||||
@@ -684,7 +704,10 @@ class DebHandler(OsHandler):
|
||||
shutil.copytree(srcdir, targdir)
|
||||
os.chmod(os.path.join(targdir, 'hooks/confluent'), 0o755)
|
||||
#cmd = ['debootstrap', '--include={0}'.format(','.join(self.includepkgs)), self.codename, self.targpath]
|
||||
cmd = ['debootstrap', self.codename, self.targpath]
|
||||
if self.arch != os.uname().machine:
|
||||
cmd = ['debootstrap', '--arch={0}'.format(deb_arch_map.get(self.arch, self.arch)), self.codename, self.targpath]
|
||||
else:
|
||||
cmd = ['debootstrap', self.codename, self.targpath]
|
||||
subprocess.check_call(cmd)
|
||||
|
||||
def _apt(self, args, cmd):
|
||||
@@ -736,6 +759,8 @@ class DebHandler(OsHandler):
|
||||
|
||||
|
||||
class ElHandler(OsHandler):
|
||||
crossbuild = True
|
||||
|
||||
def __init__(self, name, version, arch, args, hostpath='/'):
|
||||
self.oscategory = 'el{0}'.format(version.split('.')[0])
|
||||
self.yumargs = []
|
||||
@@ -774,7 +799,10 @@ class ElHandler(OsHandler):
|
||||
self.targpath = targpath
|
||||
self.yumargs.extend(
|
||||
['--installroot={0}'.format(targpath),
|
||||
'--releasever={0}'.format(self.version), 'install'])
|
||||
'--releasever={0}'.format(self.version)])
|
||||
if self.arch != os.uname().machine:
|
||||
self.yumargs.append('--forcearch={0}'.format(self.arch))
|
||||
self.yumargs.append('install')
|
||||
|
||||
def prep_root(self, args):
|
||||
mkdirp(os.path.join(self.targpath, 'usr/lib/dracut/modules.d'))
|
||||
@@ -949,6 +977,7 @@ def main():
|
||||
buildp.add_argument('-a', '--addpackagelist', action='append', default=[],
|
||||
help='A list of additional packages to include, may be specified multiple times')
|
||||
buildp.add_argument('-s', '--source', help='Directory to pull installation from, typically a subdirectory of /var/lib/confluent/distributions. By default, the repositories for the build system are used. For Ubuntu, this is not supported, the build system repositories are always used.')
|
||||
buildp.add_argument('--arch', default=None, help='Target architecture to build for (e.g. aarch64). When it differs from the build host architecture, statically linked qemu-user emulator registered in binfmt_misc with the F flag is required')
|
||||
buildp.add_argument('-y', '--non-interactive', help='Avoid prompting for confirmation', action='store_true')
|
||||
buildp.add_argument('-v', '--volume',
|
||||
help='Directory to make available in the build environment. -v / will '
|
||||
@@ -992,6 +1021,16 @@ def main():
|
||||
|
||||
|
||||
def exec_root(args):
|
||||
imgarch = None
|
||||
try:
|
||||
with open(os.path.join(args.scratchdir, 'etc/confluentimg.buildinfo')) as imginfo:
|
||||
for line in imginfo.readlines():
|
||||
if line.startswith('ARCH='):
|
||||
imgarch = line.split('=', 1)[1].strip()
|
||||
except IOError:
|
||||
pass
|
||||
if imgarch and imgarch != os.uname().machine:
|
||||
check_binfmt(imgarch, args)
|
||||
exitcode = run_constrained(exec_root_backend, args)
|
||||
if exitcode:
|
||||
sys.exit(exitcode)
|
||||
@@ -1077,6 +1116,7 @@ def build_root_backend(optargs):
|
||||
mkdirp(os.path.join(installroot, 'etc/'))
|
||||
with open(os.path.join(installroot, 'etc/confluentimg.buildinfo'), 'w') as imginfo:
|
||||
imginfo.write('BUILDDATE={}\n'.format(datetime.datetime.now().strftime('%Y-%m-%dT%H:%M')))
|
||||
imginfo.write('ARCH={}\n'.format(oshandler.arch))
|
||||
if args.source:
|
||||
imginfo.write('BUILDSRC={}\n'.format(args.source))
|
||||
|
||||
@@ -1117,6 +1157,81 @@ def check_root(installroot):
|
||||
mkdirp(installroot)
|
||||
|
||||
|
||||
qemu_arch_map = {
|
||||
'aarch64': 'qemu-aarch64',
|
||||
'x86_64': 'qemu-x86_64',
|
||||
}
|
||||
|
||||
|
||||
def check_binfmt(targetarch, args):
|
||||
qname = qemu_arch_map.get(targetarch, 'qemu-' + targetarch)
|
||||
binfmtdir = '/proc/sys/fs/binfmt_misc'
|
||||
hostdeb = fingerprint_host_deb(args)
|
||||
if hostdeb:
|
||||
# as of ubuntu 26.04, qemu-user-static is a virtual package and the
|
||||
# statically linked emulators come from qemu-user-binfmt instead
|
||||
qpkg = 'qemu-user-static'
|
||||
if float(hostdeb.version) >= 26.04:
|
||||
qpkg = 'qemu-user-binfmt'
|
||||
setupmsg = (
|
||||
'Emulating {0} requires the {2} package, which '
|
||||
'registers {1} in binfmt_misc with the F (fix-binary) flag '
|
||||
'("apt-get install {2}", then "update-binfmts '
|
||||
'--enable {1}" if the handler is disabled)\n'.format(
|
||||
targetarch, qname, qpkg))
|
||||
elif fingerprint_host_el(args):
|
||||
setupmsg = (
|
||||
'Emulating {0} requires /usr/bin/qemu-{0}-static registered in '
|
||||
'/etc/binfmt.d/qemu-{0}.conf with the F (fix-binary) flag, '
|
||||
'followed by "systemctl restart systemd-binfmt"\n'.format(
|
||||
targetarch))
|
||||
else:
|
||||
sys.stderr.write(
|
||||
'Cross-architecture builds are only supported on EL and Ubuntu '
|
||||
'build hosts\n')
|
||||
sys.exit(1)
|
||||
try:
|
||||
with open(os.path.join(binfmtdir, 'status')) as statusin:
|
||||
enabled = statusin.read().strip() == 'enabled'
|
||||
except IOError:
|
||||
enabled = False
|
||||
if not enabled:
|
||||
sys.stderr.write(
|
||||
'binfmt_misc is not enabled on this system. ' + setupmsg)
|
||||
sys.exit(1)
|
||||
for entry in glob.glob(os.path.join(binfmtdir, '*')):
|
||||
entryname = os.path.basename(entry)
|
||||
if entryname in ('register', 'status'):
|
||||
continue
|
||||
try:
|
||||
with open(entry) as entryin:
|
||||
entryinfo = entryin.read().split('\n')
|
||||
except IOError:
|
||||
continue
|
||||
interpreter = ''
|
||||
flags = ''
|
||||
for line in entryinfo[1:]:
|
||||
if line.startswith('interpreter '):
|
||||
interpreter = line.split(' ', 1)[1]
|
||||
elif line.startswith('flags:'):
|
||||
flags = line.split(':', 1)[1].strip()
|
||||
if entryname != qname and qname not in interpreter:
|
||||
continue
|
||||
if entryinfo[0].strip() != 'enabled':
|
||||
continue
|
||||
if 'F' not in flags:
|
||||
sys.stderr.write(
|
||||
'binfmt_misc handler {0} lacks the F (fix-binary) flag, so '
|
||||
'the emulator would not be reachable from within the image '
|
||||
'chroot. '.format(entryname) + setupmsg)
|
||||
sys.exit(1)
|
||||
return
|
||||
sys.stderr.write(
|
||||
'No enabled binfmt_misc handler for {0} found. '.format(targetarch)
|
||||
+ setupmsg)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def fingerprint_source_suse(files, sourcepath, args):
|
||||
if os.path.exists(os.path.join(sourcepath, 'distinfo.yaml')):
|
||||
with open(os.path.join(sourcepath, 'distinfo.yaml'), 'r') as distinfo:
|
||||
@@ -1293,6 +1408,22 @@ def build_root(args):
|
||||
sys.stderr.write(
|
||||
'Unable to recognize build system os\n')
|
||||
sys.exit(1)
|
||||
hostarch = os.uname().machine
|
||||
if args.arch and args.arch != oshandler.arch:
|
||||
if args.source:
|
||||
sys.stderr.write(
|
||||
'Requested architecture {0} conflicts with source directory '
|
||||
'architecture {1}\n'.format(args.arch, oshandler.arch))
|
||||
sys.exit(1)
|
||||
oshandler.set_arch(args.arch)
|
||||
if oshandler.arch != hostarch:
|
||||
if not oshandler.crossbuild:
|
||||
sys.stderr.write(
|
||||
'Building {0} images on a {1} host is currently not '
|
||||
'supported on this distribution\n'.format(
|
||||
oshandler.arch, hostarch))
|
||||
sys.exit(1)
|
||||
check_binfmt(oshandler.arch, args)
|
||||
if args.non_interactive:
|
||||
oshandler.set_interactive(False)
|
||||
oshandler.set_target(args.scratchdir)
|
||||
@@ -1425,12 +1556,15 @@ async def pack_image(args):
|
||||
sys.exit(1)
|
||||
imginfofile = os.path.join(args.scratchdir, 'etc/confluentimg.buildinfo')
|
||||
distpath = None
|
||||
imgarch = None
|
||||
try:
|
||||
with open(imginfofile) as imginfoin:
|
||||
imginfo = imginfoin.read().split('\n')
|
||||
for lineinfo in imginfo:
|
||||
if lineinfo.startswith('BUILDSRC='):
|
||||
distpath = lineinfo.replace('BUILDSRC=', '')
|
||||
elif lineinfo.startswith('ARCH='):
|
||||
imgarch = lineinfo.replace('ARCH=', '')
|
||||
except IOError:
|
||||
pass
|
||||
kerns = glob.glob(os.path.join(args.scratchdir, 'boot/vmlinuz-*'))
|
||||
@@ -1502,12 +1636,24 @@ async def pack_image(args):
|
||||
profiley.write('label: {0}\nkernelargs: quiet # confluent_imagemethod=untethered|tethered # tethered is default when unspecified to save on memory, untethered will use more ram, but will not have any ongoing runtime root fs dependency on the http servers.\n'.format(label))
|
||||
oscat = oshandler.oscategory
|
||||
confdir = '/opt/confluent/lib/osdeploy/{}-diskless'.format(oscat)
|
||||
archaddon = '/opt/confluent/lib/osdeploy/{}-diskless/initramfs/{}/addons.cpio'.format(oscat, platform.machine())
|
||||
packarch = imgarch if imgarch else os.uname().machine
|
||||
archaddon = '/opt/confluent/lib/osdeploy/{}-diskless/initramfs/{}/addons.cpio'.format(oscat, packarch)
|
||||
addonlink = os.path.join(outdir, 'boot/initramfs/addons.cpio')
|
||||
if os.path.exists(archaddon):
|
||||
os.symlink(archaddon, os.path.join(outdir, 'boot/initramfs/addons.cpio'))
|
||||
else:
|
||||
os.symlink(archaddon, addonlink)
|
||||
elif packarch == 'x86_64':
|
||||
# the unsuffixed addons path carries the x86_64 payload
|
||||
os.symlink('{}/initramfs/addons.cpio'.format(confdir),
|
||||
os.path.join(outdir, 'boot/initramfs/addons.cpio'))
|
||||
addonlink)
|
||||
else:
|
||||
# addons of another architecture would contain foreign
|
||||
# binaries that break boot, so link to the (currently
|
||||
# missing) target arch path rather than falling back
|
||||
sys.stderr.write(
|
||||
'Warning: {0} does not exist, the profile will not be '
|
||||
'bootable until the confluent_osdeploy-{1} package is '
|
||||
'installed\n'.format(archaddon, packarch))
|
||||
os.symlink(archaddon, addonlink)
|
||||
indir = '{}/profiles/default'.format(confdir)
|
||||
if os.path.exists(indir):
|
||||
copy_tree(indir, outdir)
|
||||
|
||||
Reference in New Issue
Block a user