diff --git a/.gitignore b/.gitignore index 648277d7..9fb5be89 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ venv /.tox /test/data/certs/lib.sh + +debian-forge \ No newline at end of file diff --git a/stages/org.osbuild.apt b/stages/org.osbuild.apt new file mode 100644 index 00000000..39b3063a --- /dev/null +++ b/stages/org.osbuild.apt @@ -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) diff --git a/stages/org.osbuild.apt.meta.json b/stages/org.osbuild.apt.meta.json new file mode 100644 index 00000000..c8a6504f --- /dev/null +++ b/stages/org.osbuild.apt.meta.json @@ -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"] + } +} diff --git a/stages/org.osbuild.debootstrap b/stages/org.osbuild.debootstrap new file mode 100644 index 00000000..9d99fdd1 --- /dev/null +++ b/stages/org.osbuild.debootstrap @@ -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) diff --git a/stages/org.osbuild.debootstrap.meta.json b/stages/org.osbuild.debootstrap.meta.json new file mode 100644 index 00000000..854392c1 --- /dev/null +++ b/stages/org.osbuild.debootstrap.meta.json @@ -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"] + } +} diff --git a/stages/org.osbuild.ostree.commit b/stages/org.osbuild.ostree.commit index 812440b0..2f8f23ad 100755 --- a/stages/org.osbuild.ostree.commit +++ b/stages/org.osbuild.ostree.commit @@ -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) diff --git a/stages/org.osbuild.ostree.commit.meta.json b/stages/org.osbuild.ostree.commit.meta.json index 99ee0548..9150e394 100644 --- a/stages/org.osbuild.ostree.commit.meta.json +++ b/stages/org.osbuild.ostree.commit.meta.json @@ -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"] } }