When creating a Manifest object, collect container SourceSpecs instead of resolved Specs. This is the same way we handle packages: The blueprint option is converted to source specs and attached to the Manifest object during creation. Later, the SourceSpecs will be resolved to full container Specs and used during serialization.
452 lines
13 KiB
Go
452 lines
13 KiB
Go
package fedora
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"strings"
|
|
|
|
"github.com/osbuild/osbuild-composer/internal/blueprint"
|
|
"github.com/osbuild/osbuild-composer/internal/common"
|
|
"github.com/osbuild/osbuild-composer/internal/container"
|
|
"github.com/osbuild/osbuild-composer/internal/distro"
|
|
"github.com/osbuild/osbuild-composer/internal/image"
|
|
"github.com/osbuild/osbuild-composer/internal/manifest"
|
|
"github.com/osbuild/osbuild-composer/internal/osbuild"
|
|
"github.com/osbuild/osbuild-composer/internal/ostree"
|
|
"github.com/osbuild/osbuild-composer/internal/rpmmd"
|
|
"github.com/osbuild/osbuild-composer/internal/users"
|
|
"github.com/osbuild/osbuild-composer/internal/workload"
|
|
)
|
|
|
|
// HELPERS
|
|
|
|
func osCustomizations(
|
|
t *imageType,
|
|
osPackageSet rpmmd.PackageSet,
|
|
containers []container.SourceSpec,
|
|
c *blueprint.Customizations) manifest.OSCustomizations {
|
|
|
|
imageConfig := t.getDefaultImageConfig()
|
|
|
|
osc := manifest.OSCustomizations{}
|
|
|
|
if t.bootable || t.rpmOstree {
|
|
osc.KernelName = c.GetKernel().Name
|
|
|
|
var kernelOptions []string
|
|
if t.kernelOptions != "" {
|
|
kernelOptions = append(kernelOptions, t.kernelOptions)
|
|
}
|
|
if bpKernel := c.GetKernel(); bpKernel.Append != "" {
|
|
kernelOptions = append(kernelOptions, bpKernel.Append)
|
|
}
|
|
osc.KernelOptionsAppend = kernelOptions
|
|
}
|
|
|
|
osc.ExtraBasePackages = osPackageSet.Include
|
|
osc.ExcludeBasePackages = osPackageSet.Exclude
|
|
osc.ExtraBaseRepos = osPackageSet.Repositories
|
|
|
|
osc.Containers = containers
|
|
|
|
osc.GPGKeyFiles = imageConfig.GPGKeyFiles
|
|
if imageConfig.ExcludeDocs != nil {
|
|
osc.ExcludeDocs = *imageConfig.ExcludeDocs
|
|
}
|
|
|
|
if !t.bootISO {
|
|
// don't put users and groups in the payload of an installer
|
|
// add them via kickstart instead
|
|
osc.Groups = users.GroupsFromBP(c.GetGroups())
|
|
osc.Users = users.UsersFromBP(c.GetUsers())
|
|
}
|
|
|
|
osc.EnabledServices = imageConfig.EnabledServices
|
|
osc.DisabledServices = imageConfig.DisabledServices
|
|
if imageConfig.DefaultTarget != nil {
|
|
osc.DefaultTarget = *imageConfig.DefaultTarget
|
|
}
|
|
|
|
if fw := c.GetFirewall(); fw != nil {
|
|
options := osbuild.FirewallStageOptions{
|
|
Ports: fw.Ports,
|
|
}
|
|
|
|
if fw.Services != nil {
|
|
options.EnabledServices = fw.Services.Enabled
|
|
options.DisabledServices = fw.Services.Disabled
|
|
}
|
|
osc.Firewall = &options
|
|
}
|
|
|
|
language, keyboard := c.GetPrimaryLocale()
|
|
if language != nil {
|
|
osc.Language = *language
|
|
} else if imageConfig.Locale != nil {
|
|
osc.Language = *imageConfig.Locale
|
|
}
|
|
if keyboard != nil {
|
|
osc.Keyboard = keyboard
|
|
} else if imageConfig.Keyboard != nil {
|
|
osc.Keyboard = &imageConfig.Keyboard.Keymap
|
|
}
|
|
|
|
if hostname := c.GetHostname(); hostname != nil {
|
|
osc.Hostname = *hostname
|
|
} else {
|
|
osc.Hostname = "localhost.localdomain"
|
|
}
|
|
|
|
timezone, ntpServers := c.GetTimezoneSettings()
|
|
if timezone != nil {
|
|
osc.Timezone = *timezone
|
|
} else if imageConfig.Timezone != nil {
|
|
osc.Timezone = *imageConfig.Timezone
|
|
}
|
|
|
|
if len(ntpServers) > 0 {
|
|
for _, server := range ntpServers {
|
|
osc.NTPServers = append(osc.NTPServers, osbuild.ChronyConfigServer{Hostname: server})
|
|
}
|
|
} else if imageConfig.TimeSynchronization != nil {
|
|
osc.NTPServers = imageConfig.TimeSynchronization.Servers
|
|
}
|
|
|
|
// Relabel the tree, unless the `NoSElinux` flag is explicitly set to `true`
|
|
if imageConfig.NoSElinux == nil || imageConfig.NoSElinux != nil && !*imageConfig.NoSElinux {
|
|
osc.SElinux = "targeted"
|
|
}
|
|
|
|
if oscapConfig := c.GetOpenSCAP(); oscapConfig != nil {
|
|
if t.rpmOstree {
|
|
panic("unexpected oscap options for ostree image type")
|
|
}
|
|
osc.OpenSCAPConfig = osbuild.NewOscapRemediationStageOptions(
|
|
osbuild.OscapConfig{
|
|
Datastream: oscapConfig.DataStream,
|
|
ProfileID: oscapConfig.ProfileID,
|
|
},
|
|
)
|
|
}
|
|
|
|
var err error
|
|
osc.Directories, err = blueprint.DirectoryCustomizationsToFsNodeDirectories(c.GetDirectories())
|
|
if err != nil {
|
|
// In theory this should never happen, because the blueprint directory customizations
|
|
// should have been validated before this point.
|
|
panic(fmt.Sprintf("failed to convert directory customizations to fs node directories: %v", err))
|
|
}
|
|
|
|
osc.Files, err = blueprint.FileCustomizationsToFsNodeFiles(c.GetFiles())
|
|
if err != nil {
|
|
// In theory this should never happen, because the blueprint file customizations
|
|
// should have been validated before this point.
|
|
panic(fmt.Sprintf("failed to convert file customizations to fs node files: %v", err))
|
|
}
|
|
|
|
customRepos, err := c.GetRepositories()
|
|
if err != nil {
|
|
// This shouldn't happen and since the repos
|
|
// should have already been validated
|
|
panic(fmt.Sprintf("failed to get custom repos: %v", err))
|
|
}
|
|
|
|
// This function returns a map of filename and corresponding yum repos
|
|
// and a list of fs node files for the inline gpg keys so we can save
|
|
// them to disk. This step also swaps the inline gpg key with the path
|
|
// to the file in the os file tree
|
|
yumRepos, gpgKeyFiles, err := blueprint.RepoCustomizationsToRepoConfigAndGPGKeyFiles(customRepos)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("failed to convert inline gpgkeys to fs node files: %v", err))
|
|
}
|
|
|
|
// add the gpg key files to the list of files to be added to the tree
|
|
if len(gpgKeyFiles) > 0 {
|
|
osc.Files = append(osc.Files, gpgKeyFiles...)
|
|
}
|
|
|
|
for filename, repos := range yumRepos {
|
|
osc.YUMRepos = append(osc.YUMRepos, osbuild.NewYumReposStageOptions(filename, repos))
|
|
}
|
|
|
|
osc.ShellInit = imageConfig.ShellInit
|
|
|
|
osc.Grub2Config = imageConfig.Grub2Config
|
|
osc.Sysconfig = imageConfig.Sysconfig
|
|
osc.SystemdLogind = imageConfig.SystemdLogind
|
|
osc.CloudInit = imageConfig.CloudInit
|
|
osc.Modprobe = imageConfig.Modprobe
|
|
osc.DracutConf = imageConfig.DracutConf
|
|
osc.SystemdUnit = imageConfig.SystemdUnit
|
|
osc.Authselect = imageConfig.Authselect
|
|
osc.SELinuxConfig = imageConfig.SELinuxConfig
|
|
osc.Tuned = imageConfig.Tuned
|
|
osc.Tmpfilesd = imageConfig.Tmpfilesd
|
|
osc.PamLimitsConf = imageConfig.PamLimitsConf
|
|
osc.Sysctld = imageConfig.Sysctld
|
|
osc.DNFConfig = imageConfig.DNFConfig
|
|
osc.SshdConfig = imageConfig.SshdConfig
|
|
osc.AuthConfig = imageConfig.Authconfig
|
|
osc.PwQuality = imageConfig.PwQuality
|
|
|
|
return osc
|
|
}
|
|
|
|
// IMAGES
|
|
|
|
func liveImage(workload workload.Workload,
|
|
t *imageType,
|
|
customizations *blueprint.Customizations,
|
|
options distro.ImageOptions,
|
|
packageSets map[string]rpmmd.PackageSet,
|
|
containers []container.SourceSpec,
|
|
rng *rand.Rand) (image.ImageKind, error) {
|
|
|
|
img := image.NewLiveImage()
|
|
img.Platform = t.platform
|
|
img.OSCustomizations = osCustomizations(t, packageSets[osPkgsKey], containers, customizations)
|
|
img.Environment = t.environment
|
|
img.Workload = workload
|
|
// TODO: move generation into LiveImage
|
|
pt, err := t.getPartitionTable(customizations.GetFilesystems(), options, rng)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
img.PartitionTable = pt
|
|
|
|
img.Filename = t.Filename()
|
|
|
|
return img, nil
|
|
}
|
|
|
|
func containerImage(workload workload.Workload,
|
|
t *imageType,
|
|
c *blueprint.Customizations,
|
|
options distro.ImageOptions,
|
|
packageSets map[string]rpmmd.PackageSet,
|
|
containers []container.SourceSpec,
|
|
rng *rand.Rand) (image.ImageKind, error) {
|
|
img := image.NewBaseContainer()
|
|
|
|
img.Platform = t.platform
|
|
img.OSCustomizations = osCustomizations(t, packageSets[osPkgsKey], containers, c)
|
|
img.Environment = t.environment
|
|
img.Workload = workload
|
|
|
|
img.Filename = t.Filename()
|
|
|
|
return img, nil
|
|
}
|
|
|
|
func imageInstallerImage(workload workload.Workload,
|
|
t *imageType,
|
|
customizations *blueprint.Customizations,
|
|
options distro.ImageOptions,
|
|
packageSets map[string]rpmmd.PackageSet,
|
|
containers []container.SourceSpec,
|
|
rng *rand.Rand) (image.ImageKind, error) {
|
|
|
|
img := image.NewImageInstaller()
|
|
|
|
// Enable anaconda-webui for Fedora > 38
|
|
distro := t.Arch().Distro()
|
|
if strings.HasPrefix(distro.Name(), "fedora") && !common.VersionLessThan(distro.Releasever(), "38") {
|
|
img.AdditionalAnacondaModules = []string{
|
|
"org.fedoraproject.Anaconda.Modules.Security",
|
|
"org.fedoraproject.Anaconda.Modules.Timezone",
|
|
"org.fedoraproject.Anaconda.Modules.Localization",
|
|
}
|
|
img.AdditionalKernelOpts = []string{"inst.webui", "inst.webui.remote"}
|
|
}
|
|
img.AdditionalAnacondaModules = append(img.AdditionalAnacondaModules, "org.fedoraproject.Anaconda.Modules.Users")
|
|
|
|
img.Platform = t.platform
|
|
img.Workload = workload
|
|
img.OSCustomizations = osCustomizations(t, packageSets[osPkgsKey], containers, customizations)
|
|
img.ExtraBasePackages = packageSets[installerPkgsKey]
|
|
img.Users = users.UsersFromBP(customizations.GetUsers())
|
|
img.Groups = users.GroupsFromBP(customizations.GetGroups())
|
|
|
|
img.SquashfsCompression = "lz4"
|
|
|
|
d := t.arch.distro
|
|
|
|
img.ISOLabelTempl = d.isolabelTmpl
|
|
img.Product = d.product
|
|
img.OSName = "fedora"
|
|
img.OSVersion = d.osVersion
|
|
img.Release = fmt.Sprintf("%s %s", d.product, d.osVersion)
|
|
|
|
img.Filename = t.Filename()
|
|
|
|
return img, nil
|
|
}
|
|
|
|
func iotCommitImage(workload workload.Workload,
|
|
t *imageType,
|
|
customizations *blueprint.Customizations,
|
|
options distro.ImageOptions,
|
|
packageSets map[string]rpmmd.PackageSet,
|
|
containers []container.SourceSpec,
|
|
rng *rand.Rand) (image.ImageKind, error) {
|
|
|
|
img := image.NewOSTreeArchive(options.OSTree.ImageRef)
|
|
|
|
img.Platform = t.platform
|
|
img.OSCustomizations = osCustomizations(t, packageSets[osPkgsKey], containers, customizations)
|
|
img.Environment = t.environment
|
|
img.Workload = workload
|
|
|
|
if options.OSTree.FetchChecksum != "" && options.OSTree.URL != "" {
|
|
img.OSTreeParent = &ostree.CommitSpec{
|
|
Checksum: options.OSTree.FetchChecksum,
|
|
URL: options.OSTree.URL,
|
|
ContentURL: options.OSTree.ContentURL,
|
|
}
|
|
}
|
|
|
|
img.OSVersion = t.arch.distro.osVersion
|
|
|
|
img.Filename = t.Filename()
|
|
|
|
return img, nil
|
|
}
|
|
|
|
func iotContainerImage(workload workload.Workload,
|
|
t *imageType,
|
|
customizations *blueprint.Customizations,
|
|
options distro.ImageOptions,
|
|
packageSets map[string]rpmmd.PackageSet,
|
|
containers []container.SourceSpec,
|
|
rng *rand.Rand) (image.ImageKind, error) {
|
|
|
|
img := image.NewOSTreeContainer(options.OSTree.ImageRef)
|
|
|
|
img.Platform = t.platform
|
|
img.OSCustomizations = osCustomizations(t, packageSets[osPkgsKey], containers, customizations)
|
|
img.ContainerLanguage = img.OSCustomizations.Language
|
|
img.Environment = t.environment
|
|
img.Workload = workload
|
|
|
|
if options.OSTree.FetchChecksum != "" && options.OSTree.URL != "" {
|
|
img.OSTreeParent = &ostree.CommitSpec{
|
|
Checksum: options.OSTree.FetchChecksum,
|
|
URL: options.OSTree.URL,
|
|
ContentURL: options.OSTree.ContentURL,
|
|
}
|
|
}
|
|
|
|
img.OSVersion = t.arch.distro.osVersion
|
|
|
|
img.ExtraContainerPackages = packageSets[containerPkgsKey]
|
|
|
|
img.Filename = t.Filename()
|
|
|
|
return img, nil
|
|
}
|
|
|
|
func iotInstallerImage(workload workload.Workload,
|
|
t *imageType,
|
|
customizations *blueprint.Customizations,
|
|
options distro.ImageOptions,
|
|
packageSets map[string]rpmmd.PackageSet,
|
|
containers []container.SourceSpec,
|
|
rng *rand.Rand) (image.ImageKind, error) {
|
|
|
|
d := t.arch.distro
|
|
|
|
commit := ostree.CommitSpec{
|
|
Ref: options.OSTree.ImageRef,
|
|
URL: options.OSTree.URL,
|
|
ContentURL: options.OSTree.ContentURL,
|
|
Checksum: options.OSTree.FetchChecksum,
|
|
}
|
|
img := image.NewOSTreeInstaller(commit)
|
|
|
|
img.Platform = t.platform
|
|
img.ExtraBasePackages = packageSets[installerPkgsKey]
|
|
img.Users = users.UsersFromBP(customizations.GetUsers())
|
|
img.Groups = users.GroupsFromBP(customizations.GetGroups())
|
|
img.AdditionalAnacondaModules = []string{
|
|
"org.fedoraproject.Anaconda.Modules.Timezone",
|
|
"org.fedoraproject.Anaconda.Modules.Localization",
|
|
"org.fedoraproject.Anaconda.Modules.Users",
|
|
}
|
|
|
|
img.SquashfsCompression = "lz4"
|
|
|
|
img.ISOLabelTempl = d.isolabelTmpl
|
|
img.Product = d.product
|
|
img.Variant = "IoT"
|
|
img.OSName = "fedora"
|
|
img.OSVersion = d.osVersion
|
|
img.Release = fmt.Sprintf("%s %s", d.product, d.osVersion)
|
|
|
|
img.Filename = t.Filename()
|
|
|
|
return img, nil
|
|
}
|
|
|
|
func iotRawImage(workload workload.Workload,
|
|
t *imageType,
|
|
customizations *blueprint.Customizations,
|
|
options distro.ImageOptions,
|
|
packageSets map[string]rpmmd.PackageSet,
|
|
containers []container.SourceSpec,
|
|
rng *rand.Rand) (image.ImageKind, error) {
|
|
|
|
commit := ostree.CommitSpec{
|
|
Ref: options.OSTree.ImageRef,
|
|
URL: options.OSTree.URL,
|
|
ContentURL: options.OSTree.ContentURL,
|
|
Checksum: options.OSTree.FetchChecksum,
|
|
}
|
|
img := image.NewOSTreeRawImage(commit)
|
|
|
|
// Set sysroot read-only only for Fedora 37+
|
|
distro := t.Arch().Distro()
|
|
if strings.HasPrefix(distro.Name(), "fedora") && !common.VersionLessThan(distro.Releasever(), "37") {
|
|
img.SysrootReadOnly = true
|
|
}
|
|
|
|
img.Users = users.UsersFromBP(customizations.GetUsers())
|
|
img.Groups = users.GroupsFromBP(customizations.GetGroups())
|
|
|
|
var err error
|
|
img.Directories, err = blueprint.DirectoryCustomizationsToFsNodeDirectories(customizations.GetDirectories())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
img.Files, err = blueprint.FileCustomizationsToFsNodeFiles(customizations.GetFiles())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// "rw" kernel option is required when /sysroot is mounted read-only to
|
|
// keep stateful parts of the filesystem writeable (/var/ and /etc)
|
|
img.KernelOptionsAppend = []string{"modprobe.blacklist=vc4", "rw"}
|
|
img.Keyboard = "us"
|
|
img.Locale = "C.UTF-8"
|
|
|
|
img.Platform = t.platform
|
|
img.Workload = workload
|
|
|
|
img.Remote = ostree.Remote{
|
|
Name: "fedora-iot",
|
|
URL: "https://ostree.fedoraproject.org/iot",
|
|
ContentURL: "mirrorlist=https://ostree.fedoraproject.org/iot/mirrorlist",
|
|
GPGKeyPaths: []string{"/etc/pki/rpm-gpg/"},
|
|
}
|
|
img.OSName = "fedora-iot"
|
|
|
|
// TODO: move generation into LiveImage
|
|
pt, err := t.getPartitionTable(customizations.GetFilesystems(), options, rng)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
img.PartitionTable = pt
|
|
|
|
img.Filename = t.Filename()
|
|
|
|
return img, nil
|
|
}
|