Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for arbitrary kickstart file injection into ISOs (HMS-3879) #438

Merged
merged 4 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,38 @@ Example:

```

### Anaconda ISO (installer) options (`installer`, mapping)

Users can include kickstart file content that will be added to an ISO build to configure the installation process.
Since multi-line strings are difficult to write and read in json, it's easier to use the toml format when adding kickstart contents:

```toml
[customizations.installer.kickstart]
contents = """
text --non-interactive
zerombr
clearpart --all --initlabel --disklabel=gpt
autopart --noswap --type=lvm
network --bootproto=dhcp --device=link --activate --onboot=on
"""
```

The equivalent in json would be:
```json
{
"customizations": {
"installer": {
"kickstart": {
"contents": "text --non-interactive\nzerombr\nclearpart --all --initlabel --disklabel=gpt\nautopart --noswap --type=lvm\nnetwork --bootproto=dhcp --device=link --activate --onboot=on"
}
}
}
}
```

Note that bootc-image-builder will automatically add the command that installs the container image (`ostreecontainer ...`), so this line or any line that conflicts with it should not be included. See the relevant [Kickstart documentation](https://pykickstart.readthedocs.io/en/latest/kickstart-docs.html#ostreecontainer) for more information.
achilleas-k marked this conversation as resolved.
Show resolved Hide resolved
No other kickstart commands are added by bootc-image-builder in this case, so it is the responsibility of the user to provide all other commands (for example, for partitioning, network, language, etc).

## Building

To build the container locally you can run
Expand Down
18 changes: 9 additions & 9 deletions bib/cmd/bootc-image-builder/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/osbuild/images/pkg/disk"
"github.com/osbuild/images/pkg/image"
"github.com/osbuild/images/pkg/manifest"
"github.com/osbuild/images/pkg/osbuild"
"github.com/osbuild/images/pkg/platform"
"github.com/osbuild/images/pkg/policies"
"github.com/osbuild/images/pkg/rpmmd"
Expand Down Expand Up @@ -228,26 +229,25 @@ func manifestForISO(c *ManifestConfig, rng *rand.Rand) (*manifest.Manifest, erro
customizations = c.Config.Customizations
}

img.Kickstart = &kickstart.Options{
Path: "/osbuild.ks",
Users: users.UsersFromBP(customizations.GetUsers()),
Groups: users.GroupsFromBP(customizations.GetGroups()),
NetworkOnBoot: true,
OSTree: &kickstart.OSTree{
OSName: "default",
},
img.Kickstart, err = kickstart.New(customizations)
if err != nil {
return nil, err
}
img.Kickstart.Path = osbuild.KickstartPathOSBuild
if kopts := customizations.GetKernel(); kopts != nil && kopts.Append != "" {
img.Kickstart.KernelOptionsAppend = append(img.Kickstart.KernelOptionsAppend, kopts.Append)
}

img.Kickstart.NetworkOnBoot = true
// XXX: this should really be done by images, the consumer should not
// need to know these details. so once images is fixed drop it here
// again.
if len(img.Kickstart.Users) > 0 || len(img.Kickstart.Groups) > 0 {
img.AdditionalAnacondaModules = append(img.AdditionalAnacondaModules, "org.fedoraproject.Anaconda.Modules.Users")
}

img.Kickstart.OSTree = &kickstart.OSTree{
OSName: "default",
}
// use lorax-templates-rhel if the source distro is not Fedora with the exception of Fedora ELN
img.UseRHELLoraxTemplates =
c.SourceInfo.OSRelease.ID != "fedora" || c.SourceInfo.OSRelease.VersionID == "eln"
Expand Down
62 changes: 60 additions & 2 deletions test/test_manifest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import base64
import hashlib
import json
import platform
import pathlib
import platform
import subprocess
import textwrap

Expand All @@ -13,7 +15,8 @@
if not testutil.can_start_rootful_containers():
pytest.skip("tests require to be able to run rootful containers (try: sudo)", allow_module_level=True)

from containerbuild import build_container_fixture, make_container # noqa: F401
from containerbuild import build_container_fixture # noqa: F401
from containerbuild import make_container
from testcases import gen_testcases


Expand Down Expand Up @@ -49,6 +52,26 @@ def test_manifest_smoke(build_container, testcase_ref):
assert int(disk_size) == 10 * 1024 * 1024 * 1024


@pytest.mark.parametrize("testcase_ref", gen_testcases("anaconda-iso"))
def test_iso_manifest_smoke(build_container, testcase_ref):
# testcases_ref has the form "container_url,img_type1+img_type2,arch"
container_ref = testcase_ref.split(",")[0]

output = subprocess.check_output([
"podman", "run", "--rm",
"--privileged",
"--security-opt", "label=type:unconfined_t",
('--entrypoint=["/usr/bin/bootc-image-builder", "manifest", "--rootfs", "ext4", '
f'"--type=anaconda-iso", "{container_ref}"]'),
build_container,
])
manifest = json.loads(output)
# just some basic validation
expected_pipeline_names = ["build", "anaconda-tree", "rootfs-image", "efiboot-tree", "bootiso-tree", "bootiso"]
assert manifest["version"] == "2"
assert [pipeline["name"] for pipeline in manifest["pipelines"]] == expected_pipeline_names


@pytest.mark.parametrize("testcase_ref", gen_testcases("manifest"))
def test_manifest_disksize(tmp_path, build_container, testcase_ref):
# create derrived container with 6G silly file to ensure that
Expand Down Expand Up @@ -227,6 +250,41 @@ def test_manifest_user_customizations_toml(tmp_path, build_container):
}


def test_manifest_installer_customizations(tmp_path, build_container):
container_ref = "quay.io/centos-bootc/centos-bootc:stream9"

config_toml_path = tmp_path / "config.toml"
config_toml_path.write_text(textwrap.dedent("""\
[customizations.installer.kickstart]
contents = \"\"\"
autopart --type=lvm
\"\"\"
"""))
output = subprocess.check_output([
"podman", "run", "--rm",
"--privileged",
"-v", "/var/lib/containers/storage:/var/lib/containers/storage",
"-v", f"{config_toml_path}:/config.toml",
"--security-opt", "label=type:unconfined_t",
f'--entrypoint=["/usr/bin/bootc-image-builder", "manifest", "--type=anaconda-iso", "{container_ref}"]',
build_container,
])
manifest = json.loads(output)

# expected values for the following inline file contents
achilleas-k marked this conversation as resolved.
Show resolved Hide resolved
ks_content = textwrap.dedent("""\
%include /run/install/repo/osbuild-base.ks
autopart --type=lvm
""").encode("utf8")
expected_data = base64.b64encode(ks_content).decode()
expected_content_hash = hashlib.sha256(ks_content).hexdigest()
expected_content_id = f"sha256:{expected_content_hash}" # hash with algo prefix

# check the inline source for the custom kickstart contents
assert expected_content_id in manifest["sources"]["org.osbuild.inline"]["items"]
achilleas-k marked this conversation as resolved.
Show resolved Hide resolved
assert manifest["sources"]["org.osbuild.inline"]["items"][expected_content_id]["data"] == expected_data


def test_mount_ostree_error(tmpdir_factory, build_container):
# no need to parameterize this test, toml is the same for all containers
container_ref = "quay.io/centos-bootc/centos-bootc:stream9"
Expand Down
Loading