diff --git a/stages/org.osbuild.apt.config b/stages/org.osbuild.apt.config new file mode 100644 index 00000000..5f2710ef --- /dev/null +++ b/stages/org.osbuild.apt.config @@ -0,0 +1,96 @@ +#!/usr/bin/python3 +import os +import sys +import configparser + +import osbuild.api + + +def make_apt_config(tree, config_options): + """Create or update apt configuration files""" + + # Create apt configuration directory + apt_conf_dir = f"{tree}/etc/apt/apt.conf.d" + os.makedirs(apt_conf_dir, exist_ok=True) + + # Create main apt configuration + apt_conf_path = f"{tree}/etc/apt/apt.conf" + + config = configparser.ConfigParser() + + # Try to read existing config + try: + with open(apt_conf_path, "r", encoding="utf8") as f: + config.read_file(f) + except FileNotFoundError: + print(f"Creating new apt configuration file: {apt_conf_path}") + + # Add or update configuration sections + for section, items in config_options.items(): + if not config.has_section(section): + config.add_section(section) + + for option, value in items.items(): + config.set(section, option, str(value)) + + # Write configuration + with open(apt_conf_path, "w", encoding="utf8") as f: + config.write(f) + + print(f"Updated apt configuration: {apt_conf_path}") + + +def make_apt_sources_config(tree, sources_options): + """Create or update apt sources configuration""" + + sources_list_dir = f"{tree}/etc/apt/sources.list.d" + os.makedirs(sources_list_dir, exist_ok=True) + + for source_name, source_config in sources_options.items(): + source_file = f"{sources_list_dir}/{source_name}.list" + + with open(source_file, "w", encoding="utf8") as f: + for line in source_config: + f.write(f"{line}\n") + + print(f"Created source file: {source_file}") + + +def main(tree, options): + """Main function for apt config stage""" + + # Get options + config_options = options.get("config", {}) + sources_options = options.get("sources", {}) + preferences = options.get("preferences", {}) + + # Create apt configuration + if config_options: + make_apt_config(tree, config_options) + + # Create sources configuration + if sources_options: + make_apt_sources_config(tree, sources_options) + + # Create preferences file + if preferences: + pref_dir = f"{tree}/etc/apt/preferences.d" + os.makedirs(pref_dir, exist_ok=True) + + for pref_name, pref_rules in preferences.items(): + pref_file = f"{pref_dir}/{pref_name}" + + with open(pref_file, "w", encoding="utf8") as f: + for rule in pref_rules: + f.write(f"{rule}\n") + + print(f"Created preferences file: {pref_file}") + + print("apt configuration 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.config.meta.json b/stages/org.osbuild.apt.config.meta.json new file mode 100644 index 00000000..af73dd90 --- /dev/null +++ b/stages/org.osbuild.apt.config.meta.json @@ -0,0 +1,63 @@ +{ + "name": "org.osbuild.apt.config", + "version": "1", + "description": "Configure apt package manager settings and sources", + "options": { + "type": "object", + "properties": { + "config": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "oneOf": [ + {"type": "string"}, + {"type": "number"}, + {"type": "boolean"} + ] + } + }, + "description": "apt.conf configuration sections and options" + }, + "sources": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Additional sources.list.d files" + }, + "preferences": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Package preference rules for apt" + } + } + }, + "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.deploy b/stages/org.osbuild.ostree.deploy index a0cf93be..4076b53e 100755 --- a/stages/org.osbuild.ostree.deploy +++ b/stages/org.osbuild.ostree.deploy @@ -1,73 +1,116 @@ #!/usr/bin/python3 import os import sys +import subprocess +import json import osbuild.api -from osbuild.util import ostree -from osbuild.util.mnt import MountGuard -def make_fs_identifier(desc): - for key in ["uuid", "label"]: - val = desc.get(key) - if val: - return f"{key.upper()}={val}" - raise ValueError("unknown rootfs type") +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 -def ostree_commit_deploy(tree, inputs, osname, remote, ref, kopts): - if len(inputs) == 0: - if not ref: - raise ValueError("ref should be specified in options") - elif len(inputs) == 1: - if ref: - raise ValueError("Should not specify ref if input was specified") - - # If we have an input then we need to pull_local() from the input - # first before we deploy. - source_repo, commits = ostree.parse_input_commits(inputs["commits"]) - target_repo = f"{tree}/ostree/repo" - for commit, data in commits.items(): - ref = data.get("ref", commit) - ostree.pull_local(source_repo, target_repo, remote, ref) - - if remote: - ref = f"{remote}:{ref}" - - kargs = [f'--karg-append={v}' for v in kopts] - ostree.cli("admin", "deploy", ref, - *kargs, sysroot=tree, os=osname) +def deploy_ostree_branch(repo_path, target_path, branch, ref=None): + """Deploy OSTree branch to target filesystem""" + + # Prepare deploy command + cmd = ["ostree", "admin", "deploy", "--repo", repo_path] + + if ref: + cmd.extend(["--branch", ref]) + else: + cmd.extend(["--branch", branch]) + + cmd.append(target_path) + + print(f"Deploying OSTree branch: {' '.join(cmd)}") + + success, output = run_ostree_command(cmd) + if not success: + return False, None + + # Extract deployment ID from output + deployment_id = output.strip() + print(f"Deployed branch: {deployment_id}") + + return True, deployment_id -def main(tree, inputs, options): - osname = options["osname"] - rootfs = options.get("rootfs") - mounts = options.get("mounts", []) - kopts = options.get("kernel_opts", []) - ref = options.get("ref", "") - remote = options.get("remote") +def create_ostree_layout(target_path): + """Create OSTree filesystem layout""" + + # Create required directories + ostree_dirs = [ + "ostree", + "boot", + "sysroot" + ] + + for dir_name in ostree_dirs: + os.makedirs(os.path.join(target_path, dir_name), exist_ok=True) + + print(f"Created OSTree layout in {target_path}") - # schema should catch the case in which there are more - # than one input but this adds a second layer of security - if len(inputs) > 1: - raise ValueError("Only one input accepted") - if rootfs: - rootfs_id = make_fs_identifier(rootfs) - kopts += [f"root={rootfs_id}"] - - with MountGuard() as mounter: - for mount in mounts: - path = mount.lstrip("/") - path = os.path.join(tree, path) - mounter.mount(path, path) - - ostree_commit_deploy(tree, inputs, osname, remote, ref, kopts) +def main(tree, options): + """Main function for ostree deploy stage""" + + # Get options + repository = options.get("repository", "ostree-repo") + branch = options.get("branch", "debian/atomic") + ref = options.get("ref") + target_subdir = options.get("target_subdir", "sysroot") + + if not branch: + print("No branch specified for OSTree deployment") + return 1 + + # Create target path + target_path = os.path.join(tree, target_subdir) + os.makedirs(target_path, exist_ok=True) + + # Create OSTree layout + create_ostree_layout(target_path) + + # Deploy branch + success, deployment_id = deploy_ostree_branch( + repository, target_path, branch, ref + ) + + if not success: + return 1 + + # Write deployment info + deployment_info = { + "repository": repository, + "branch": branch, + "ref": ref, + "deployment_id": deployment_id, + "target_path": target_subdir + } + + output_file = os.path.join(tree, "ostree-deployment.json") + with open(output_file, "w") as f: + json.dump(deployment_info, f, indent=2) + + print("OSTree deployment completed successfully") + return 0 if __name__ == '__main__': - stage_args = osbuild.api.arguments() - r = main(stage_args["tree"], - stage_args["inputs"], - stage_args["options"]) + args = osbuild.api.arguments() + r = main(args["tree"], args["options"]) sys.exit(r) diff --git a/stages/org.osbuild.ostree.deploy.meta.json b/stages/org.osbuild.ostree.deploy.meta.json index d967f918..645445d5 100644 --- a/stages/org.osbuild.ostree.deploy.meta.json +++ b/stages/org.osbuild.ostree.deploy.meta.json @@ -1,98 +1,49 @@ { - "summary": "Deploy an OStree commit", - "description": [ - "Create an OSTree deployment[1] for a given ref.", - "Since OStree internally uses a hardlink farm to create the file system tree", - "for the deployment from the commit data, the mountpoints for the final image", - "need to be supplied via the `mounts` option, as hardlinks must not span", - "across file systems and therefore the boundaries need to be known when doing", - "the deployment.", - "Creating a deployment also entails generating the Boot Loader Specification", - "entries to boot the system, which contain this the kernel command line.", - "The `rootfs` option can be used to indicate the root file system, containing", - "the sysroot and the deployments. Additional kernel options can be passed via", - "`kernel_opts`.", - "[1] https://ostree.readthedocs.io/en/latest/manual/deployment/" - ], - "capabilities": [ - "CAP_MAC_ADMIN" - ], - "schema_2": { - "options": { - "additionalProperties": false, - "required": [ - "osname" - ], - "properties": { - "mounts": { - "description": "Mount points of the final file system", - "type": "array", - "items": { - "description": "Description of one mount point", - "type": "string" - } - }, - "osname": { - "description": "Name of the stateroot to be used in the deployment", - "type": "string" - }, - "kernel_opts": { - "description": "Additional kernel command line options", - "type": "array", - "items": { - "description": "A single kernel command line option", - "type": "string" - } - }, - "ref": { - "description": "OStree ref to use for the deployment", - "type": "string" - }, - "remote": { - "description": "optional OStree remote to use for the deployment", - "type": "string" - }, - "rootfs": { - "description": "Identifier to locate the root file system", - "type": "object", - "oneOf": [ - { - "required": [ - "uuid" - ] - }, - { - "required": [ - "label" - ] - } - ], - "properties": { - "label": { - "description": "Identify the root file system by label", - "type": "string" - }, - "uuid": { - "description": "Identify the root file system by UUID", - "type": "string" - } - } - } + "name": "org.osbuild.ostree.deploy", + "version": "1", + "description": "Deploy OSTree branch to target filesystem", + "options": { + "type": "object", + "properties": { + "repository": { + "type": "string", + "default": "ostree-repo", + "description": "OSTree repository path" + }, + "branch": { + "type": "string", + "default": "debian/atomic", + "description": "OSTree branch to deploy" + }, + "ref": { + "type": "string", + "description": "Specific OSTree ref to deploy (overrides branch)" + }, + "target_subdir": { + "type": "string", + "default": "sysroot", + "description": "Target subdirectory for deployment" } }, - "inputs": { - "type": "object", - "additionalProperties": false, - "required": [ - "commits" - ], - "properties": { - "commits": { - "type": "object", - "description": "OStree commit to deploy", - "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"] } }