apt-ostree/apt_ostree/bootstrap.py
Charles Short 2d8d3e9a3f Use python standard logging
Make the UI consistent by using python standarized logging everywhere.

Also log to the systemd-journald service so the logs are kept
around when a user deploys a newer version of a branch.

Story: 2010867
Task: 48556

Change-Id: I9f203dd6d3e0e17c0563f59efd5f4fa003fa030e
Signed-off-by: Charles Short <charles.short@windriver.com>
2023-10-23 14:02:10 -04:00

270 lines
9.8 KiB
Python

"""
Copyright (c) 2023 Wind River Systems, Inc.
SPDX-License-Identifier: Apache-2.0
"""
import hashlib
import logging
import os
import shutil
import sys
import apt
from rich.console import Console
from apt_ostree.constants import excluded_packages
from apt_ostree.ostree import Ostree
from apt_ostree.utils import run_command
class Bootstrap:
def __init__(self, state):
self.logging = logging.getLogger(__name__)
self.console = Console()
self.state = state
self.ostree = Ostree(self.state)
def create_rootfs(self):
"""Create a Debian system from a configuration file."""
if not self.state.base.exists():
self.logging.error("Configuration directory does not exist.")
sys.exit(1)
self.logging.info(f"Found configuration directory: {self.state.base}")
config = self.state.base.joinpath("bootstrap.yaml")
if not config.exists():
self.logging.error("bootstrap.yaml does not exist.")
sys.exit(1)
else:
self.loging.info("Found configuration file bootstrap.yaml.")
with self.console.status(
f"Setting up workspace for {self.state.branch}."):
workspace = self.state.workspace
workdir = workspace.joinpath(f"build/{self.state.branch}")
rootfs = workdir.joinpath("rootfs")
self.logging.info(f"Building workspace for {self.state.branch} "
f"in {workspace}")
if workdir.exists():
self.logging.info("Found working directory from "
"previous run...removing.")
shutil.rmtree(workdir)
workdir.mkdir(parents=True, exist_ok=True)
rootfs.mkdir(parents=True, exist_ok=True)
self.logging.info("Running bdebstrap, please wait.")
verbosity = "-q"
if self.state.debug:
verbosity = "-v"
run_command(
["bdebstrap", "-c", "bootstrap.yaml", verbosity,
"--force", "--name", str(self.state.branch),
"--target", str(rootfs),
"--output", str(workdir)], cwd=self.state.base)
self.ostree.init()
self.logging.info(f"Found ostree branch: {self.state.branch}")
self.create_ostree(rootfs)
r = self.ostree.ostree_commit(
rootfs,
branch=self.state.branch,
repo=self.state.repo,
subject="Commit by apt-ostree",
msg="Initialized by apt-ostree.")
if r.returncode != 0:
self.logging.info(f"Failed to commit {self.state.branch} to "
f"{self.state.repo}.")
self.logging.info(f"Commited {self.state.repo} to {self.state.repo}.")
def create_ostree(self, rootdir):
"""Create an ostree branch from a rootfs."""
with self.console.status(f"Creating ostree from {rootdir}."):
self.logging.info("Setting up /usr/lib/ostree-boot")
self.setup_boot(rootdir,
rootdir.joinpath("boot"),
rootdir.joinpath("usr/lib/ostree-boot"))
self.create_tmpfile_dir(rootdir)
self.convert_to_ostree(rootdir)
def convert_to_ostree(self, rootdir):
"""Convert rootfs to ostree."""
CRUFT = ["boot/initrd.img", "boot/vmlinuz",
"initrd.img", "initrd.img.old",
"vmlinuz", "vmlinuz.old"]
assert rootdir is not None and rootdir != ""
with self.console.status(f"Converting {rootdir} to ostree."):
dir_perm = 0o755
# Emptying /dev
self.logging.info("Emptying /dev.")
shutil.rmtree(rootdir.joinpath("dev"))
os.mkdir(rootdir.joinpath("dev"), dir_perm)
# Copying /var
self.sanitize_usr_symlinks(rootdir)
self.logging.info("Moving /var to /usr/rootdirs.")
os.mkdir(rootdir.joinpath("usr/rootdirs"), dir_perm)
# Make sure we preserve file permissions otherwise
# bubblewrap will complain that a file/directory
# permisisons/onership is not mapped correctly.
shutil.copytree(
rootdir.joinpath("var"),
rootdir.joinpath("usr/rootdirs/var"),
symlinks=True
)
shutil.rmtree(rootdir.joinpath("var"))
os.mkdir(rootdir.joinpath("var"), dir_perm)
# Remove unecessary files
self.logging.info("Removing unnecessary files.")
for c in CRUFT:
try:
os.remove(rootdir.joinpath(c))
except OSError:
pass
# Setup and split out etc
self.logging.info("Moving /etc to /usr/etc.")
shutil.move(rootdir.joinpath("etc"),
rootdir.joinpath("usr"))
self.logging.info("Setting up /ostree and /sysroot.")
try:
rootdir.joinpath("ostree").mkdir(
parents=True, exist_ok=True)
rootdir.joinpath("sysroot").mkdir(
parents=True, exist_ok=True)
except OSError:
pass
self.logging.info("Setting up symlinks.")
TOPLEVEL_LINKS = {
"media": "run/media",
"mnt": "var/mnt",
"opt": "var/opt",
"ostree": "sysroot/ostree",
"root": "var/roothome",
"srv": "var/srv",
"usr/local": "../var/usrlocal",
}
fd = os.open(rootdir, os.O_DIRECTORY)
for l, t in TOPLEVEL_LINKS.items():
shutil.rmtree(rootdir.joinpath(l))
os.symlink(t, l, dir_fd=fd)
def sanitize_usr_symlinks(self, rootdir):
"""Replace symlinks from /usr pointing to /var"""
usrdir = os.path.join(rootdir, "usr")
for base, dirs, files in os.walk(usrdir):
for name in files:
p = os.path.join(base, name)
if not os.path.islink(p):
continue
# Resolve symlink relative to root
link = os.readlink(p)
if os.path.isabs(link):
target = os.path.join(rootdir, link[1:])
else:
target = os.path.join(base, link)
rel = os.path.relpath(target, rootdir)
# Keep symlinks if they're pointing to a location under /usr
if os.path.commonpath([target, usrdir]) == usrdir:
continue
toplevel = self.get_toplevel(rel)
# Sanitize links going into /var, potentially
# other location can be added later
if toplevel != 'var':
continue
os.remove(p)
os.link(target, p)
def get_toplevel(self, path):
"""Get the top level diretory."""
head, tail = os.path.split(path)
while head != '/' and head != '':
head, tail = os.path.split(head)
return tail
def setup_boot(self, rootdir, bootdir, targetdir):
"""Setup up the ostree bootdir"""
vmlinuz = None
initrd = None
dtbs = None
version = None
try:
os.mkdir(targetdir)
except OSError:
pass
for item in os.listdir(bootdir):
if item.startswith("vmlinuz"):
assert vmlinuz is None
vmlinuz = item
_, version = item.split("-", 1)
elif item.startswith("initrd.img") or item.startswith("initramfs"):
assert initrd is None
initrd = item
elif item.startswith("dtbs"):
assert dtbs is None
dtbs = os.path.join(bootdir, item)
else:
# Move all other artifacts as is
shutil.move(os.path.join(bootdir, item), targetdir)
assert vmlinuz is not None
m = hashlib.sha256()
m.update(open(os.path.join(bootdir, vmlinuz), mode="rb").read())
if initrd is not None:
m.update(open(os.path.join(bootdir, initrd), "rb").read())
csum = m.hexdigest()
os.rename(os.path.join(bootdir, vmlinuz),
os.path.join(targetdir, vmlinuz + "-" + csum))
if initrd is not None:
os.rename(os.path.join(bootdir, initrd),
os.path.join(targetdir,
initrd.replace(
"initrd.img", "initramfs")
+ "-" + csum))
def create_tmpfile_dir(self, rootdir):
"""Ensure directoeies in /var are created."""
with self.console.status("Creating systemd-tmpfiles configuration"):
cache = apt.cache.Cache(rootdir=rootdir)
dirs = []
for pkg in cache:
if "/var" in pkg.installed_files and \
pkg.name not in excluded_packages:
dirs += [file for file in pkg.installed_files
if file.startswith("/var")]
if len(dirs) == 0:
return
conf = rootdir.joinpath(
"usr/lib/tmpfiles.d/ostree-integration-autovar.conf")
if conf.exists():
os.unlink(conf)
with open(conf, "w") as f:
f.write("# Auto-genernated by apt-ostree\n")
for d in (dirs):
if d not in [
"/var",
"/var/lock",
"/var/cache",
"/var/spool",
"/var/log",
"/var/lib"]:
f.write(f"L {d} - - - - ../../usr/rootdirs{d}\n")