debian-forge-composer/internal/manifest/os.go
Achilleas Koutsou b39d802155 manifest: add support for selecting grub2.legacy
Older OS versions (RHEL 7) with older versions of grub2 don't support
BLS entries.  Setting NoBLS to true configures the bootloader with
traditional menu entries through the grub2.legacy osbuild stage.  This
requires specifying extra information for the OS to the pipeline:
version, product, and nick.
2023-01-25 20:37:12 +01:00

660 lines
20 KiB
Go

package manifest
import (
"fmt"
"path/filepath"
"strings"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/osbuild/osbuild-composer/internal/container"
"github.com/osbuild/osbuild-composer/internal/disk"
"github.com/osbuild/osbuild-composer/internal/distro"
"github.com/osbuild/osbuild-composer/internal/environment"
"github.com/osbuild/osbuild-composer/internal/osbuild"
"github.com/osbuild/osbuild-composer/internal/ostree"
"github.com/osbuild/osbuild-composer/internal/platform"
"github.com/osbuild/osbuild-composer/internal/rpmmd"
"github.com/osbuild/osbuild-composer/internal/users"
"github.com/osbuild/osbuild-composer/internal/workload"
)
// OSCustomizations encapsulates all configuration applied to the base
// operating system independently of where and how it is integrated and what
// workload it is running.
// TODO: move out kernel/bootloader/cloud-init/... to other
//
// abstractions, this should ideally only contain things that
// can always be applied.
type OSCustomizations struct {
// Packages to install in addition to the ones required by the
// pipeline.
ExtraBasePackages []string
// Packages to exclude from the base package set. This is useful in
// case of weak dependencies, comps groups, or where multiple packages
// can satisfy a dependency. Must not conflict with the included base
// package set.
ExcludeBasePackages []string
// Additional repos to install the base packages from.
ExtraBaseRepos []rpmmd.RepoConfig
// Containers to embed in the image
Containers []container.Spec
// KernelName indicates that a kernel is installed, and names the kernel
// package.
KernelName string
// KernelOptionsAppend are appended to the kernel commandline
KernelOptionsAppend []string
// KernelOptionsBootloader controls whether kernel command line options
// should be specified in the bootloader configuration. Otherwise they are
// specified in /etc/kernel/cmdline (default).
// This should only be used for RHEL 8 and CentOS 8 images that use grub
// (non s390x). Newer releases (9+) should keep this disabled.
KernelOptionsBootloader bool
GPGKeyFiles []string
Language string
Keyboard *string
X11KeymapLayouts []string
Hostname string
Timezone string
EnabledServices []string
DisabledServices []string
DefaultTarget string
// SELinux policy, when set it enables the labeling of the tree with the
// selected profile
SElinux string
// Do not install documentation
ExcludeDocs bool
Groups []users.Group
Users []users.User
// TODO: drop osbuild types from the API
Firewall *osbuild.FirewallStageOptions
Grub2Config *osbuild.GRUB2Config
Sysconfig []*osbuild.SysconfigStageOptions
SystemdLogind []*osbuild.SystemdLogindStageOptions
CloudInit []*osbuild.CloudInitStageOptions
Modprobe []*osbuild.ModprobeStageOptions
DracutConf []*osbuild.DracutConfStageOptions
SystemdUnit []*osbuild.SystemdUnitStageOptions
Authselect *osbuild.AuthselectStageOptions
SELinuxConfig *osbuild.SELinuxConfigStageOptions
Tuned *osbuild.TunedStageOptions
Tmpfilesd []*osbuild.TmpfilesdStageOptions
PamLimitsConf []*osbuild.PamLimitsConfStageOptions
Sysctld []*osbuild.SysctldStageOptions
DNFConfig []*osbuild.DNFConfigStageOptions
DNFAutomaticConfig *osbuild.DNFAutomaticConfigStageOptions
YUMRepos []*osbuild.YumReposStageOptions
SshdConfig *osbuild.SshdConfigStageOptions
GCPGuestAgentConfig *osbuild.GcpGuestAgentConfigOptions
AuthConfig *osbuild.AuthconfigStageOptions
PwQuality *osbuild.PwqualityConfStageOptions
OpenSCAPConfig *osbuild.OscapRemediationStageOptions
NTPServers []osbuild.ChronyConfigServer
WAAgentConfig *osbuild.WAAgentConfStageOptions
UdevRules *osbuild.UdevRulesStageOptions
LeapSecTZ *string
FactAPIType string
Subscription *distro.SubscriptionImageOptions
RHSMConfig map[distro.RHSMSubscriptionStatus]*osbuild.RHSMStageOptions
}
// OS represents the filesystem tree of the target image. This roughly
// corresponds to the root filesystem once an instance of the image is running.
type OS struct {
Base
// Customizations to apply to the base OS
OSCustomizations
// Environment the system will run in
Environment environment.Environment
// Workload to install on top of the base system
Workload workload.Workload
// Ref of ostree commit, if empty the tree cannot be in an ostree commit
OSTreeRef string
// OSTree parent spec, if nil the new commit (if applicable) will have no parent
OSTreeParent *ostree.CommitSpec
// Partition table, if nil the tree cannot be put on a partitioned disk
PartitionTable *disk.PartitionTable
repos []rpmmd.RepoConfig
packageSpecs []rpmmd.PackageSpec
platform platform.Platform
kernelVer string
// NoBLS configures the image bootloader with traditional menu entries
// instead of BLS. Required for legacy systems like RHEL 7.
NoBLS bool
OSProduct string
OSVersion string
OSNick string
}
// NewOS creates a new OS pipeline. build is the build pipeline to use for
// building the OS pipeline. platform is the target platform for the final
// image. repos are the repositories to install RPMs from.
func NewOS(m *Manifest,
buildPipeline *Build,
platform platform.Platform,
repos []rpmmd.RepoConfig) *OS {
p := &OS{
Base: NewBase(m, "os", buildPipeline),
repos: repos,
platform: platform,
}
buildPipeline.addDependent(p)
m.addPipeline(p)
return p
}
func (p *OS) getPackageSetChain() []rpmmd.PackageSet {
packages := p.platform.GetPackages()
if p.KernelName != "" {
packages = append(packages, p.KernelName)
}
// If we have a logical volume we need to include the lvm2 package.
// OSTree-based images (commit and container) aren't bootable images and
// don't have partition tables.
if p.PartitionTable != nil && p.OSTreeRef == "" {
packages = append(packages, p.PartitionTable.GetBuildPackages()...)
}
if p.Environment != nil {
packages = append(packages, p.Environment.GetPackages()...)
}
if len(p.NTPServers) > 0 {
packages = append(packages, "chrony")
}
if p.SElinux != "" {
packages = append(packages, fmt.Sprintf("selinux-policy-%s", p.SElinux))
}
if p.OpenSCAPConfig != nil {
packages = append(packages, "openscap-scanner", "scap-security-guide")
}
chain := []rpmmd.PackageSet{
{
Include: append(packages, p.ExtraBasePackages...),
Exclude: p.ExcludeBasePackages,
Repositories: append(p.repos, p.ExtraBaseRepos...),
},
}
if p.Workload != nil {
workloadPackages := p.Workload.GetPackages()
if len(workloadPackages) > 0 {
chain = append(chain, rpmmd.PackageSet{
Include: workloadPackages,
Repositories: append(p.repos, p.Workload.GetRepos()...),
})
}
}
return chain
}
func (p *OS) getBuildPackages() []string {
packages := p.platform.GetBuildPackages()
if p.PartitionTable != nil {
packages = append(packages, p.PartitionTable.GetBuildPackages()...)
}
packages = append(packages, "rpm")
if p.OSTreeRef != "" {
packages = append(packages, "rpm-ostree")
}
if p.SElinux != "" {
packages = append(packages, "policycoreutils", fmt.Sprintf("selinux-policy-%s", p.SElinux))
}
if len(p.CloudInit) > 0 {
packages = append(packages, "python3-pyyaml")
}
if len(p.DNFConfig) > 0 || len(p.RHSMConfig) > 0 {
packages = append(packages, "python3-iniparse")
}
if len(p.OSCustomizations.Containers) > 0 {
if p.OSTreeRef != "" {
packages = append(packages, "python3-toml")
}
packages = append(packages, "skopeo")
}
return packages
}
func (p *OS) getOSTreeCommits() []ostree.CommitSpec {
if p.OSTreeParent == nil {
return nil
}
return []ostree.CommitSpec{*p.OSTreeParent}
}
func (p *OS) getPackageSpecs() []rpmmd.PackageSpec {
return p.packageSpecs
}
func (p *OS) getContainerSpecs() []container.Spec {
return p.Containers
}
func (p *OS) serializeStart(packages []rpmmd.PackageSpec) {
if len(p.packageSpecs) > 0 {
panic("double call to serializeStart()")
}
p.packageSpecs = packages
if p.KernelName != "" {
p.kernelVer = rpmmd.GetVerStrFromPackageSpecListPanic(p.packageSpecs, p.KernelName)
}
}
func (p *OS) serializeEnd() {
if len(p.packageSpecs) == 0 {
panic("serializeEnd() call when serialization not in progress")
}
p.kernelVer = ""
p.packageSpecs = nil
}
func (p *OS) serialize() osbuild.Pipeline {
if len(p.packageSpecs) == 0 {
panic("serialization not started")
}
pipeline := p.Base.serialize()
if p.OSTreeRef != "" && p.OSTreeParent != nil {
pipeline.AddStage(osbuild.NewOSTreePasswdStage("org.osbuild.source", p.OSTreeParent.Checksum))
}
rpmOptions := osbuild.NewRPMStageOptions(p.repos)
if p.ExcludeDocs {
if rpmOptions.Exclude == nil {
rpmOptions.Exclude = &osbuild.Exclude{}
}
rpmOptions.Exclude.Docs = true
}
rpmOptions.GPGKeysFromTree = p.GPGKeyFiles
if p.OSTreeRef != "" {
rpmOptions.OSTreeBooted = common.ToPtr(true)
rpmOptions.DBPath = "/usr/share/rpm"
}
pipeline.AddStage(osbuild.NewRPMStage(rpmOptions, osbuild.NewRpmStageSourceFilesInputs(p.packageSpecs)))
if !p.NoBLS {
// If the /boot is on a separate partition, the prefix for the BLS stage must be ""
if p.PartitionTable == nil || p.PartitionTable.FindMountable("/boot") == nil {
pipeline.AddStage(osbuild.NewFixBLSStage(&osbuild.FixBLSStageOptions{}))
} else {
pipeline.AddStage(osbuild.NewFixBLSStage(&osbuild.FixBLSStageOptions{Prefix: common.ToPtr("")}))
}
}
if len(p.Containers) > 0 {
images := osbuild.NewContainersInputForSources(p.Containers)
var storagePath string
// OSTree commits do not include data in `/var` since that is tied to the
// deployment, rather than the commit. Therefore the containers need to be
// stored in a different location, like `/usr/share`, and the container
// storage engine configured accordingly.
if p.OSTreeRef != "" {
storagePath = "/usr/share/containers/storage"
storageConf := "/etc/containers/storage.conf"
containerStoreOpts := osbuild.NewContainerStorageOptions(storageConf, storagePath)
pipeline.AddStage(osbuild.NewContainersStorageConfStage(containerStoreOpts))
}
skopeo := osbuild.NewSkopeoStage(images, storagePath)
pipeline.AddStage(skopeo)
}
pipeline.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: p.Language}))
if p.Keyboard != nil {
keymapOptions := &osbuild.KeymapStageOptions{Keymap: *p.Keyboard}
if len(p.X11KeymapLayouts) > 0 {
keymapOptions.X11Keymap = &osbuild.X11KeymapOptions{Layouts: p.X11KeymapLayouts}
}
pipeline.AddStage(osbuild.NewKeymapStage(keymapOptions))
}
if p.Hostname != "" {
pipeline.AddStage(osbuild.NewHostnameStage(&osbuild.HostnameStageOptions{Hostname: p.Hostname}))
}
pipeline.AddStage(osbuild.NewTimezoneStage(&osbuild.TimezoneStageOptions{Zone: p.Timezone}))
if len(p.NTPServers) > 0 {
chronyOptions := &osbuild.ChronyStageOptions{Servers: p.NTPServers}
if p.LeapSecTZ != nil {
chronyOptions.LeapsecTz = p.LeapSecTZ
}
pipeline.AddStage(osbuild.NewChronyStage(chronyOptions))
}
if len(p.Groups) > 0 {
pipeline.AddStage(osbuild.GenGroupsStage(p.Groups))
}
if len(p.Users) > 0 {
if p.OSTreeRef != "" {
// for ostree, writing the key during user creation is
// redundant and can cause issues so create users without keys
// and write them on first boot
usersStageSansKeys, err := osbuild.GenUsersStage(p.Users, true)
if err != nil {
// TODO: move encryption into weldr
panic("password encryption failed")
}
pipeline.AddStage(usersStageSansKeys)
pipeline.AddStage(osbuild.NewFirstBootStage(usersFirstBootOptions(p.Users)))
} else {
usersStage, err := osbuild.GenUsersStage(p.Users, false)
if err != nil {
// TODO: move encryption into weldr
panic("password encryption failed")
}
pipeline.AddStage(usersStage)
}
}
enabledServices := []string{}
disabledServices := []string{}
enabledServices = append(enabledServices, p.EnabledServices...)
disabledServices = append(disabledServices, p.DisabledServices...)
if p.Environment != nil {
enabledServices = append(enabledServices, p.Environment.GetServices()...)
}
if p.Workload != nil {
enabledServices = append(enabledServices, p.Workload.GetServices()...)
disabledServices = append(disabledServices, p.Workload.GetDisabledServices()...)
}
if len(enabledServices) != 0 ||
len(disabledServices) != 0 || p.DefaultTarget != "" {
pipeline.AddStage(osbuild.NewSystemdStage(&osbuild.SystemdStageOptions{
EnabledServices: enabledServices,
DisabledServices: disabledServices,
DefaultTarget: p.DefaultTarget,
}))
}
if p.Firewall != nil {
pipeline.AddStage(osbuild.NewFirewallStage(p.Firewall))
}
for _, sysconfigConfig := range p.Sysconfig {
pipeline.AddStage(osbuild.NewSysconfigStage(sysconfigConfig))
}
for _, systemdLogindConfig := range p.SystemdLogind {
pipeline.AddStage(osbuild.NewSystemdLogindStage(systemdLogindConfig))
}
for _, cloudInitConfig := range p.CloudInit {
pipeline.AddStage(osbuild.NewCloudInitStage(cloudInitConfig))
}
for _, modprobeConfig := range p.Modprobe {
pipeline.AddStage(osbuild.NewModprobeStage(modprobeConfig))
}
for _, dracutConfConfig := range p.DracutConf {
pipeline.AddStage(osbuild.NewDracutConfStage(dracutConfConfig))
}
for _, systemdUnitConfig := range p.SystemdUnit {
pipeline.AddStage(osbuild.NewSystemdUnitStage(systemdUnitConfig))
}
if p.Authselect != nil {
pipeline.AddStage(osbuild.NewAuthselectStage(p.Authselect))
}
if p.SELinuxConfig != nil {
pipeline.AddStage(osbuild.NewSELinuxConfigStage(p.SELinuxConfig))
}
if p.Tuned != nil {
pipeline.AddStage(osbuild.NewTunedStage(p.Tuned))
}
for _, tmpfilesdConfig := range p.Tmpfilesd {
pipeline.AddStage(osbuild.NewTmpfilesdStage(tmpfilesdConfig))
}
for _, pamLimitsConfConfig := range p.PamLimitsConf {
pipeline.AddStage(osbuild.NewPamLimitsConfStage(pamLimitsConfConfig))
}
for _, sysctldConfig := range p.Sysctld {
pipeline.AddStage(osbuild.NewSysctldStage(sysctldConfig))
}
for _, dnfConfig := range p.DNFConfig {
pipeline.AddStage(osbuild.NewDNFConfigStage(dnfConfig))
}
if p.DNFAutomaticConfig != nil {
pipeline.AddStage(osbuild.NewDNFAutomaticConfigStage(p.DNFAutomaticConfig))
}
for _, yumRepo := range p.YUMRepos {
pipeline.AddStage(osbuild.NewYumReposStage(yumRepo))
}
if p.GCPGuestAgentConfig != nil {
pipeline.AddStage(osbuild.NewGcpGuestAgentConfigStage(p.GCPGuestAgentConfig))
}
if p.SshdConfig != nil {
pipeline.AddStage((osbuild.NewSshdConfigStage(p.SshdConfig)))
}
if p.AuthConfig != nil {
pipeline.AddStage(osbuild.NewAuthconfigStage(p.AuthConfig))
}
if p.PwQuality != nil {
pipeline.AddStage(osbuild.NewPwqualityConfStage(p.PwQuality))
}
if p.Subscription != nil {
commands := []string{
fmt.Sprintf("/usr/sbin/subscription-manager register --org=%s --activationkey=%s --serverurl %s --baseurl %s", p.Subscription.Organization, p.Subscription.ActivationKey, p.Subscription.ServerUrl, p.Subscription.BaseUrl),
}
if p.Subscription.Insights {
commands = append(commands, "/usr/bin/insights-client --register")
}
pipeline.AddStage(osbuild.NewFirstBootStage(&osbuild.FirstBootStageOptions{
Commands: commands,
WaitForNetwork: true,
}))
if rhsmConfig, exists := p.RHSMConfig[distro.RHSMConfigWithSubscription]; exists {
pipeline.AddStage(osbuild.NewRHSMStage(rhsmConfig))
}
} else {
if rhsmConfig, exists := p.RHSMConfig[distro.RHSMConfigNoSubscription]; exists {
pipeline.AddStage(osbuild.NewRHSMStage(rhsmConfig))
}
}
if waConfig := p.WAAgentConfig; waConfig != nil {
pipeline.AddStage(osbuild.NewWAAgentConfStage(waConfig))
}
if udevRules := p.UdevRules; udevRules != nil {
pipeline.AddStage(osbuild.NewUdevRulesStage(udevRules))
}
if pt := p.PartitionTable; pt != nil {
kernelOptions := osbuild.GenImageKernelOptions(p.PartitionTable)
kernelOptions = append(kernelOptions, p.KernelOptionsAppend...)
if !p.KernelOptionsBootloader {
pipeline = prependKernelCmdlineStage(pipeline, strings.Join(kernelOptions, " "), pt)
}
pipeline.AddStage(osbuild.NewFSTabStage(osbuild.NewFSTabStageOptions(pt)))
var bootloader *osbuild.Stage
switch p.platform.GetArch() {
case platform.ARCH_S390X:
bootloader = osbuild.NewZiplStage(new(osbuild.ZiplStageOptions))
default:
if p.NoBLS {
// BLS entries not supported: use grub2.legacy
id := "76a22bf4-f153-4541-b6c7-0332c0dfaeac"
product := osbuild.GRUB2Product{
Name: p.OSProduct,
Version: p.OSVersion,
Nick: p.OSNick,
}
rescueVer, _ := rpmmd.GetVerStrFromPackageSpecList(p.packageSpecs, "dracut-config-rescue")
hasRescue := rescueVer != ""
bootloader = osbuild.NewGrub2LegacyStage(
osbuild.NewGrub2LegacyStageOptions(
p.Grub2Config,
p.PartitionTable,
kernelOptions,
p.platform.GetBIOSPlatform(),
p.platform.GetUEFIVendor(),
osbuild.MakeGrub2MenuEntries(id, p.kernelVer, product, hasRescue),
),
)
} else {
options := osbuild.NewGrub2StageOptionsUnified(pt,
p.kernelVer,
p.platform.GetUEFIVendor() != "",
p.platform.GetBIOSPlatform(),
p.platform.GetUEFIVendor(), false)
if cfg := p.Grub2Config; cfg != nil {
// TODO: don't store Grub2Config in OSPipeline, making the overrides unnecessary
// grub2.Config.Default is owned and set by `NewGrub2StageOptionsUnified`
// and thus we need to preserve it
if options.Config != nil {
cfg.Default = options.Config.Default
}
options.Config = cfg
}
if p.KernelOptionsBootloader {
options.WriteCmdLine = nil
if options.UEFI != nil {
options.UEFI.Unified = false
}
options.KernelOptions = strings.Join(kernelOptions, " ")
}
bootloader = osbuild.NewGRUB2Stage(options)
}
}
pipeline.AddStage(bootloader)
}
if p.OpenSCAPConfig != nil {
pipeline.AddStage(osbuild.NewOscapRemediationStage(p.OpenSCAPConfig))
}
if p.FactAPIType != "" {
pipeline.AddStage(osbuild.NewRHSMFactsStage(&osbuild.RHSMFactsStageOptions{
Facts: osbuild.RHSMFacts{
ApiType: p.FactAPIType,
},
}))
}
if p.OSTreeRef != "" {
pipeline.AddStage(osbuild.NewSystemdJournaldStage(
&osbuild.SystemdJournaldStageOptions{
Filename: "10-persistent.conf",
Config: osbuild.SystemdJournaldConfigDropin{
Journal: osbuild.SystemdJournaldConfigJournalSection{
Storage: osbuild.StoragePresistent,
},
},
}))
}
if p.SElinux != "" {
pipeline.AddStage(osbuild.NewSELinuxStage(&osbuild.SELinuxStageOptions{
FileContexts: fmt.Sprintf("etc/selinux/%s/contexts/files/file_contexts", p.SElinux),
}))
}
if p.OSTreeRef != "" {
pipeline.AddStage(osbuild.NewOSTreePrepTreeStage(&osbuild.OSTreePrepTreeStageOptions{
EtcGroupMembers: []string{
// NOTE: We may want to make this configurable.
"wheel", "docker",
},
}))
}
return pipeline
}
func prependKernelCmdlineStage(pipeline osbuild.Pipeline, kernelOptions string, pt *disk.PartitionTable) osbuild.Pipeline {
rootFs := pt.FindMountable("/")
if rootFs == nil {
panic("root filesystem must be defined for kernel-cmdline stage, this is a programming error")
}
rootFsUUID := rootFs.GetFSSpec().UUID
kernelStage := osbuild.NewKernelCmdlineStage(osbuild.NewKernelCmdlineStageOptions(rootFsUUID, kernelOptions))
pipeline.Stages = append([]*osbuild.Stage{kernelStage}, pipeline.Stages...)
return pipeline
}
func usersFirstBootOptions(users []users.User) *osbuild.FirstBootStageOptions {
cmds := make([]string, 0, 3*len(users)+2)
// workaround for creating authorized_keys file for user
// need to special case the root user, which has its home in a different place
varhome := filepath.Join("/var", "home")
roothome := filepath.Join("/var", "roothome")
for _, user := range users {
if user.Key != nil {
var home string
if user.Name == "root" {
home = roothome
} else {
home = filepath.Join(varhome, user.Name)
}
sshdir := filepath.Join(home, ".ssh")
cmds = append(cmds, fmt.Sprintf("mkdir -p %s", sshdir))
cmds = append(cmds, fmt.Sprintf("sh -c 'echo %q >> %q'", *user.Key, filepath.Join(sshdir, "authorized_keys")))
cmds = append(cmds, fmt.Sprintf("chown %s:%s -Rc %s", user.Name, user.Name, sshdir))
}
}
cmds = append(cmds, fmt.Sprintf("restorecon -rvF %s", varhome))
cmds = append(cmds, fmt.Sprintf("restorecon -rvF %s", roothome))
options := &osbuild.FirstBootStageOptions{
Commands: cmds,
WaitForNetwork: false,
}
return options
}
func (p *OS) GetPlatform() platform.Platform {
return p.platform
}