Add Debian-specific OSBuild stages: apt, debootstrap, ostree-commit
Some checks are pending
Checks / Spelling (push) Waiting to run
Checks / Python Linters (push) Waiting to run
Checks / Shell Linters (push) Waiting to run
Checks / 📦 Packit config lint (push) Waiting to run
Checks / 🔍 Check for valid snapshot urls (push) Waiting to run
Checks / 🔍 Check JSON files for formatting consistency (push) Waiting to run
Generate / Documentation (push) Waiting to run
Generate / Test Data (push) Waiting to run
Tests / Unittest (push) Waiting to run
Tests / Assembler test (legacy) (push) Waiting to run
Tests / Smoke run: unittest as normal user on default runner (push) Waiting to run
Some checks are pending
Checks / Spelling (push) Waiting to run
Checks / Python Linters (push) Waiting to run
Checks / Shell Linters (push) Waiting to run
Checks / 📦 Packit config lint (push) Waiting to run
Checks / 🔍 Check for valid snapshot urls (push) Waiting to run
Checks / 🔍 Check JSON files for formatting consistency (push) Waiting to run
Generate / Documentation (push) Waiting to run
Generate / Test Data (push) Waiting to run
Tests / Unittest (push) Waiting to run
Tests / Assembler test (legacy) (push) Waiting to run
Tests / Smoke run: unittest as normal user on default runner (push) Waiting to run
This commit is contained in:
parent
3e37ad3b92
commit
31162116f8
7 changed files with 465 additions and 105 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -25,3 +25,5 @@ venv
|
|||
/.tox
|
||||
|
||||
/test/data/certs/lib.sh
|
||||
|
||||
debian-forge
|
||||
82
stages/org.osbuild.apt
Normal file
82
stages/org.osbuild.apt
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
#!/usr/bin/python3
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import json
|
||||
|
||||
import osbuild.api
|
||||
|
||||
|
||||
def run_apt_command(tree, command, env=None):
|
||||
"""Run apt command in the target filesystem"""
|
||||
if env is None:
|
||||
env = {}
|
||||
|
||||
# Set up environment for non-interactive operation
|
||||
apt_env = {
|
||||
"DEBIAN_FRONTEND": "noninteractive",
|
||||
"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
}
|
||||
apt_env.update(env)
|
||||
|
||||
# Run command in chroot
|
||||
cmd = ["chroot", tree] + command
|
||||
result = subprocess.run(cmd, env=apt_env, capture_output=True, text=True)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"Error running apt command: {command}")
|
||||
print(f"stdout: {result.stdout}")
|
||||
print(f"stderr: {result.stderr}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def main(tree, options):
|
||||
"""Main function for apt stage"""
|
||||
|
||||
# Get options
|
||||
packages = options.get("packages", [])
|
||||
recommends = options.get("recommends", False)
|
||||
unauthenticated = options.get("unauthenticated", False)
|
||||
update = options.get("update", True)
|
||||
|
||||
if not packages:
|
||||
print("No packages specified for installation")
|
||||
return 1
|
||||
|
||||
# Update package lists if requested
|
||||
if update:
|
||||
print("Updating package lists...")
|
||||
if not run_apt_command(tree, ["apt-get", "update"]):
|
||||
return 1
|
||||
|
||||
# Build apt-get install command
|
||||
apt_options = ["apt-get", "-y"]
|
||||
|
||||
if not recommends:
|
||||
apt_options.append("--no-install-recommends")
|
||||
|
||||
if unauthenticated:
|
||||
apt_options.append("--allow-unauthenticated")
|
||||
|
||||
apt_options.extend(["install"] + packages)
|
||||
|
||||
# Install packages
|
||||
print(f"Installing packages: {', '.join(packages)}")
|
||||
if not run_apt_command(tree, apt_options):
|
||||
return 1
|
||||
|
||||
# Clean up package cache
|
||||
print("Cleaning package cache...")
|
||||
if not run_apt_command(tree, ["apt-get", "clean"]):
|
||||
return 1
|
||||
|
||||
print("Package installation completed successfully")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = osbuild.api.arguments()
|
||||
r = main(args["tree"], args["options"])
|
||||
sys.exit(r)
|
||||
52
stages/org.osbuild.apt.meta.json
Normal file
52
stages/org.osbuild.apt.meta.json
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"name": "org.osbuild.apt",
|
||||
"version": "1",
|
||||
"description": "Install Debian packages using apt",
|
||||
"options": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"packages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of packages to install"
|
||||
},
|
||||
"recommends": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Install recommended packages"
|
||||
},
|
||||
"unauthenticated": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Allow unauthenticated packages"
|
||||
},
|
||||
"update": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Update package lists before installation"
|
||||
}
|
||||
},
|
||||
"required": ["packages"]
|
||||
},
|
||||
"inputs": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"devices": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"mounts": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"capabilities": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": ["CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_FSETID", "CAP_MKNOD", "CAP_SETGID", "CAP_SETUID", "CAP_SYS_CHROOT"]
|
||||
}
|
||||
}
|
||||
111
stages/org.osbuild.debootstrap
Normal file
111
stages/org.osbuild.debootstrap
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
#!/usr/bin/python3
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
import osbuild.api
|
||||
|
||||
|
||||
def run_debootstrap(suite, target, mirror, arch=None, variant=None, extra_packages=None):
|
||||
"""Run debootstrap to create base Debian filesystem"""
|
||||
|
||||
cmd = ["debootstrap"]
|
||||
|
||||
if arch:
|
||||
cmd.extend(["--arch", arch])
|
||||
|
||||
if variant:
|
||||
cmd.extend(["--variant", variant])
|
||||
|
||||
if extra_packages:
|
||||
cmd.extend(["--include", ",".join(extra_packages)])
|
||||
|
||||
cmd.extend([suite, target, mirror])
|
||||
|
||||
print(f"Running debootstrap: {' '.join(cmd)}")
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"Error running debootstrap:")
|
||||
print(f"stdout: {result.stdout}")
|
||||
print(f"stderr: {result.stderr}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def setup_apt_sources(tree, suite, mirror):
|
||||
"""Set up apt sources.list for the target filesystem"""
|
||||
|
||||
sources_content = f"""deb {mirror} {suite} main
|
||||
deb {mirror} {suite}-updates main
|
||||
deb {mirror} {suite}-security main
|
||||
"""
|
||||
|
||||
sources_path = os.path.join(tree, "etc/apt/sources.list")
|
||||
os.makedirs(os.path.dirname(sources_path), exist_ok=True)
|
||||
|
||||
with open(sources_path, "w") as f:
|
||||
f.write(sources_content)
|
||||
|
||||
print(f"Created sources.list for {suite}")
|
||||
|
||||
|
||||
def main(tree, options):
|
||||
"""Main function for debootstrap stage"""
|
||||
|
||||
# Get options
|
||||
suite = options.get("suite", "bookworm")
|
||||
mirror = options.get("mirror", "http://deb.debian.org/debian")
|
||||
arch = options.get("arch")
|
||||
variant = options.get("variant", "minbase")
|
||||
extra_packages = options.get("extra_packages", [])
|
||||
|
||||
if not suite:
|
||||
print("No suite specified for debootstrap")
|
||||
return 1
|
||||
|
||||
if not mirror:
|
||||
print("No mirror specified for debootstrap")
|
||||
return 1
|
||||
|
||||
# Create temporary directory for debootstrap
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
print(f"Creating base Debian filesystem in {temp_dir}")
|
||||
|
||||
# Run debootstrap
|
||||
if not run_debootstrap(suite, temp_dir, mirror, arch, variant, extra_packages):
|
||||
return 1
|
||||
|
||||
# Set up apt sources
|
||||
setup_apt_sources(temp_dir, suite, mirror)
|
||||
|
||||
# Copy files to target tree
|
||||
print(f"Copying filesystem to target tree: {tree}")
|
||||
|
||||
# Ensure target directory exists
|
||||
os.makedirs(tree, exist_ok=True)
|
||||
|
||||
# Copy all files from temp directory to target
|
||||
for item in os.listdir(temp_dir):
|
||||
src = os.path.join(temp_dir, item)
|
||||
dst = os.path.join(tree, item)
|
||||
|
||||
if os.path.isdir(src):
|
||||
if os.path.exists(dst):
|
||||
shutil.rmtree(dst)
|
||||
shutil.copytree(src, dst)
|
||||
else:
|
||||
shutil.copy2(src, dst)
|
||||
|
||||
print("Base Debian filesystem created successfully")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = osbuild.api.arguments()
|
||||
r = main(args["tree"], args["options"])
|
||||
sys.exit(r)
|
||||
57
stages/org.osbuild.debootstrap.meta.json
Normal file
57
stages/org.osbuild.debootstrap.meta.json
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"name": "org.osbuild.debootstrap",
|
||||
"version": "1",
|
||||
"description": "Create base Debian filesystem using debootstrap",
|
||||
"options": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"suite": {
|
||||
"type": "string",
|
||||
"default": "bookworm",
|
||||
"description": "Debian suite to bootstrap (e.g., bookworm, sid)"
|
||||
},
|
||||
"mirror": {
|
||||
"type": "string",
|
||||
"default": "http://deb.debian.org/debian",
|
||||
"description": "Debian mirror URL"
|
||||
},
|
||||
"arch": {
|
||||
"type": "string",
|
||||
"description": "Target architecture (e.g., amd64, arm64)"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string",
|
||||
"default": "minbase",
|
||||
"description": "Debootstrap variant (e.g., minbase, buildd)"
|
||||
},
|
||||
"extra_packages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": [],
|
||||
"description": "Additional packages to include in base filesystem"
|
||||
}
|
||||
},
|
||||
"required": ["suite", "mirror"]
|
||||
},
|
||||
"inputs": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"devices": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"mounts": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"capabilities": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": ["CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_FSETID", "CAP_MKNOD", "CAP_SETGID", "CAP_SETUID"]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,73 +1,122 @@
|
|||
#!/usr/bin/python3
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import subprocess
|
||||
import json
|
||||
import tempfile
|
||||
|
||||
from osbuild import api
|
||||
from osbuild.util import ostree
|
||||
import osbuild.api
|
||||
|
||||
|
||||
def main(inputs, output_dir, options, meta):
|
||||
tree = inputs["tree"]["path"]
|
||||
selinux_label_version = options.get("selinux-label-version", 0)
|
||||
def run_ostree_command(cmd, cwd=None, env=None):
|
||||
"""Run ostree command and return result"""
|
||||
if env is None:
|
||||
env = {}
|
||||
|
||||
result = subprocess.run(cmd, cwd=cwd, env=env, capture_output=True, text=True)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"Error running ostree command: {' '.join(cmd)}")
|
||||
print(f"stdout: {result.stdout}")
|
||||
print(f"stderr: {result.stderr}")
|
||||
return False, result.stderr
|
||||
|
||||
return True, result.stdout
|
||||
|
||||
ref = options["ref"]
|
||||
os_version = options.get("os_version", None)
|
||||
parent = options.get("parent", None)
|
||||
|
||||
# rpm-ostree compose commit wants to consume the commit
|
||||
# so for now, we copy the whole tree and let it have its
|
||||
# way
|
||||
with tempfile.TemporaryDirectory(dir=output_dir) as root:
|
||||
subprocess.run(["cp", "--reflink=auto", "-a",
|
||||
f"{tree}/.", root],
|
||||
check=True)
|
||||
def create_ostree_repo(repo_path):
|
||||
"""Create OSTree repository if it doesn't exist"""
|
||||
if not os.path.exists(repo_path):
|
||||
print(f"Creating OSTree repository at {repo_path}")
|
||||
success, _ = run_ostree_command(["ostree", "init", "--repo", repo_path])
|
||||
if not success:
|
||||
return False
|
||||
return True
|
||||
|
||||
repo = os.path.join(output_dir, "repo")
|
||||
|
||||
treefile = ostree.Treefile()
|
||||
treefile["ref"] = ref
|
||||
if selinux_label_version != 0:
|
||||
# Don't set if 0 (default), to support older rpm-ostree versions
|
||||
treefile["selinux-label-version"] = selinux_label_version
|
||||
def create_commit(repo_path, tree_path, branch, subject, metadata=None, collection_id=None, ref_binding=None):
|
||||
"""Create OSTree commit from filesystem tree"""
|
||||
|
||||
# Prepare commit command
|
||||
cmd = ["ostree", "commit", "--repo", repo_path, "--branch", branch]
|
||||
|
||||
if subject:
|
||||
cmd.extend(["--subject", subject])
|
||||
|
||||
if collection_id:
|
||||
cmd.extend(["--collection-id", collection_id])
|
||||
|
||||
if ref_binding:
|
||||
for ref in ref_binding:
|
||||
cmd.extend(["--ref-binding", ref])
|
||||
|
||||
if metadata:
|
||||
for key, value in metadata.items():
|
||||
cmd.extend(["--add-metadata-string", f"{key}={value}"])
|
||||
|
||||
# Add tree path
|
||||
cmd.append(tree_path)
|
||||
|
||||
print(f"Creating OSTree commit: {' '.join(cmd)}")
|
||||
|
||||
success, output = run_ostree_command(cmd)
|
||||
if not success:
|
||||
return False, None
|
||||
|
||||
# Extract commit hash from output
|
||||
commit_hash = output.strip()
|
||||
print(f"Created commit: {commit_hash}")
|
||||
|
||||
return True, commit_hash
|
||||
|
||||
argv = ["rpm-ostree", "compose", "commit"]
|
||||
argv += [f"--repo={repo}"]
|
||||
|
||||
if parent:
|
||||
argv += [f"--parent={parent}"]
|
||||
|
||||
if os_version:
|
||||
argv += [
|
||||
f"--add-metadata-string=version={os_version}",
|
||||
]
|
||||
|
||||
argv += [
|
||||
f"--add-metadata-string=rpmostree.inputhash={meta['id']}",
|
||||
f"--write-composejson-to={output_dir}/compose.json"
|
||||
]
|
||||
|
||||
with treefile.as_tmp_file() as path:
|
||||
argv += [path, root]
|
||||
|
||||
subprocess.run(argv,
|
||||
stdout=sys.stderr,
|
||||
check=True)
|
||||
|
||||
with open(os.path.join(output_dir, "compose.json"), "r", encoding="utf8") as f:
|
||||
compose = json.load(f)
|
||||
|
||||
api.metadata({"compose": compose})
|
||||
def main(tree, options):
|
||||
"""Main function for ostree commit stage"""
|
||||
|
||||
# Get options
|
||||
repository = options.get("repository", "ostree-repo")
|
||||
branch = options.get("branch", "debian/atomic")
|
||||
subject = options.get("subject", "Debian atomic commit")
|
||||
metadata = options.get("metadata", {})
|
||||
collection_id = options.get("collection_id")
|
||||
ref_binding = options.get("ref_binding", [])
|
||||
|
||||
if not branch:
|
||||
print("No branch specified for OSTree commit")
|
||||
return 1
|
||||
|
||||
# Create repository path
|
||||
repo_path = os.path.join(tree, repository)
|
||||
|
||||
# Create OSTree repository
|
||||
if not create_ostree_repo(repo_path):
|
||||
return 1
|
||||
|
||||
# Create commit
|
||||
success, commit_hash = create_commit(
|
||||
repo_path, tree, branch, subject, metadata, collection_id, ref_binding
|
||||
)
|
||||
|
||||
if not success:
|
||||
return 1
|
||||
|
||||
# Write commit info to output
|
||||
commit_info = {
|
||||
"repository": repository,
|
||||
"branch": branch,
|
||||
"commit": commit_hash,
|
||||
"subject": subject
|
||||
}
|
||||
|
||||
output_file = os.path.join(tree, "ostree-commit.json")
|
||||
with open(output_file, "w") as f:
|
||||
json.dump(commit_info, f, indent=2)
|
||||
|
||||
print("OSTree commit created successfully")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = api.arguments()
|
||||
|
||||
r = main(args["inputs"],
|
||||
args["tree"],
|
||||
args["options"],
|
||||
args["meta"])
|
||||
|
||||
args = osbuild.api.arguments()
|
||||
r = main(args["tree"], args["options"])
|
||||
sys.exit(r)
|
||||
|
|
|
|||
|
|
@ -1,58 +1,65 @@
|
|||
{
|
||||
"summary": "Assemble a file system tree into a ostree commit",
|
||||
"description": [
|
||||
"Needs a file system tree that is already conforming to the ostree",
|
||||
"system layout[1], specified via the `tree` input and commits it",
|
||||
"to a repository. The repository must have been created at `/repo`.",
|
||||
"Additional metadata is stored in `/compose.json` which contains",
|
||||
"the commit compose information. This is also returned via the",
|
||||
"metadata API to osbuild.",
|
||||
"[1] https://ostree.readthedocs.io/en/stable/manual/adapting-existing/"
|
||||
],
|
||||
"capabilities": [
|
||||
"CAP_MAC_ADMIN",
|
||||
"CAP_NET_ADMIN",
|
||||
"CAP_SYS_PTRACE"
|
||||
],
|
||||
"schema_2": {
|
||||
"options": {
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"ref"
|
||||
],
|
||||
"properties": {
|
||||
"ref": {
|
||||
"description": "OStree ref to create for the commit",
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"os_version": {
|
||||
"description": "Set the version of the OS as commit metadata",
|
||||
"name": "org.osbuild.ostree.commit",
|
||||
"version": "1",
|
||||
"description": "Create OSTree commit from filesystem tree",
|
||||
"options": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"repository": {
|
||||
"type": "string",
|
||||
"default": "ostree-repo",
|
||||
"description": "OSTree repository name/path"
|
||||
},
|
||||
"branch": {
|
||||
"type": "string",
|
||||
"default": "debian/atomic",
|
||||
"description": "OSTree branch name for the commit"
|
||||
},
|
||||
"subject": {
|
||||
"type": "string",
|
||||
"default": "Debian atomic commit",
|
||||
"description": "Commit message/subject"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"parent": {
|
||||
"description": "commit id of the parent commit",
|
||||
"default": {},
|
||||
"description": "Additional metadata key-value pairs"
|
||||
},
|
||||
"collection_id": {
|
||||
"type": "string",
|
||||
"description": "Collection ID for ref binding"
|
||||
},
|
||||
"ref_binding": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"selinux-label-version": {
|
||||
"description": "Set selinux label version",
|
||||
"type": "integer",
|
||||
"default": 0
|
||||
}
|
||||
"default": [],
|
||||
"description": "List of ref bindings for the commit"
|
||||
}
|
||||
},
|
||||
"inputs": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"tree"
|
||||
],
|
||||
"properties": {
|
||||
"tree": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
"required": ["branch"]
|
||||
},
|
||||
"inputs": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"devices": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"mounts": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"capabilities": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": ["CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_FSETID", "CAP_MKNOD", "CAP_SETGID", "CAP_SETUID"]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue