Skip to content

Commit

Permalink
anaconda: introduce container-installer
Browse files Browse the repository at this point in the history
A new image type that will use Anaconda to install a container to a
filesystem unattended. This initial commit introduces the image without
actually making the install unattended, just a bootable ISO.
  • Loading branch information
supakeen committed Dec 21, 2023
1 parent ddf8e9a commit ea8df08
Show file tree
Hide file tree
Showing 3 changed files with 231 additions and 33 deletions.
149 changes: 149 additions & 0 deletions pkg/image/anaconda_container_installer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package image

import (
"fmt"
"math/rand"

"github.com/osbuild/images/internal/common"
"github.com/osbuild/images/pkg/arch"
"github.com/osbuild/images/pkg/artifact"
"github.com/osbuild/images/pkg/container"
"github.com/osbuild/images/pkg/customizations/users"
"github.com/osbuild/images/pkg/disk"
"github.com/osbuild/images/pkg/manifest"
"github.com/osbuild/images/pkg/platform"
"github.com/osbuild/images/pkg/rpmmd"
"github.com/osbuild/images/pkg/runner"
)

type AnacondaContainerInstaller struct {
Base
Platform platform.Platform
ExtraBasePackages rpmmd.PackageSet
Users []users.User
Groups []users.Group

SquashfsCompression string

ISOLabelTempl string
Product string
Variant string
OSName string
Ref string
OSVersion string
Release string

ContainerSource container.SourceSpec

Filename string

AdditionalDracutModules []string
AdditionalAnacondaModules []string
AdditionalDrivers []string
FIPS bool
}

func NewAnacondaContainerInstaller(container container.SourceSpec, ref string) *AnacondaContainerInstaller {
return &AnacondaContainerInstaller{
Base: NewBase("container-installer"),
ContainerSource: container,
Ref: ref,
}
}

func (img *AnacondaContainerInstaller) InstantiateManifest(m *manifest.Manifest,
repos []rpmmd.RepoConfig,
runner runner.Runner,
rng *rand.Rand) (*artifact.Artifact, error) {
buildPipeline := manifest.NewBuild(m, runner, repos, &manifest.BuildOptions{ContainerBuildable: true})
buildPipeline.Checkpoint()

anacondaPipeline := manifest.NewAnacondaInstaller(m,
manifest.AnacondaInstallerTypePayload,
buildPipeline,
img.Platform,
repos,
"kernel",
img.Product,
img.OSVersion)

// This is only built with ELN for now
anacondaPipeline.UseRHELLoraxTemplates = true

anacondaPipeline.ExtraPackages = img.ExtraBasePackages.Include
anacondaPipeline.ExcludePackages = img.ExtraBasePackages.Exclude
anacondaPipeline.ExtraRepos = img.ExtraBasePackages.Repositories
anacondaPipeline.Users = img.Users
anacondaPipeline.Groups = img.Groups
anacondaPipeline.Variant = img.Variant
anacondaPipeline.Biosdevname = (img.Platform.GetArch() == arch.ARCH_X86_64)
anacondaPipeline.Checkpoint()
anacondaPipeline.AdditionalDracutModules = img.AdditionalDracutModules
anacondaPipeline.AdditionalAnacondaModules = img.AdditionalAnacondaModules
if img.FIPS {
anacondaPipeline.AdditionalAnacondaModules = append(
anacondaPipeline.AdditionalAnacondaModules,
"org.fedoraproject.Anaconda.Modules.Security",
)
}
anacondaPipeline.AdditionalDrivers = img.AdditionalDrivers

rootfsPartitionTable := &disk.PartitionTable{
Size: 20 * common.MebiByte,
Partitions: []disk.Partition{
{
Start: 0,
Size: 20 * common.MebiByte,
Payload: &disk.Filesystem{
Type: "vfat",
Mountpoint: "/",
UUID: disk.NewVolIDFromRand(rng),
},
},
},
}

// TODO: replace isoLabelTmpl with more high-level properties
isoLabel := fmt.Sprintf(img.ISOLabelTempl, img.Platform.GetArch())

rootfsImagePipeline := manifest.NewISORootfsImg(buildPipeline, anacondaPipeline)
rootfsImagePipeline.Size = 4 * common.GibiByte

bootTreePipeline := manifest.NewEFIBootTree(m, buildPipeline, img.Product, img.OSVersion)
bootTreePipeline.Platform = img.Platform
bootTreePipeline.UEFIVendor = img.Platform.GetUEFIVendor()
bootTreePipeline.ISOLabel = isoLabel
bootTreePipeline.KernelOpts = []string{fmt.Sprintf("inst.stage2=hd:LABEL=%s", isoLabel), fmt.Sprintf("inst.ks=hd:LABEL=%s:%s", isoLabel, kspath)}
if img.FIPS {
bootTreePipeline.KernelOpts = append(bootTreePipeline.KernelOpts, "fips=1")
}

// enable ISOLinux on x86_64 only
isoLinuxEnabled := img.Platform.GetArch() == arch.ARCH_X86_64

isoTreePipeline := manifest.NewAnacondaInstallerISOTree(buildPipeline, anacondaPipeline, rootfsImagePipeline, bootTreePipeline)
isoTreePipeline.PartitionTable = rootfsPartitionTable
isoTreePipeline.Release = img.Release
isoTreePipeline.OSName = img.OSName
isoTreePipeline.Users = img.Users
isoTreePipeline.Groups = img.Groups

isoTreePipeline.SquashfsCompression = img.SquashfsCompression

// For ostree installers, always put the kickstart file in the root of the ISO
isoTreePipeline.KSPath = kspath
isoTreePipeline.PayloadPath = "/container"

isoTreePipeline.ContainerSource = &img.ContainerSource
isoTreePipeline.ISOLinux = isoLinuxEnabled
if img.FIPS {
isoTreePipeline.KernelOpts = append(isoTreePipeline.KernelOpts, "fips=1")
}

isoPipeline := manifest.NewISO(buildPipeline, isoTreePipeline, isoLabel)
isoPipeline.SetFilename(img.Filename)
isoPipeline.ISOLinux = isoLinuxEnabled
artifact := isoPipeline.Export()

return artifact, nil
}
25 changes: 23 additions & 2 deletions pkg/manifest/anaconda_installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ type AnacondaInstaller struct {
AdditionalDrivers []string

Files []*fsnode.File

// Temporary
UseRHELLoraxTemplates bool
}

func NewAnacondaInstaller(m *Manifest,
Expand Down Expand Up @@ -135,8 +138,18 @@ func (p *AnacondaInstaller) getBuildPackages(Distro) []string {
packages := p.anacondaBootPackageSet()
packages = append(packages,
"rpm",
"lorax-templates-generic",
)

if p.UseRHELLoraxTemplates {
packages = append(packages,
"lorax-templates-rhel",
)
} else {
packages = append(packages,
"lorax-templates-generic",
)
}

return packages
}

Expand Down Expand Up @@ -269,9 +282,17 @@ func (p *AnacondaInstaller) serialize() osbuild.Pipeline {
}

if p.Type == AnacondaInstallerTypePayload {
var LoraxPath string

if p.UseRHELLoraxTemplates {
LoraxPath = "80-rhel/runtime-postinstall.tmpl"
} else {
LoraxPath = "99-generic/runtime-postinstall.tmpl"
}

pipeline.AddStage(osbuild.NewAnacondaStage(osbuild.NewAnacondaStageOptions(p.AdditionalAnacondaModules)))
pipeline.AddStage(osbuild.NewLoraxScriptStage(&osbuild.LoraxScriptStageOptions{
Path: "99-generic/runtime-postinstall.tmpl",
Path: LoraxPath,
BaseArch: p.platform.GetArch().String(),
}))
}
Expand Down
90 changes: 59 additions & 31 deletions pkg/manifest/anaconda_installer_iso_tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type AnacondaInstallerISOTree struct {
// Anaconda pipeline.
KSPath string

// The path where the payload (tarball or ostree repo) will be stored.
// The path where the payload (tarball, ostree repo, or container) will be stored.
PayloadPath string

isoLabel string
Expand All @@ -48,6 +48,8 @@ type AnacondaInstallerISOTree struct {
OSTreeCommitSource *ostree.SourceSpec

ostreeCommitSpec *ostree.CommitSpec
ContainerSource *container.SourceSpec
containerSpec *container.Spec

KernelOpts []string

Expand All @@ -74,21 +76,27 @@ func NewAnacondaInstallerISOTree(buildPipeline *Build, anacondaPipeline *Anacond
return p
}

func (p *AnacondaInstallerISOTree) getOSTreeCommitSources() []ostree.SourceSpec {
if p.OSTreeCommitSource == nil {
func (p *AnacondaInstallerISOTree) getOSTreeCommits() []ostree.CommitSpec {
if p.ostreeCommitSpec == nil {
return nil
}
return []ostree.CommitSpec{*p.ostreeCommitSpec}
}

return []ostree.SourceSpec{
*p.OSTreeCommitSource,
func (p *AnacondaInstallerISOTree) getContainerSpecs() []container.Spec {
if p.containerSpec == nil {
return []container.Spec{}
}
return []container.Spec{*p.containerSpec}
}

func (p *AnacondaInstallerISOTree) getOSTreeCommits() []ostree.CommitSpec {
if p.ostreeCommitSpec == nil {
return nil
func (p *AnacondaInstallerISOTree) getContainerSources() []container.SourceSpec {
if p.ContainerSource == nil {
return []container.SourceSpec{}
}
return []container.SourceSpec{
*p.ContainerSource,
}
return []ostree.CommitSpec{*p.ostreeCommitSpec}
}

func (p *AnacondaInstallerISOTree) getBuildPackages(_ Distro) []string {
Expand All @@ -100,43 +108,29 @@ func (p *AnacondaInstallerISOTree) getBuildPackages(_ Distro) []string {
packages = append(packages, "rpm-ostree")
}

if p.ContainerSource != nil {
packages = append(packages, "skopeo")
}

if p.OSPipeline != nil {
packages = append(packages, "tar")
}

return packages
}

func (p *AnacondaInstallerISOTree) serializeStart(_ []rpmmd.PackageSpec, _ []container.Spec, commits []ostree.CommitSpec) {
if len(commits) == 0 {
// nothing to do
return
}

if len(commits) > 1 {
panic("pipeline supports at most one ostree commit")
func (p *AnacondaInstallerISOTree) serializeStart(_ []rpmmd.PackageSpec, containers []container.Spec, commits []ostree.CommitSpec) {
if p.ostreeCommitSpec != nil || p.containerSpec != nil {
panic("double call to serializeStart()")
}

p.ostreeCommitSpec = &commits[0]
}

func (p *AnacondaInstallerISOTree) serializeEnd() {
p.ostreeCommitSpec = nil
p.containerSpec = nil
}

func (p *AnacondaInstallerISOTree) serialize() osbuild.Pipeline {
// If the anaconda pipeline is a payload then we need one of two payload types
if p.anacondaPipeline.Type == AnacondaInstallerTypePayload {
if p.ostreeCommitSpec == nil && p.OSPipeline == nil {
panic("missing ostree or ospipeline parameters in ISO tree pipeline")
}

// But not both payloads
if p.ostreeCommitSpec != nil && p.OSPipeline != nil {
panic("got both ostree and ospipeline parameters in ISO tree pipeline")
}
}

pipeline := p.Base.serialize()

kernelOpts := []string{}
Expand Down Expand Up @@ -287,6 +281,40 @@ func (p *AnacondaInstallerISOTree) serialize() osbuild.Pipeline {
pipeline.AddStage(osbuild.NewKickstartStage(kickstartOptions))
}

if p.containerSpec != nil {
images := osbuild.NewContainersInputForSources([]container.Spec{*p.containerSpec})
manifests := osbuild.NewFilesInputForManifestLists([]container.Spec{*p.containerSpec})

pipeline.AddStage(osbuild.NewMkdirStage(&osbuild.MkdirStageOptions{
Paths: []osbuild.MkdirStagePath{
{
Path: p.PayloadPath,
},
},
}))

// copy the container in
pipeline.AddStage(osbuild.NewSkopeoStageWithOCI(
p.PayloadPath,
images,
manifests))

kickstartOptions, err := osbuild.NewKickstartStageOptionsWithOSTreeContainer(
p.KSPath,
p.Users,
p.Groups,
path.Join("/run/install/repo", p.PayloadPath),
"oci",
"",
"")

if err != nil {
panic("failed to create kickstartstage options")
}

pipeline.AddStage(osbuild.NewKickstartStage(kickstartOptions))
}

if p.OSPipeline != nil {
// Create the payload tarball
pipeline.AddStage(osbuild.NewTarStage(&osbuild.TarStageOptions{Filename: p.PayloadPath}, p.OSPipeline.name))
Expand Down

0 comments on commit ea8df08

Please sign in to comment.