Update osbuild/images to v0.40.0

In addition, simplify the SPEC file to not have to update the minimum
required osbuild version gazillion times, but just once.

Update the minimum required osbuild version to v109, due to changes in
grub2 stages required by the new osbuild/images version.

Update osbild SHA in Schutzfile to v109.

Signed-off-by: Tomáš Hozza <thozza@redhat.com>
This commit is contained in:
Tomáš Hozza 2024-02-22 21:55:33 +01:00 committed by Tomáš Hozza
parent c138ea6939
commit 2f087f1a6c
190 changed files with 57031 additions and 52810 deletions

View file

@ -26,6 +26,7 @@ type Customizations struct {
Repositories []RepositoryCustomization `json:"repositories,omitempty" toml:"repositories,omitempty"`
FIPS *bool `json:"fips,omitempty" toml:"fips,omitempty"`
ContainersStorage *ContainerStorageCustomization `json:"containers-storage,omitempty" toml:"containers-storage,omitempty"`
Installer *InstallerCustomization `json:"installer,omitempty" toml:"installer,omitempty"`
}
type IgnitionCustomization struct {
@ -383,3 +384,10 @@ func (c *Customizations) GetContainerStorage() *ContainerStorageCustomization {
}
return c.ContainersStorage
}
func (c *Customizations) GetInstaller() *InstallerCustomization {
if c == nil || c.Installer == nil {
return nil
}
return c.Installer
}

View file

@ -0,0 +1,6 @@
package blueprint
type InstallerCustomization struct {
Unattended bool `json:"unattended,omitempty" toml:"unattended,omitempty"`
WheelSudoNopasswd bool `json:"wheel-sudo-nopasswd,omitempty" toml:"wheel-sudo-nopasswd,omitempty"`
}

View file

@ -31,8 +31,8 @@ type SourceSpec struct {
StoragePath *string
}
func NewResolver(arch string) Resolver {
return Resolver{
func NewResolver(arch string) *Resolver {
return &Resolver{
ctx: context.Background(),
queue: make(chan resolveResult, 2),
Arch: arch,

View file

@ -750,3 +750,20 @@ func (pt *PartitionTable) GetBuildPackages() []string {
return packages
}
// GetMountpointSize takes a mountpoint and returns the size of the entity this
// mountpoint belongs to.
func (pt *PartitionTable) GetMountpointSize(mountpoint string) (uint64, error) {
path := entityPath(pt, mountpoint)
if path == nil {
return 0, fmt.Errorf("cannot find mountpoint %s", mountpoint)
}
for _, ent := range path {
if sizeable, ok := ent.(Sizeable); ok {
return sizeable.GetSize(), nil
}
}
panic(fmt.Sprintf("no sizeable of the entity path for mountpoint %s, this is a programming error", mountpoint))
}

View file

@ -344,8 +344,15 @@ func imageInstallerImage(workload workload.Workload,
containers []container.SourceSpec,
rng *rand.Rand) (image.ImageKind, error) {
customizations := bp.Customizations
img := image.NewAnacondaTarInstaller()
if instCust := customizations.GetInstaller(); instCust != nil {
img.WheelNoPasswd = instCust.WheelSudoNopasswd
img.UnattendedKickstart = instCust.Unattended
}
// Enable anaconda-webui for Fedora > 38
distro := t.Arch().Distro()
if !common.VersionLessThan(distro.Releasever(), "38") {
@ -354,11 +361,20 @@ func imageInstallerImage(workload workload.Workload,
"org.fedoraproject.Anaconda.Modules.Timezone",
"org.fedoraproject.Anaconda.Modules.Localization",
}
img.AdditionalKernelOpts = []string{"inst.webui", "inst.webui.remote"}
if img.UnattendedKickstart {
// NOTE: this is not supported right now because the
// image-installer on Fedora isn't working when unattended.
// These options are probably necessary but could change.
// Unattended/non-interactive installations are better set to text
// time since they might be running headless and a UI is
// unnecessary.
img.AdditionalKernelOpts = []string{"inst.text", "inst.noninteractive"}
} else {
img.AdditionalKernelOpts = []string{"inst.webui", "inst.webui.remote"}
}
}
img.AdditionalAnacondaModules = append(img.AdditionalAnacondaModules, "org.fedoraproject.Anaconda.Modules.Users")
customizations := bp.Customizations
img.Platform = t.platform
img.Workload = workload
img.OSCustomizations = osCustomizations(t, packageSets[osPkgsKey], containers, customizations)
@ -513,12 +529,23 @@ func iotInstallerImage(workload workload.Workload,
img.ExtraBasePackages = packageSets[installerPkgsKey]
img.Users = users.UsersFromBP(customizations.GetUsers())
img.Groups = users.GroupsFromBP(customizations.GetGroups())
img.Language, img.Keyboard = customizations.GetPrimaryLocale()
// ignore ntp servers - we don't currently support setting these in the
// kickstart though kickstart does support setting them
img.Timezone, _ = customizations.GetTimezoneSettings()
img.AdditionalAnacondaModules = []string{
"org.fedoraproject.Anaconda.Modules.Timezone",
"org.fedoraproject.Anaconda.Modules.Localization",
"org.fedoraproject.Anaconda.Modules.Users",
}
if instCust := customizations.GetInstaller(); instCust != nil {
img.WheelNoPasswd = instCust.WheelSudoNopasswd
img.UnattendedKickstart = instCust.Unattended
}
img.SquashfsCompression = "lz4"
img.ISOLabelTempl = d.isolabelTmpl

View file

@ -333,12 +333,13 @@ func (t *imageType) checkOptions(bp *blueprint.Blueprint, options distro.ImageOp
}
}
} else if t.name == "iot-installer" || t.name == "image-installer" {
allowed := []string{"User", "Group", "FIPS"}
// "Installer" is actually not allowed for image-installer right now, but this is checked at the end
allowed := []string{"User", "Group", "FIPS", "Installer", "Timezone", "Locale"}
if err := customizations.CheckAllowed(allowed...); err != nil {
return nil, fmt.Errorf(distro.UnsupportedCustomizationError, t.name, strings.Join(allowed, ", "))
}
} else if t.name == "live-installer" {
allowed := []string{}
allowed := []string{"Installer"}
if err := customizations.CheckAllowed(allowed...); err != nil {
return nil, fmt.Errorf(distro.NoCustomizationsAllowedError, t.name)
}
@ -403,5 +404,12 @@ func (t *imageType) checkOptions(bp *blueprint.Blueprint, options distro.ImageOp
return []string{w}, nil
}
if customizations.GetInstaller() != nil {
// only supported by the Anaconda installer
if slices.Index([]string{"iot-installer"}, t.name) == -1 {
return nil, fmt.Errorf("installer customizations are not supported for %q", t.name)
}
}
return nil, nil
}

View file

@ -203,8 +203,10 @@ func bootableContainerPackageSet(t *imageType) rpmmd.PackageSet {
"crun",
"cryptsetup",
"dnf",
"dosfstools",
"e2fsprogs",
"fwupd", // if you're using linux-firmware, you probably also want fwupd
"fwupd", // if you're using linux-firmware, you probably also want fwupd
"gdisk",
"iproute", "iproute-tc", // route manipulation and QoS
"iptables", "nftables", // firewall manipulation
"iptables-services", // additional firewall support

View file

@ -16,7 +16,7 @@ func amiImgTypeX86_64(rd distribution) imageType {
packageSets: map[string]packageSetFunc{
osPkgsKey: ec2CommonPackageSet,
},
defaultImageConfig: defaultAMIImageConfigX86_64(rd),
defaultImageConfig: defaultAMIImageConfigX86_64(),
kernelOptions: "console=ttyS0,115200n8 console=tty0 net.ifnames=0 rd.blacklist=nouveau nvme_core.io_timeout=4294967295 crashkernel=auto",
bootable: true,
defaultSize: 10 * common.GibiByte,
@ -24,19 +24,13 @@ func amiImgTypeX86_64(rd distribution) imageType {
buildPipelines: []string{"build"},
payloadPipelines: []string{"os", "image"},
exports: []string{"image"},
basePartitionTables: ec2BasePartitionTables,
basePartitionTables: getEc2PartitionTables(rd.osVersion, rd.isRHEL()),
}
return it
}
func ec2ImgTypeX86_64(rd distribution) imageType {
basePartitionTables := ec2BasePartitionTables
// use legacy partition tables for RHEL 8.8 and older
if common.VersionLessThan(rd.osVersion, "8.9") {
basePartitionTables = ec2LegacyBasePartitionTables
}
it := imageType{
name: "ec2",
filename: "image.raw.xz",
@ -53,18 +47,12 @@ func ec2ImgTypeX86_64(rd distribution) imageType {
buildPipelines: []string{"build"},
payloadPipelines: []string{"os", "image", "xz"},
exports: []string{"xz"},
basePartitionTables: basePartitionTables,
basePartitionTables: getEc2PartitionTables(rd.osVersion, rd.isRHEL()),
}
return it
}
func ec2HaImgTypeX86_64(rd distribution) imageType {
basePartitionTables := ec2BasePartitionTables
// use legacy partition tables for RHEL 8.8 and older
if rd.isRHEL() && common.VersionLessThan(rd.osVersion, "8.9") {
basePartitionTables = ec2LegacyBasePartitionTables
}
it := imageType{
name: "ec2-ha",
filename: "image.raw.xz",
@ -81,7 +69,7 @@ func ec2HaImgTypeX86_64(rd distribution) imageType {
buildPipelines: []string{"build"},
payloadPipelines: []string{"os", "image", "xz"},
exports: []string{"xz"},
basePartitionTables: basePartitionTables,
basePartitionTables: getEc2PartitionTables(rd.osVersion, rd.isRHEL()),
}
return it
}
@ -94,7 +82,7 @@ func amiImgTypeAarch64(rd distribution) imageType {
packageSets: map[string]packageSetFunc{
osPkgsKey: ec2CommonPackageSet,
},
defaultImageConfig: defaultAMIImageConfig(rd),
defaultImageConfig: defaultAMIImageConfig(),
kernelOptions: "console=ttyS0,115200n8 console=tty0 net.ifnames=0 rd.blacklist=nouveau nvme_core.io_timeout=4294967295 iommu.strict=0 crashkernel=auto",
bootable: true,
defaultSize: 10 * common.GibiByte,
@ -102,18 +90,12 @@ func amiImgTypeAarch64(rd distribution) imageType {
buildPipelines: []string{"build"},
payloadPipelines: []string{"os", "image"},
exports: []string{"image"},
basePartitionTables: ec2BasePartitionTables,
basePartitionTables: getEc2PartitionTables(rd.osVersion, rd.isRHEL()),
}
return it
}
func ec2ImgTypeAarch64(rd distribution) imageType {
basePartitionTables := ec2BasePartitionTables
// use legacy partition tables for RHEL 8.8 and older
if common.VersionLessThan(rd.osVersion, "8.9") {
basePartitionTables = ec2LegacyBasePartitionTables
}
it := imageType{
name: "ec2",
filename: "image.raw.xz",
@ -130,18 +112,12 @@ func ec2ImgTypeAarch64(rd distribution) imageType {
buildPipelines: []string{"build"},
payloadPipelines: []string{"os", "image", "xz"},
exports: []string{"xz"},
basePartitionTables: basePartitionTables,
basePartitionTables: getEc2PartitionTables(rd.osVersion, rd.isRHEL()),
}
return it
}
func ec2SapImgTypeX86_64(rd distribution) imageType {
basePartitionTables := ec2BasePartitionTables
// use legacy partition tables for RHEL 8.8 and older
if common.VersionLessThan(rd.osVersion, "8.9") {
basePartitionTables = ec2LegacyBasePartitionTables
}
it := imageType{
name: "ec2-sap",
filename: "image.raw.xz",
@ -158,7 +134,7 @@ func ec2SapImgTypeX86_64(rd distribution) imageType {
buildPipelines: []string{"build"},
payloadPipelines: []string{"os", "image", "xz"},
exports: []string{"xz"},
basePartitionTables: basePartitionTables,
basePartitionTables: getEc2PartitionTables(rd.osVersion, rd.isRHEL()),
}
return it
}
@ -227,7 +203,6 @@ func baseEc2ImageConfig() *distro.ImageConfig {
{
Filename: "00-getty-fixes.conf",
Config: osbuild.SystemdLogindConfigDropin{
Login: osbuild.SystemdLogindConfigLoginSection{
NAutoVTs: common.ToPtr(0),
},
@ -307,24 +282,23 @@ func defaultEc2ImageConfig(rd distribution) *distro.ImageConfig {
return ic
}
// default AMI (EC2 BYOS) images config
func defaultAMIImageConfig(rd distribution) *distro.ImageConfig {
ic := defaultEc2ImageConfig(rd)
if rd.isRHEL() {
// defaultEc2ImageConfig() adds the rhsm options only for RHEL < 8.7
// Add it unconditionally for AMI
ic = appendRHSM(ic)
}
return ic
}
func defaultEc2ImageConfigX86_64(rd distribution) *distro.ImageConfig {
ic := defaultEc2ImageConfig(rd)
return appendEC2DracutX86_64(ic)
}
func defaultAMIImageConfigX86_64(rd distribution) *distro.ImageConfig {
ic := defaultAMIImageConfig(rd).InheritFrom(defaultEc2ImageConfigX86_64(rd))
// Default AMI (custom image built by users) images config.
// The configuration does not touch the RHSM configuration at all.
// https://issues.redhat.com/browse/COMPOSER-2157
func defaultAMIImageConfig() *distro.ImageConfig {
return baseEc2ImageConfig()
}
// Default AMI x86_64 (custom image built by users) images config.
// The configuration does not touch the RHSM configuration at all.
// https://issues.redhat.com/browse/COMPOSER-2157
func defaultAMIImageConfigX86_64() *distro.ImageConfig {
ic := defaultAMIImageConfig()
return appendEC2DracutX86_64(ic)
}

View file

@ -11,7 +11,8 @@ import (
"github.com/osbuild/images/pkg/subscription"
)
const defaultAzureKernelOptions = "ro crashkernel=auto console=tty1 console=ttyS0 earlyprintk=ttyS0 rootdelay=300"
// use loglevel=3 as described in the RHEL documentation and used in existing RHEL images built by MSFT
const defaultAzureKernelOptions = "ro loglevel=3 crashkernel=auto console=tty1 console=ttyS0 earlyprintk=ttyS0 rootdelay=300"
func azureRhuiImgType() imageType {
return imageType{
@ -473,6 +474,7 @@ var azureRhuiBasePartitionTables = distro.BasePartitionTableMap{
},
}
// based on https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/deploying_rhel_8_on_microsoft_azure/assembly_deploying-a-rhel-image-as-a-virtual-machine-on-microsoft-azure_cloud-content-azure#making-configuration-changes_configure-the-image-azure
var defaultAzureImageConfig = &distro.ImageConfig{
Timezone: common.ToPtr("Etc/UTC"),
Locale: common.ToPtr("en_US.UTF-8"),
@ -584,10 +586,13 @@ var defaultAzureImageConfig = &distro.ImageConfig{
},
},
Grub2Config: &osbuild.GRUB2Config{
TerminalInput: []string{"serial", "console"},
TerminalOutput: []string{"serial", "console"},
Serial: "serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1",
Timeout: 10,
DisableRecovery: common.ToPtr(true),
DisableSubmenu: common.ToPtr(true),
Distributor: "$(sed 's, release .*$,,g' /etc/system-release)",
Terminal: []string{"serial", "console"},
Serial: "serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1",
Timeout: 10,
TimeoutStyle: osbuild.GRUB2ConfigTimeoutStyleCountdown,
},
UdevRules: &osbuild.UdevRulesStageOptions{
Filename: "/etc/udev/rules.d/68-azure-sriov-nm-unmanaged.rules",
@ -624,35 +629,12 @@ var defaultAzureImageConfig = &distro.ImageConfig{
}
// Diff of the default Image Config compare to the `defaultAzureImageConfig`
// The configuration for non-RHUI images does not touch the RHSM configuration at all.
// https://issues.redhat.com/browse/COMPOSER-2157
var defaultAzureByosImageConfig = &distro.ImageConfig{
GPGKeyFiles: []string{
"/etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release",
},
RHSMConfig: map[subscription.RHSMStatus]*osbuild.RHSMStageOptions{
subscription.RHSMConfigNoSubscription: {
SubMan: &osbuild.RHSMStageOptionsSubMan{
Rhsmcertd: &osbuild.SubManConfigRHSMCERTDSection{
AutoRegistration: common.ToPtr(true),
},
// Don't disable RHSM redhat.repo management on the GCE
// image, which is BYOS and does not use RHUI for content.
// Otherwise subscribing the system manually after booting
// it would result in empty redhat.repo. Without RHUI, such
// system would have no way to get Red Hat content, but
// enable the repo management manually, which would be very
// confusing.
},
},
subscription.RHSMConfigWithSubscription: {
SubMan: &osbuild.RHSMStageOptionsSubMan{
Rhsmcertd: &osbuild.SubManConfigRHSMCERTDSection{
AutoRegistration: common.ToPtr(true),
},
// do not disable the redhat.repo management if the user
// explicitly request the system to be subscribed
},
},
},
}
// Diff of the default Image Config compare to the `defaultAzureImageConfig`

View file

@ -52,6 +52,8 @@ func gceRhuiImgType(rd distribution) imageType {
}
}
// The configuration for non-RHUI images does not touch the RHSM configuration at all.
// https://issues.redhat.com/browse/COMPOSER-2157
func defaultGceByosImageConfig(rd distribution) *distro.ImageConfig {
ic := &distro.ImageConfig{
Timezone: common.ToPtr("UTC"),
@ -156,33 +158,6 @@ func defaultGceByosImageConfig(rd distribution) *distro.ImageConfig {
)
}
if rd.isRHEL() {
ic.RHSMConfig = map[subscription.RHSMStatus]*osbuild.RHSMStageOptions{
subscription.RHSMConfigNoSubscription: {
SubMan: &osbuild.RHSMStageOptionsSubMan{
Rhsmcertd: &osbuild.SubManConfigRHSMCERTDSection{
AutoRegistration: common.ToPtr(true),
},
// Don't disable RHSM redhat.repo management on the GCE
// image, which is BYOS and does not use RHUI for content.
// Otherwise subscribing the system manually after booting
// it would result in empty redhat.repo. Without RHUI, such
// system would have no way to get Red Hat content, but
// enable the repo management manually, which would be very
// confusing.
},
},
subscription.RHSMConfigWithSubscription: {
SubMan: &osbuild.RHSMStageOptionsSubMan{
Rhsmcertd: &osbuild.SubManConfigRHSMCERTDSection{
AutoRegistration: common.ToPtr(true),
},
// do not disable the redhat.repo management if the user
// explicitly request the system to be subscribed
},
},
}
}
return ic
}

View file

@ -331,6 +331,11 @@ func imageInstallerImage(workload workload.Workload,
img.AdditionalDracutModules = []string{"prefixdevname", "prefixdevname-tools"}
img.AdditionalAnacondaModules = []string{"org.fedoraproject.Anaconda.Modules.Users"}
if instCust := customizations.GetInstaller(); instCust != nil {
img.WheelNoPasswd = instCust.WheelSudoNopasswd
img.UnattendedKickstart = instCust.Unattended
}
img.SquashfsCompression = "xz"
// put the kickstart file in the root of the iso
@ -437,6 +442,16 @@ func edgeInstallerImage(workload workload.Workload,
img.Users = users.UsersFromBP(customizations.GetUsers())
img.Groups = users.GroupsFromBP(customizations.GetGroups())
img.Language, img.Keyboard = customizations.GetPrimaryLocale()
// ignore ntp servers - we don't currently support setting these in the
// kickstart though kickstart does support setting them
img.Timezone, _ = customizations.GetTimezoneSettings()
if instCust := customizations.GetInstaller(); instCust != nil {
img.WheelNoPasswd = instCust.WheelSudoNopasswd
img.UnattendedKickstart = instCust.Unattended
}
img.SquashfsCompression = "xz"
img.AdditionalDracutModules = []string{"prefixdevname", "prefixdevname-tools"}

View file

@ -344,7 +344,7 @@ func (t *imageType) checkOptions(bp *blueprint.Blueprint, options distro.ImageOp
}
}
} else if t.name == "edge-installer" {
allowed := []string{"User", "Group", "FIPS"}
allowed := []string{"User", "Group", "FIPS", "Installer", "Timezone", "Locale"}
if err := customizations.CheckAllowed(allowed...); err != nil {
return warnings, fmt.Errorf(distro.UnsupportedCustomizationError, t.name, strings.Join(allowed, ", "))
}
@ -439,5 +439,12 @@ func (t *imageType) checkOptions(bp *blueprint.Blueprint, options distro.ImageOp
warnings = append(warnings, w)
}
if customizations.GetInstaller() != nil {
// only supported by the Anaconda installer
if slices.Index([]string{"image-installer", "edge-installer", "live-installer"}, t.name) == -1 {
return warnings, fmt.Errorf("installer customizations are not supported for %q", t.name)
}
}
return warnings, nil
}

View file

@ -117,182 +117,6 @@ var defaultBasePartitionTables = distro.BasePartitionTableMap{
},
},
}
var ec2BasePartitionTables = distro.BasePartitionTableMap{
arch.ARCH_X86_64.String(): disk.PartitionTable{
UUID: "D209C89E-EA5E-4FBD-B161-B461CCE297E0",
Type: "gpt",
Partitions: []disk.Partition{
{
Size: 1 * common.MebiByte,
Bootable: true,
Type: disk.BIOSBootPartitionGUID,
UUID: disk.BIOSBootPartitionUUID,
},
{
Size: 200 * common.MebiByte,
Type: disk.EFISystemPartitionGUID,
UUID: disk.EFISystemPartitionUUID,
Payload: &disk.Filesystem{
Type: "vfat",
UUID: disk.EFIFilesystemUUID,
Mountpoint: "/boot/efi",
Label: "EFI-SYSTEM",
FSTabOptions: "defaults,uid=0,gid=0,umask=077,shortname=winnt",
FSTabFreq: 0,
FSTabPassNo: 2,
},
},
{
Size: 500 * common.MebiByte,
Type: disk.XBootLDRPartitionGUID,
UUID: disk.FilesystemDataUUID,
Payload: &disk.Filesystem{
Type: "xfs",
Mountpoint: "/boot",
Label: "boot",
FSTabOptions: "defaults",
FSTabFreq: 0,
FSTabPassNo: 0,
},
},
{
Size: 2 * common.GibiByte,
Type: disk.FilesystemDataGUID,
UUID: disk.RootPartitionUUID,
Payload: &disk.Filesystem{
Type: "xfs",
Label: "root",
Mountpoint: "/",
FSTabOptions: "defaults",
FSTabFreq: 0,
FSTabPassNo: 0,
},
},
},
},
arch.ARCH_AARCH64.String(): disk.PartitionTable{
UUID: "D209C89E-EA5E-4FBD-B161-B461CCE297E0",
Type: "gpt",
Partitions: []disk.Partition{
{
Size: 200 * common.MebiByte,
Type: disk.EFISystemPartitionGUID,
UUID: disk.EFISystemPartitionUUID,
Payload: &disk.Filesystem{
Type: "vfat",
UUID: disk.EFIFilesystemUUID,
Mountpoint: "/boot/efi",
Label: "EFI-SYSTEM",
FSTabOptions: "defaults,uid=0,gid=0,umask=077,shortname=winnt",
FSTabFreq: 0,
FSTabPassNo: 2,
},
},
{
Size: 500 * common.MebiByte,
Type: disk.XBootLDRPartitionGUID,
UUID: disk.FilesystemDataUUID,
Payload: &disk.Filesystem{
Type: "xfs",
Mountpoint: "/boot",
Label: "boot",
FSTabOptions: "defaults",
FSTabFreq: 0,
FSTabPassNo: 0,
},
},
{
Size: 2 * common.GibiByte,
Type: disk.FilesystemDataGUID,
UUID: disk.RootPartitionUUID,
Payload: &disk.Filesystem{
Type: "xfs",
Label: "root",
Mountpoint: "/",
FSTabOptions: "defaults",
FSTabFreq: 0,
FSTabPassNo: 0,
},
},
},
},
}
// ec2LegacyBasePartitionTables is the partition table layout for RHEL EC2
// images prior to 8.9. It is used for backwards compatibility.
var ec2LegacyBasePartitionTables = distro.BasePartitionTableMap{
arch.ARCH_X86_64.String(): disk.PartitionTable{
UUID: "D209C89E-EA5E-4FBD-B161-B461CCE297E0",
Type: "gpt",
Partitions: []disk.Partition{
{
Size: 1 * common.MebiByte,
Bootable: true,
Type: disk.BIOSBootPartitionGUID,
UUID: disk.BIOSBootPartitionUUID,
},
{
Size: 2 * common.GibiByte,
Type: disk.FilesystemDataGUID,
UUID: disk.RootPartitionUUID,
Payload: &disk.Filesystem{
Type: "xfs",
Label: "root",
Mountpoint: "/",
FSTabOptions: "defaults",
FSTabFreq: 0,
FSTabPassNo: 0,
},
},
},
},
arch.ARCH_AARCH64.String(): disk.PartitionTable{
UUID: "D209C89E-EA5E-4FBD-B161-B461CCE297E0",
Type: "gpt",
Partitions: []disk.Partition{
{
Size: 200 * common.MebiByte,
Type: disk.EFISystemPartitionGUID,
UUID: disk.EFISystemPartitionUUID,
Payload: &disk.Filesystem{
Type: "vfat",
UUID: disk.EFIFilesystemUUID,
Mountpoint: "/boot/efi",
FSTabOptions: "defaults,uid=0,gid=0,umask=077,shortname=winnt",
FSTabFreq: 0,
FSTabPassNo: 2,
},
},
{
Size: 512 * common.MebiByte,
Type: disk.FilesystemDataGUID,
UUID: disk.FilesystemDataUUID,
Payload: &disk.Filesystem{
Type: "xfs",
Mountpoint: "/boot",
FSTabOptions: "defaults",
FSTabFreq: 0,
FSTabPassNo: 0,
},
},
{
Size: 2 * common.GibiByte,
Type: disk.FilesystemDataGUID,
UUID: disk.RootPartitionUUID,
Payload: &disk.Filesystem{
Type: "xfs",
Label: "root",
Mountpoint: "/",
FSTabOptions: "defaults",
FSTabFreq: 0,
FSTabPassNo: 0,
},
},
},
},
}
var edgeBasePartitionTables = distro.BasePartitionTableMap{
arch.ARCH_X86_64.String(): disk.PartitionTable{
UUID: "D209C89E-EA5E-4FBD-B161-B461CCE297E0",
@ -423,3 +247,100 @@ var edgeBasePartitionTables = distro.BasePartitionTableMap{
},
},
}
func getEc2PartitionTables(osVersion string, isRHEL bool) distro.BasePartitionTableMap {
// x86_64 - without /boot
// aarch - <= 8.9 - 512MiB, 8.10 and centos: 1 GiB
var aarch64BootSize uint64
switch {
case common.VersionLessThan(osVersion, "8.10") && isRHEL:
aarch64BootSize = 512 * common.MebiByte
default:
aarch64BootSize = 1 * common.GibiByte
}
return distro.BasePartitionTableMap{
arch.ARCH_X86_64.String(): disk.PartitionTable{
UUID: "D209C89E-EA5E-4FBD-B161-B461CCE297E0",
Type: "gpt",
Partitions: []disk.Partition{
{
Size: 1 * common.MebiByte,
Bootable: true,
Type: disk.BIOSBootPartitionGUID,
UUID: disk.BIOSBootPartitionUUID,
},
{
Size: 200 * common.MebiByte,
Type: disk.EFISystemPartitionGUID,
UUID: disk.EFISystemPartitionUUID,
Payload: &disk.Filesystem{
Type: "vfat",
UUID: disk.EFIFilesystemUUID,
Mountpoint: "/boot/efi",
FSTabOptions: "defaults,uid=0,gid=0,umask=077,shortname=winnt",
FSTabFreq: 0,
FSTabPassNo: 2,
},
},
{
Size: 2 * common.GibiByte,
Type: disk.FilesystemDataGUID,
UUID: disk.RootPartitionUUID,
Payload: &disk.Filesystem{
Type: "xfs",
Label: "root",
Mountpoint: "/",
FSTabOptions: "defaults",
FSTabFreq: 0,
FSTabPassNo: 0,
},
},
},
},
arch.ARCH_AARCH64.String(): disk.PartitionTable{
UUID: "D209C89E-EA5E-4FBD-B161-B461CCE297E0",
Type: "gpt",
Partitions: []disk.Partition{
{
Size: 200 * common.MebiByte,
Type: disk.EFISystemPartitionGUID,
UUID: disk.EFISystemPartitionUUID,
Payload: &disk.Filesystem{
Type: "vfat",
UUID: disk.EFIFilesystemUUID,
Mountpoint: "/boot/efi",
FSTabOptions: "defaults,uid=0,gid=0,umask=077,shortname=winnt",
FSTabFreq: 0,
FSTabPassNo: 2,
},
},
{
Size: aarch64BootSize,
Type: disk.FilesystemDataGUID,
UUID: disk.FilesystemDataUUID,
Payload: &disk.Filesystem{
Type: "xfs",
Mountpoint: "/boot",
FSTabOptions: "defaults",
FSTabFreq: 0,
FSTabPassNo: 0,
},
},
{
Size: 2 * common.GibiByte,
Type: disk.FilesystemDataGUID,
UUID: disk.RootPartitionUUID,
Payload: &disk.Filesystem{
Type: "xfs",
Label: "root",
Mountpoint: "/",
FSTabOptions: "defaults",
FSTabFreq: 0,
FSTabPassNo: 0,
},
},
},
},
}
}

View file

@ -189,7 +189,6 @@ func baseEc2ImageConfig() *distro.ImageConfig {
{
Filename: "00-getty-fixes.conf",
Config: osbuild.SystemdLogindConfigDropin{
Login: osbuild.SystemdLogindConfigLoginSection{
NAutoVTs: common.ToPtr(0),
},
@ -266,24 +265,23 @@ func defaultEc2ImageConfig(osVersion string, rhsm bool) *distro.ImageConfig {
return ic
}
// default AMI (EC2 BYOS) images config
func defaultAMIImageConfig(osVersion string, rhsm bool) *distro.ImageConfig {
ic := defaultEc2ImageConfig(osVersion, rhsm)
if rhsm {
// defaultEc2ImageConfig() adds the rhsm options only for RHEL < 9.1
// Add it unconditionally for AMI
ic = appendRHSM(ic)
}
return ic
}
func defaultEc2ImageConfigX86_64(osVersion string, rhsm bool) *distro.ImageConfig {
ic := defaultEc2ImageConfig(osVersion, rhsm)
return appendEC2DracutX86_64(ic)
}
func defaultAMIImageConfigX86_64(osVersion string, rhsm bool) *distro.ImageConfig {
ic := defaultAMIImageConfig(osVersion, rhsm).InheritFrom(defaultEc2ImageConfigX86_64(osVersion, rhsm))
// Default AMI (custom image built by users) images config.
// The configuration does not touch the RHSM configuration at all.
// https://issues.redhat.com/browse/COMPOSER-2157
func defaultAMIImageConfig() *distro.ImageConfig {
return baseEc2ImageConfig()
}
// Default AMI x86_64 (custom image built by users) images config.
// The configuration does not touch the RHSM configuration at all.
// https://issues.redhat.com/browse/COMPOSER-2157
func defaultAMIImageConfigX86_64() *distro.ImageConfig {
ic := defaultAMIImageConfig()
return appendEC2DracutX86_64(ic)
}
@ -418,9 +416,9 @@ func mkEc2ImgTypeX86_64(osVersion string, rhsm bool) imageType {
return it
}
func mkAMIImgTypeX86_64(osVersion string, rhsm bool) imageType {
func mkAMIImgTypeX86_64() imageType {
it := amiImgTypeX86_64
ic := defaultAMIImageConfigX86_64(osVersion, rhsm)
ic := defaultAMIImageConfigX86_64()
it.defaultImageConfig = ic
return it
}
@ -438,9 +436,9 @@ func mkEc2HaImgTypeX86_64(osVersion string, rhsm bool) imageType {
return it
}
func mkAMIImgTypeAarch64(osVersion string, rhsm bool) imageType {
func mkAMIImgTypeAarch64() imageType {
it := amiImgTypeAarch64
ic := defaultAMIImageConfig(osVersion, rhsm)
ic := defaultAMIImageConfig()
it.defaultImageConfig = ic
return it
}

View file

@ -414,8 +414,10 @@ func azureRhuiBasePartitionTables(t *imageType) (disk.PartitionTable, bool) {
}
}
var defaultAzureKernelOptions = "ro console=tty1 console=ttyS0 earlyprintk=ttyS0 rootdelay=300"
// use loglevel=3 as described in the RHEL documentation and used in existing RHEL images built by MSFT
var defaultAzureKernelOptions = "ro loglevel=3 console=tty1 console=ttyS0 earlyprintk=ttyS0 rootdelay=300"
// based on https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9/html/deploying_rhel_9_on_microsoft_azure/assembly_deploying-a-rhel-image-as-a-virtual-machine-on-microsoft-azure_cloud-content-azure#making-configuration-changes_configure-the-image-azure
var defaultAzureImageConfig = &distro.ImageConfig{
Timezone: common.ToPtr("Etc/UTC"),
Locale: common.ToPtr("en_US.UTF-8"),
@ -456,6 +458,12 @@ var defaultAzureImageConfig = &distro.ImageConfig{
osbuild.NewModprobeConfigCmdBlacklist("amdgpu"),
},
},
{
Filename: "blacklist-intel-cstate.conf",
Commands: osbuild.ModprobeConfigCmdList{
osbuild.NewModprobeConfigCmdBlacklist("intel_cstate"),
},
},
{
Filename: "blacklist-floppy.conf",
Commands: osbuild.ModprobeConfigCmdList{
@ -469,6 +477,12 @@ var defaultAzureImageConfig = &distro.ImageConfig{
osbuild.NewModprobeConfigCmdBlacklist("lbm-nouveau"),
},
},
{
Filename: "blacklist-skylake-edac.conf",
Commands: osbuild.ModprobeConfigCmdList{
osbuild.NewModprobeConfigCmdBlacklist("skx_edac"),
},
},
},
CloudInit: []*osbuild.CloudInitStageOptions{
{
@ -515,10 +529,13 @@ var defaultAzureImageConfig = &distro.ImageConfig{
},
},
Grub2Config: &osbuild.GRUB2Config{
TerminalInput: []string{"serial", "console"},
TerminalOutput: []string{"serial", "console"},
Serial: "serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1",
Timeout: 10,
DisableRecovery: common.ToPtr(true),
DisableSubmenu: common.ToPtr(true),
Distributor: "$(sed 's, release .*$,,g' /etc/system-release)",
Terminal: []string{"serial", "console"},
Serial: "serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1",
Timeout: 10,
TimeoutStyle: osbuild.GRUB2ConfigTimeoutStyleCountdown,
},
UdevRules: &osbuild.UdevRulesStageOptions{
Filename: "/etc/udev/rules.d/68-azure-sriov-nm-unmanaged.rules",
@ -555,35 +572,12 @@ var defaultAzureImageConfig = &distro.ImageConfig{
}
// Diff of the default Image Config compare to the `defaultAzureImageConfig`
// The configuration for non-RHUI images does not touch the RHSM configuration at all.
// https://issues.redhat.com/browse/COMPOSER-2157
var defaultAzureByosImageConfig = &distro.ImageConfig{
GPGKeyFiles: []string{
"/etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release",
},
RHSMConfig: map[subscription.RHSMStatus]*osbuild.RHSMStageOptions{
subscription.RHSMConfigNoSubscription: {
SubMan: &osbuild.RHSMStageOptionsSubMan{
Rhsmcertd: &osbuild.SubManConfigRHSMCERTDSection{
AutoRegistration: common.ToPtr(true),
},
// Don't disable RHSM redhat.repo management on the GCE
// image, which is BYOS and does not use RHUI for content.
// Otherwise subscribing the system manually after booting
// it would result in empty redhat.repo. Without RHUI, such
// system would have no way to get Red Hat content, but
// enable the repo management manually, which would be very
// confusing.
},
},
subscription.RHSMConfigWithSubscription: {
SubMan: &osbuild.RHSMStageOptionsSubMan{
Rhsmcertd: &osbuild.SubManConfigRHSMCERTDSection{
AutoRegistration: common.ToPtr(true),
},
// do not disable the redhat.repo management if the user
// explicitly request the system to be subscribed
},
},
},
}
// Diff of the default Image Config compare to the `defaultAzureImageConfig`

View file

@ -253,7 +253,7 @@ func newDistro(name string, minor int) *distribution {
}
x86_64.addImageTypes(
ec2X86Platform,
mkAMIImgTypeX86_64(rd.osVersion, rd.isRHEL()),
mkAMIImgTypeX86_64(),
)
gceX86Platform := &platform.X86{
@ -264,7 +264,7 @@ func newDistro(name string, minor int) *distribution {
}
x86_64.addImageTypes(
gceX86Platform,
mkGCEImageType(rd.isRHEL()),
mkGCEImageType(),
)
x86_64.addImageTypes(
@ -391,7 +391,7 @@ func newDistro(name string, minor int) *distribution {
ImageFormat: platform.FORMAT_RAW,
},
},
mkAMIImgTypeAarch64(rd.osVersion, rd.isRHEL()),
mkAMIImgTypeAarch64(),
)
ppc64le.addImageTypes(
@ -455,7 +455,7 @@ func newDistro(name string, minor int) *distribution {
)
// add GCE RHUI image to RHEL only
x86_64.addImageTypes(gceX86Platform, mkGCERHUIImageType(rd.isRHEL()))
x86_64.addImageTypes(gceX86Platform, mkGCERHUIImageType())
} else {
x86_64.addImageTypes(azureX64Platform, azureImgType)
aarch64.addImageTypes(azureAarch64Platform, azureImgType)

View file

@ -48,19 +48,21 @@ var (
}
)
func mkGCEImageType(rhsm bool) imageType {
func mkGCEImageType() imageType {
it := gceImgType
it.defaultImageConfig = baseGCEImageConfig(rhsm)
// The configuration for non-RHUI images does not touch the RHSM configuration at all.
// https://issues.redhat.com/browse/COMPOSER-2157
it.defaultImageConfig = baseGCEImageConfig()
return it
}
func mkGCERHUIImageType(rhsm bool) imageType {
func mkGCERHUIImageType() imageType {
it := gceRhuiImgType
it.defaultImageConfig = defaultGceRhuiImageConfig(rhsm)
it.defaultImageConfig = defaultGceRhuiImageConfig()
return it
}
func baseGCEImageConfig(rhsm bool) *distro.ImageConfig {
func baseGCEImageConfig() *distro.ImageConfig {
ic := &distro.ImageConfig{
Timezone: common.ToPtr("UTC"),
TimeSynchronization: &osbuild.ChronyStageOptions{
@ -154,37 +156,10 @@ func baseGCEImageConfig(rhsm bool) *distro.ImageConfig {
},
}
if rhsm {
ic.RHSMConfig = map[subscription.RHSMStatus]*osbuild.RHSMStageOptions{
subscription.RHSMConfigNoSubscription: {
SubMan: &osbuild.RHSMStageOptionsSubMan{
Rhsmcertd: &osbuild.SubManConfigRHSMCERTDSection{
AutoRegistration: common.ToPtr(true),
},
// Don't disable RHSM redhat.repo management on the GCE
// image, which is BYOS and does not use RHUI for content.
// Otherwise subscribing the system manually after booting
// it would result in empty redhat.repo. Without RHUI, such
// system would have no way to get Red Hat content, but
// enable the repo management manually, which would be very
// confusing.
},
},
subscription.RHSMConfigWithSubscription: {
SubMan: &osbuild.RHSMStageOptionsSubMan{
Rhsmcertd: &osbuild.SubManConfigRHSMCERTDSection{
AutoRegistration: common.ToPtr(true),
},
// do not disable the redhat.repo management if the user
// explicitly request the system to be subscribed
},
},
}
}
return ic
}
func defaultGceRhuiImageConfig(rhsm bool) *distro.ImageConfig {
func defaultGceRhuiImageConfig() *distro.ImageConfig {
ic := &distro.ImageConfig{
RHSMConfig: map[subscription.RHSMStatus]*osbuild.RHSMStageOptions{
subscription.RHSMConfigNoSubscription: {
@ -208,7 +183,7 @@ func defaultGceRhuiImageConfig(rhsm bool) *distro.ImageConfig {
},
},
}
return ic.InheritFrom(baseGCEImageConfig(rhsm))
return ic.InheritFrom(baseGCEImageConfig())
}
func gceCommonPackageSet(t *imageType) rpmmd.PackageSet {

View file

@ -390,6 +390,16 @@ func edgeInstallerImage(workload workload.Workload,
img.Users = users.UsersFromBP(customizations.GetUsers())
img.Groups = users.GroupsFromBP(customizations.GetGroups())
img.Language, img.Keyboard = customizations.GetPrimaryLocale()
// ignore ntp servers - we don't currently support setting these in the
// kickstart though kickstart does support setting them
img.Timezone, _ = customizations.GetTimezoneSettings()
if instCust := customizations.GetInstaller(); instCust != nil {
img.WheelNoPasswd = instCust.WheelSudoNopasswd
img.UnattendedKickstart = instCust.Unattended
}
img.SquashfsCompression = "xz"
img.AdditionalDracutModules = []string{
"nvdimm", // non-volatile DIMM firmware (provides nfit, cuse, and nd_e820)
@ -595,6 +605,11 @@ func imageInstallerImage(workload workload.Workload,
img.AdditionalDrivers = []string{"cuse", "ipmi_devintf", "ipmi_msghandler"}
img.AdditionalAnacondaModules = []string{"org.fedoraproject.Anaconda.Modules.Users"}
if instCust := customizations.GetInstaller(); instCust != nil {
img.WheelNoPasswd = instCust.WheelSudoNopasswd
img.UnattendedKickstart = instCust.Unattended
}
img.SquashfsCompression = "xz"
// put the kickstart file in the root of the iso

View file

@ -167,7 +167,12 @@ func (t *imageType) getPartitionTable(
partitioningMode := options.PartitioningMode
if t.rpmOstree {
// Edge supports only LVM, force it.
// Raw is not supported, return an error if it is requested
// TODO Need a central location for logic like this
if partitioningMode == disk.RawPartitioningMode {
return nil, fmt.Errorf("partitioning mode raw not supported for %s on %s", t.Name(), t.arch.Name())
}
partitioningMode = disk.LVMPartitioningMode
}
@ -317,7 +322,7 @@ func (t *imageType) checkOptions(bp *blueprint.Blueprint, options distro.ImageOp
}
if t.name == "edge-simplified-installer" {
allowed := []string{"InstallationDevice", "FDO", "Ignition", "Kernel", "User", "Group", "FIPS", "Filesystem"}
allowed := []string{"InstallationDevice", "FDO", "Ignition", "Kernel", "User", "Group", "FIPS"}
if err := customizations.CheckAllowed(allowed...); err != nil {
return warnings, fmt.Errorf(distro.UnsupportedCustomizationError, t.name, strings.Join(allowed, ", "))
}
@ -355,7 +360,7 @@ func (t *imageType) checkOptions(bp *blueprint.Blueprint, options distro.ImageOp
}
}
} else if t.name == "edge-installer" {
allowed := []string{"User", "Group", "FIPS"}
allowed := []string{"User", "Group", "FIPS", "Installer", "Timezone", "Locale"}
if err := customizations.CheckAllowed(allowed...); err != nil {
return warnings, fmt.Errorf(distro.UnsupportedCustomizationError, t.name, strings.Join(allowed, ", "))
}
@ -367,7 +372,8 @@ func (t *imageType) checkOptions(bp *blueprint.Blueprint, options distro.ImageOp
if options.OSTree == nil || options.OSTree.URL == "" {
return warnings, fmt.Errorf("%q images require specifying a URL from which to retrieve the OSTree commit", t.name)
}
allowed := []string{"Ignition", "Kernel", "User", "Group", "FIPS", "Filesystem"}
allowed := []string{"Ignition", "Kernel", "User", "Group", "FIPS"}
if err := customizations.CheckAllowed(allowed...); err != nil {
return warnings, fmt.Errorf(distro.UnsupportedCustomizationError, t.name, strings.Join(allowed, ", "))
}
@ -394,14 +400,9 @@ func (t *imageType) checkOptions(bp *blueprint.Blueprint, options distro.ImageOp
}
mountpoints := customizations.GetFilesystems()
if mountpoints != nil && t.rpmOstree && (t.name == "edge-container" || t.name == "edge-commit") {
return warnings, fmt.Errorf("Custom mountpoints are not supported for edge-container and edge-commit")
} else if mountpoints != nil && t.rpmOstree && !(t.name == "edge-container" || t.name == "edge-commit") {
//customization allowed for edge-raw-image,edge-ami,edge-vsphere,edge-simplified-installer
err := blueprint.CheckMountpointsPolicy(mountpoints, policies.OstreeMountpointPolicies)
if err != nil {
return warnings, err
}
if mountpoints != nil && t.rpmOstree {
return warnings, fmt.Errorf("Custom mountpoints are not supported for ostree types")
}
err := blueprint.CheckMountpointsPolicy(mountpoints, policies.MountpointPolicies)
@ -454,5 +455,12 @@ func (t *imageType) checkOptions(bp *blueprint.Blueprint, options distro.ImageOp
warnings = append(warnings, w)
}
if customizations.GetInstaller() != nil {
// only supported by the Anaconda installer
if slices.Index([]string{"image-installer", "edge-installer", "live-installer"}, t.name) == -1 {
return warnings, fmt.Errorf("installer customizations are not supported for %q", t.name)
}
}
return warnings, nil
}

View file

@ -7,10 +7,17 @@ import (
)
func defaultBasePartitionTables(t *imageType) (disk.PartitionTable, bool) {
// RHEL >= 9.3 needs to have a bigger /boot, see RHEL-7999
bootSize := uint64(600) * common.MebiByte
if common.VersionLessThan(t.arch.distro.osVersion, "9.3") && t.arch.distro.isRHEL() {
var bootSize uint64
switch {
case common.VersionLessThan(t.arch.distro.osVersion, "9.3") && t.arch.distro.isRHEL():
// RHEL <= 9.2 had only 500 MiB /boot
bootSize = 500 * common.MebiByte
case common.VersionLessThan(t.arch.distro.osVersion, "9.4") && t.arch.distro.isRHEL():
// RHEL 9.3 had 600 MiB /boot, see RHEL-7999
bootSize = 600 * common.MebiByte
default:
// RHEL >= 9.4 needs to have even a bigger /boot, see COMPOSER-2155
bootSize = 1 * common.GibiByte
}
switch t.platform.GetArch() {

View file

@ -10,6 +10,7 @@ import (
"github.com/osbuild/images/pkg/container"
"github.com/osbuild/images/pkg/customizations/users"
"github.com/osbuild/images/pkg/manifest"
"github.com/osbuild/images/pkg/osbuild"
"github.com/osbuild/images/pkg/platform"
"github.com/osbuild/images/pkg/rpmmd"
"github.com/osbuild/images/pkg/runner"
@ -98,6 +99,8 @@ func (img *AnacondaContainerInstaller) InstantiateManifest(m *manifest.Manifest,
bootTreePipeline.Platform = img.Platform
bootTreePipeline.UEFIVendor = img.Platform.GetUEFIVendor()
bootTreePipeline.ISOLabel = isoLabel
kspath := osbuild.KickstartPathOSBuild
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")

View file

@ -9,6 +9,7 @@ import (
"github.com/osbuild/images/pkg/artifact"
"github.com/osbuild/images/pkg/customizations/users"
"github.com/osbuild/images/pkg/manifest"
"github.com/osbuild/images/pkg/osbuild"
"github.com/osbuild/images/pkg/ostree"
"github.com/osbuild/images/pkg/platform"
"github.com/osbuild/images/pkg/rpmmd"
@ -22,6 +23,16 @@ type AnacondaOSTreeInstaller struct {
Users []users.User
Groups []users.Group
Language *string
Keyboard *string
Timezone *string
// Create a sudoers drop-in file for wheel group with NOPASSWD option
WheelNoPasswd bool
// Add kickstart options to make the installation fully unattended
UnattendedKickstart bool
SquashfsCompression string
ISOLabelTempl string
@ -93,6 +104,8 @@ func (img *AnacondaOSTreeInstaller) InstantiateManifest(m *manifest.Manifest,
bootTreePipeline.Platform = img.Platform
bootTreePipeline.UEFIVendor = img.Platform.GetUEFIVendor()
bootTreePipeline.ISOLabel = isoLabel
kspath := osbuild.KickstartPathOSBuild
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")
@ -108,8 +121,12 @@ func (img *AnacondaOSTreeInstaller) InstantiateManifest(m *manifest.Manifest,
isoTreePipeline.Remote = img.Remote
isoTreePipeline.Users = img.Users
isoTreePipeline.Groups = img.Groups
isoTreePipeline.WheelNoPasswd = img.WheelNoPasswd
isoTreePipeline.UnattendedKickstart = img.UnattendedKickstart
isoTreePipeline.SquashfsCompression = img.SquashfsCompression
isoTreePipeline.Language = img.Language
isoTreePipeline.Keyboard = img.Keyboard
isoTreePipeline.Timezone = img.Timezone
// For ostree installers, always put the kickstart file in the root of the ISO
isoTreePipeline.KSPath = kspath

View file

@ -13,13 +13,12 @@ import (
"github.com/osbuild/images/pkg/customizations/users"
"github.com/osbuild/images/pkg/disk"
"github.com/osbuild/images/pkg/manifest"
"github.com/osbuild/images/pkg/osbuild"
"github.com/osbuild/images/pkg/platform"
"github.com/osbuild/images/pkg/rpmmd"
"github.com/osbuild/images/pkg/runner"
)
const kspath = "/osbuild.ks"
func efiBootPartitionTable(rng *rand.Rand) *disk.PartitionTable {
var efibootImageSize uint64 = 20 * common.MebiByte
return &disk.PartitionTable{
@ -49,11 +48,22 @@ type AnacondaTarInstaller struct {
Users []users.User
Groups []users.Group
// If set, the kickstart file will be added to the bootiso-tree as
// /osbuild.ks, otherwise any kickstart options will be configured in the
// default /usr/share/anaconda/interactive-defaults.ks in the rootfs.
// If set, the kickstart file will be added to the bootiso-tree at the
// default path for osbuild, otherwise any kickstart options will be
// configured in the default location for interactive defaults in the
// rootfs. Enabling UnattendedKickstart automatically enables this option
// because automatic installations cannot be configured using interactive
// defaults.
ISORootKickstart bool
// Create a sudoers drop-in file for wheel group with NOPASSWD option
WheelNoPasswd bool
// Add kickstart options to make the installation fully unattended.
// Enabling this option also automatically enables the ISORootKickstart
// option.
UnattendedKickstart bool
SquashfsCompression string
ISOLabelTempl string
@ -84,6 +94,12 @@ func (img *AnacondaTarInstaller) InstantiateManifest(m *manifest.Manifest,
buildPipeline := manifest.NewBuild(m, runner, repos, nil)
buildPipeline.Checkpoint()
if img.UnattendedKickstart {
// if we're building an unattended installer, override the
// ISORootKickstart option
img.ISORootKickstart = true
}
anacondaPipeline := manifest.NewAnacondaInstaller(
manifest.AnacondaInstallerTypePayload,
buildPipeline,
@ -131,6 +147,7 @@ func (img *AnacondaTarInstaller) InstantiateManifest(m *manifest.Manifest,
bootTreePipeline.UEFIVendor = img.Platform.GetUEFIVendor()
bootTreePipeline.ISOLabel = isoLabel
kspath := osbuild.KickstartPathOSBuild
kernelOpts := []string{fmt.Sprintf("inst.stage2=hd:LABEL=%s", isoLabel)}
if img.ISORootKickstart {
kernelOpts = append(kernelOpts, fmt.Sprintf("inst.ks=hd:LABEL=%s:%s", isoLabel, kspath))
@ -150,16 +167,27 @@ func (img *AnacondaTarInstaller) InstantiateManifest(m *manifest.Manifest,
isoLinuxEnabled := img.Platform.GetArch() == arch.ARCH_X86_64
isoTreePipeline := manifest.NewAnacondaInstallerISOTree(buildPipeline, anacondaPipeline, rootfsImagePipeline, bootTreePipeline)
// TODO: the partition table is required - make it a ctor arg or set a default one in the pipeline
isoTreePipeline.PartitionTable = efiBootPartitionTable(rng)
isoTreePipeline.Release = img.Release
isoTreePipeline.OSName = img.OSName
isoTreePipeline.Users = img.Users
isoTreePipeline.Groups = img.Groups
isoTreePipeline.Keyboard = img.OSCustomizations.Keyboard
if img.OSCustomizations.Language != "" {
isoTreePipeline.Language = &img.OSCustomizations.Language
}
if img.OSCustomizations.Timezone != "" {
isoTreePipeline.Timezone = &img.OSCustomizations.Timezone
}
isoTreePipeline.PayloadPath = tarPath
if img.ISORootKickstart {
isoTreePipeline.KSPath = kspath
}
isoTreePipeline.WheelNoPasswd = img.WheelNoPasswd
isoTreePipeline.UnattendedKickstart = img.UnattendedKickstart
isoTreePipeline.SquashfsCompression = img.SquashfsCompression
isoTreePipeline.OSPipeline = osPipeline

View file

@ -3,6 +3,8 @@ package image
import (
"fmt"
"math/rand"
"path/filepath"
"strings"
"github.com/osbuild/images/internal/common"
"github.com/osbuild/images/internal/environment"
@ -87,8 +89,14 @@ func (img *DiskImage) InstantiateManifest(m *manifest.Manifest,
ovfPipeline := manifest.NewOVF(buildPipeline, vmdkPipeline)
tarPipeline := manifest.NewTar(buildPipeline, ovfPipeline, "archive")
tarPipeline.Format = osbuild.TarArchiveFormatUstar
tarPipeline.RootNode = osbuild.TarRootNodeOmit
tarPipeline.SetFilename(img.Filename)
extLess := strings.TrimSuffix(img.Filename, filepath.Ext(img.Filename))
// The .ovf descriptor needs to be the first file in the archive
tarPipeline.Paths = []string{
fmt.Sprintf("%s.ovf", extLess),
fmt.Sprintf("%s.mf", extLess),
fmt.Sprintf("%s.vmdk", extLess),
}
imagePipeline = tarPipeline
case platform.FORMAT_GCE:
// NOTE(akoutsou): temporary workaround; filename required for GCP

View file

@ -63,6 +63,7 @@ func (img *OSTreeArchive) InstantiateManifest(m *manifest.Manifest,
var artifact *artifact.Artifact
if img.BootContainer {
osPipeline.Bootupd = true
encapsulatePipeline := manifest.NewOSTreeEncapsulate(buildPipeline, ostreeCommitPipeline, "ostree-encapsulate")
encapsulatePipeline.SetFilename(img.Filename)
artifact = encapsulatePipeline.Export()

View file

@ -4,6 +4,7 @@ import (
"fmt"
"os"
"github.com/osbuild/images/internal/common"
"github.com/osbuild/images/pkg/arch"
"github.com/osbuild/images/pkg/container"
"github.com/osbuild/images/pkg/customizations/fsnode"
@ -188,26 +189,17 @@ func (p *AnacondaInstaller) serializeEnd() {
p.packageSpecs = nil
}
func installerRootUser() osbuild.UsersStageOptionsUser {
return osbuild.UsersStageOptionsUser{
Password: common.ToPtr(""),
}
}
func (p *AnacondaInstaller) serialize() osbuild.Pipeline {
if len(p.packageSpecs) == 0 {
panic("serialization not started")
}
// Let's do a bunch of sanity checks that are dependent on the installer type
// being serialized
if p.Type == AnacondaInstallerTypeLive {
if len(p.Users) != 0 || len(p.Groups) != 0 {
panic("anaconda installer type payload does not support users and groups customization")
}
if p.InteractiveDefaults != nil {
panic("anaconda installer type payload does not support interactive defaults")
}
} else if p.Type == AnacondaInstallerTypePayload {
} else {
panic("invalid anaconda installer type")
}
pipeline := p.Base.serialize()
pipeline.AddStage(osbuild.NewRPMStage(osbuild.NewRPMStageOptions(p.repos), osbuild.NewRpmStageSourceFilesInputs(p.packageSpecs)))
@ -220,130 +212,143 @@ func (p *AnacondaInstaller) serialize() osbuild.Pipeline {
}))
pipeline.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: "en_US.UTF-8"}))
rootPassword := ""
rootUser := osbuild.UsersStageOptionsUser{
Password: &rootPassword,
}
var usersStageOptions *osbuild.UsersStageOptions
if p.Type == AnacondaInstallerTypePayload {
installUID := 0
installGID := 0
installHome := "/root"
installShell := "/usr/libexec/anaconda/run-anaconda"
installPassword := ""
installUser := osbuild.UsersStageOptionsUser{
UID: &installUID,
GID: &installGID,
Home: &installHome,
Shell: &installShell,
Password: &installPassword,
// Let's do a bunch of sanity checks that are dependent on the installer type
// being serialized
switch p.Type {
case AnacondaInstallerTypeLive:
if len(p.Users) != 0 || len(p.Groups) != 0 {
panic("anaconda installer type live does not support users and groups customization")
}
usersStageOptions = &osbuild.UsersStageOptions{
Users: map[string]osbuild.UsersStageOptionsUser{
"root": rootUser,
"install": installUser,
},
if p.InteractiveDefaults != nil {
panic("anaconda installer type live does not support interactive defaults")
}
} else if p.Type == AnacondaInstallerTypeLive {
usersStageOptions = &osbuild.UsersStageOptions{
Users: map[string]osbuild.UsersStageOptionsUser{
"root": rootUser,
},
}
}
pipeline.AddStage(osbuild.NewUsersStage(usersStageOptions))
if p.Type == AnacondaInstallerTypeLive {
systemdStageOptions := &osbuild.SystemdStageOptions{
EnabledServices: []string{
"livesys.service",
"livesys-late.service",
},
}
pipeline.AddStage(osbuild.NewSystemdStage(systemdStageOptions))
livesysMode := os.FileMode(int(0644))
livesysFile, err := fsnode.NewFile("/etc/sysconfig/livesys", &livesysMode, "root", "root", []byte("livesys_session=\"gnome\""))
if err != nil {
panic(err)
}
p.Files = []*fsnode.File{livesysFile}
pipeline.AddStages(osbuild.GenFileNodesStages(p.Files)...)
}
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: LoraxPath,
BaseArch: p.platform.GetArch().String(),
}))
}
var dracutModules []string
if p.Type == AnacondaInstallerTypePayload {
dracutModules = append(
p.AdditionalDracutModules,
"anaconda",
"rdma",
"rngd",
"multipath",
"fcoe",
"fcoe-uefi",
"iscsi",
"lunmask",
"nfs",
)
} else if p.Type == AnacondaInstallerTypeLive {
dracutModules = append(
p.AdditionalDracutModules,
"anaconda",
"rdma",
"rngd",
)
} else {
pipeline.AddStages(p.liveStages()...)
case AnacondaInstallerTypePayload:
pipeline.AddStages(p.payloadStages()...)
default:
panic("invalid anaconda installer type")
}
dracutOptions := dracutStageOptions(p.kernelVer, p.Biosdevname, dracutModules)
dracutOptions.AddDrivers = p.AdditionalDrivers
pipeline.AddStage(osbuild.NewDracutStage(dracutOptions))
pipeline.AddStage(osbuild.NewSELinuxConfigStage(&osbuild.SELinuxConfigStageOptions{State: osbuild.SELinuxStatePermissive}))
return pipeline
}
if p.Type == AnacondaInstallerTypePayload {
if p.InteractiveDefaults != nil {
kickstartOptions, err := osbuild.NewKickstartStageOptionsWithLiveIMG(
"/usr/share/anaconda/interactive-defaults.ks",
p.Users,
p.Groups,
p.InteractiveDefaults.TarPath,
)
func (p *AnacondaInstaller) payloadStages() []*osbuild.Stage {
stages := make([]*osbuild.Stage, 0)
if err != nil {
panic("failed to create kickstartstage options for interactive defaults")
}
pipeline.AddStage(osbuild.NewKickstartStage(kickstartOptions))
}
installUID := 0
installGID := 0
installHome := "/root"
installShell := "/usr/libexec/anaconda/run-anaconda"
installPassword := ""
installUser := osbuild.UsersStageOptionsUser{
UID: &installUID,
GID: &installGID,
Home: &installHome,
Shell: &installShell,
Password: &installPassword,
}
return pipeline
usersStageOptions := &osbuild.UsersStageOptions{
Users: map[string]osbuild.UsersStageOptionsUser{
"root": installerRootUser(),
"install": installUser,
},
}
stages = append(stages, osbuild.NewUsersStage(usersStageOptions))
var LoraxPath string
if p.UseRHELLoraxTemplates {
LoraxPath = "80-rhel/runtime-postinstall.tmpl"
} else {
LoraxPath = "99-generic/runtime-postinstall.tmpl"
}
stages = append(stages, osbuild.NewAnacondaStage(osbuild.NewAnacondaStageOptions(p.AdditionalAnacondaModules)))
stages = append(stages, osbuild.NewLoraxScriptStage(&osbuild.LoraxScriptStageOptions{
Path: LoraxPath,
BaseArch: p.platform.GetArch().String(),
}))
dracutModules := append(
p.AdditionalDracutModules,
"anaconda",
"rdma",
"rngd",
"multipath",
"fcoe",
"fcoe-uefi",
"iscsi",
"lunmask",
"nfs",
)
dracutOptions := dracutStageOptions(p.kernelVer, p.Biosdevname, dracutModules)
dracutOptions.AddDrivers = p.AdditionalDrivers
stages = append(stages, osbuild.NewDracutStage(dracutOptions))
stages = append(stages, osbuild.NewSELinuxConfigStage(&osbuild.SELinuxConfigStageOptions{State: osbuild.SELinuxStatePermissive}))
if p.InteractiveDefaults != nil {
kickstartOptions, err := osbuild.NewKickstartStageOptionsWithLiveIMG(
osbuild.KickstartPathInteractiveDefaults,
p.Users,
p.Groups,
p.InteractiveDefaults.TarPath,
)
if err != nil {
panic(fmt.Sprintf("failed to create kickstart stage options for interactive defaults: %v", err))
}
stages = append(stages, osbuild.NewKickstartStage(kickstartOptions))
}
return stages
}
func (p *AnacondaInstaller) liveStages() []*osbuild.Stage {
stages := make([]*osbuild.Stage, 0)
usersStageOptions := &osbuild.UsersStageOptions{
Users: map[string]osbuild.UsersStageOptionsUser{
"root": installerRootUser(),
},
}
stages = append(stages, osbuild.NewUsersStage(usersStageOptions))
systemdStageOptions := &osbuild.SystemdStageOptions{
EnabledServices: []string{
"livesys.service",
"livesys-late.service",
},
}
stages = append(stages, osbuild.NewSystemdStage(systemdStageOptions))
livesysMode := os.FileMode(int(0644))
livesysFile, err := fsnode.NewFile("/etc/sysconfig/livesys", &livesysMode, "root", "root", []byte("livesys_session=\"gnome\""))
if err != nil {
panic(err)
}
p.Files = []*fsnode.File{livesysFile}
stages = append(stages, osbuild.GenFileNodesStages(p.Files)...)
dracutModules := append(
p.AdditionalDracutModules,
"anaconda",
"rdma",
"rngd",
)
dracutOptions := dracutStageOptions(p.kernelVer, p.Biosdevname, dracutModules)
dracutOptions.AddDrivers = p.AdditionalDrivers
stages = append(stages, osbuild.NewDracutStage(dracutOptions))
stages = append(stages, osbuild.NewSELinuxConfigStage(&osbuild.SELinuxConfigStageOptions{State: osbuild.SELinuxStatePermissive}))
return stages
}
func dracutStageOptions(kernelVer string, biosdevname bool, additionalModules []string) *osbuild.DracutStageOptions {

View file

@ -4,6 +4,7 @@ import (
"fmt"
"path"
"github.com/osbuild/images/internal/common"
"github.com/osbuild/images/pkg/container"
"github.com/osbuild/images/pkg/customizations/fsnode"
"github.com/osbuild/images/pkg/customizations/users"
@ -27,6 +28,16 @@ type AnacondaInstallerISOTree struct {
Users []users.User
Groups []users.Group
Language *string
Keyboard *string
Timezone *string
// Create a sudoers drop-in file for wheel group with NOPASSWD option
WheelNoPasswd bool
// Add kickstart options to make the installation fully unattended
UnattendedKickstart bool
PartitionTable *disk.PartitionTable
anacondaPipeline *AnacondaInstaller
@ -322,112 +333,19 @@ func (p *AnacondaInstallerISOTree) serialize() osbuild.Pipeline {
copyInputs,
))
if p.ostreeCommitSpec != nil {
// Set up the payload ostree repo
pipeline.AddStage(osbuild.NewOSTreeInitStage(&osbuild.OSTreeInitStageOptions{Path: p.PayloadPath}))
pipeline.AddStage(osbuild.NewOSTreePullStage(
&osbuild.OSTreePullStageOptions{Repo: p.PayloadPath},
osbuild.NewOstreePullStageInputs("org.osbuild.source", p.ostreeCommitSpec.Checksum, p.ostreeCommitSpec.Ref),
))
// Configure the kickstart file with the payload and any user options
kickstartOptions, err := osbuild.NewKickstartStageOptionsWithOSTreeCommit(
p.KSPath,
p.Users,
p.Groups,
makeISORootPath(p.PayloadPath),
p.ostreeCommitSpec.Ref,
p.Remote,
p.OSName)
if err != nil {
panic("failed to create kickstartstage options")
}
pipeline.AddStage(osbuild.NewKickstartStage(kickstartOptions))
}
if p.containerSpec != nil {
images := osbuild.NewContainersInputForSources([]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,
nil))
// do what we can in our kickstart stage
kickstartOptions, err := osbuild.NewKickstartStageOptionsWithOSTreeContainer(
"/osbuild-base.ks",
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))
// and what we can't do in a separate kickstart that we include
kickstartFile, err := fsnode.NewFile(p.KSPath, nil, nil, nil, []byte(`
%include /run/install/repo/osbuild-base.ks
rootpw --lock
lang en_US.UTF-8
keyboard us
timezone UTC
clearpart --all
reqpart --add-boot
part swap --fstype=swap --size=1024
part / --fstype=ext4 --grow
reboot --eject
`))
if err != nil {
panic(err)
}
p.Files = []*fsnode.File{kickstartFile}
pipeline.AddStages(osbuild.GenFileNodesStages(p.Files)...)
}
if p.OSPipeline != nil {
// Create the payload tarball
pipeline.AddStage(osbuild.NewTarStage(&osbuild.TarStageOptions{Filename: p.PayloadPath}, p.OSPipeline.name))
// If the KSPath is set, we need to add the kickstart stage to this (bootiso-tree) pipeline.
// If it's not specified here, it should have been added to the InteractiveDefaults in the anaconda-tree.
if p.KSPath != "" {
kickstartOptions, err := osbuild.NewKickstartStageOptionsWithLiveIMG(
p.KSPath,
p.Users,
p.Groups,
makeISORootPath(p.PayloadPath))
if err != nil {
panic("failed to create kickstartstage options")
}
pipeline.AddStage(osbuild.NewKickstartStage(kickstartOptions))
if p.anacondaPipeline.Type == AnacondaInstallerTypePayload {
// the following pipelines are only relevant for payload installers
switch {
case p.ostreeCommitSpec != nil:
pipeline.AddStages(p.ostreeCommitStages()...)
case p.containerSpec != nil:
pipeline.AddStages(p.ostreeContainerStages()...)
case p.OSPipeline != nil:
pipeline.AddStages(p.tarPayloadStages()...)
default:
// this should have been caught at the top of the function, but
// let's check again in case we refactor the function.
panic("missing ostree, container, or ospipeline parameters in ISO tree pipeline")
}
}
@ -439,6 +357,210 @@ reboot --eject
return pipeline
}
func (p *AnacondaInstallerISOTree) ostreeCommitStages() []*osbuild.Stage {
stages := make([]*osbuild.Stage, 0)
// Set up the payload ostree repo
stages = append(stages, osbuild.NewOSTreeInitStage(&osbuild.OSTreeInitStageOptions{Path: p.PayloadPath}))
stages = append(stages, osbuild.NewOSTreePullStage(
&osbuild.OSTreePullStageOptions{Repo: p.PayloadPath},
osbuild.NewOstreePullStageInputs("org.osbuild.source", p.ostreeCommitSpec.Checksum, p.ostreeCommitSpec.Ref),
))
// Configure the kickstart file with the payload and any user options
kickstartOptions, err := osbuild.NewKickstartStageOptionsWithOSTreeCommit(
p.KSPath,
p.Users,
p.Groups,
makeISORootPath(p.PayloadPath),
p.ostreeCommitSpec.Ref,
p.Remote,
p.OSName)
if err != nil {
panic(fmt.Sprintf("failed to create kickstart stage options: %v", err))
}
stages = append(stages, p.makeKickstartStages(kickstartOptions)...)
return stages
}
func (p *AnacondaInstallerISOTree) ostreeContainerStages() []*osbuild.Stage {
stages := make([]*osbuild.Stage, 0)
images := osbuild.NewContainersInputForSources([]container.Spec{*p.containerSpec})
stages = append(stages, osbuild.NewMkdirStage(&osbuild.MkdirStageOptions{
Paths: []osbuild.MkdirStagePath{
{
Path: p.PayloadPath,
},
},
}))
// copy the container in
stages = append(stages, osbuild.NewSkopeoStageWithOCI(
p.PayloadPath,
images,
nil))
// do what we can in our kickstart stage
kickstartOptions, err := osbuild.NewKickstartStageOptionsWithOSTreeContainer(
p.KSPath,
p.Users,
p.Groups,
path.Join("/run/install/repo", p.PayloadPath),
"oci",
"",
"")
if err != nil {
panic(fmt.Sprintf("failed to create kickstart stage options: %v", err))
}
// NOTE: these are similar to the unattended kickstart options in the
// other two payload configurations but partitioning is different and
// we need to add that separately, so we can't use makeKickstartStage
kickstartOptions.RootPassword = &osbuild.RootPasswordOptions{
Lock: true,
}
// NOTE: These were decided somewhat arbitrarily for the BIB installer. We
// might want to drop them here and move them into the bib code as
// project-specific defaults.
kickstartOptions.Lang = "en_US.UTF-8"
kickstartOptions.Keyboard = "us"
kickstartOptions.Timezone = "UTC"
kickstartOptions.ClearPart = &osbuild.ClearPartOptions{
All: true,
}
stages = append(stages, osbuild.NewKickstartStage(kickstartOptions))
// and what we can't do in a separate kickstart that we include
targetContainerTransport := "registry"
if p.containerSpec.ContainersTransport != nil {
targetContainerTransport = *p.containerSpec.ContainersTransport
}
// Canonicalize to registry, as that's what the bootc stack wants
if targetContainerTransport == "docker://" {
targetContainerTransport = "registry"
}
// Because osbuild core only supports a subset of options, we append to the
// base here with some more hardcoded defaults
// that should very likely become configurable.
hardcodedKickstartBits := `
reqpart --add-boot
part swap --fstype=swap --size=1024
part / --fstype=ext4 --grow
reboot --eject
`
// Workaround for lack of --target-imgref in Anaconda, xref https://github.com/osbuild/images/issues/380
hardcodedKickstartBits += fmt.Sprintf(`%%post
bootc switch --mutate-in-place --transport %s %s
%%end
`, targetContainerTransport, p.containerSpec.LocalName)
kickstartFile, err := kickstartOptions.IncludeRaw(hardcodedKickstartBits)
if err != nil {
panic(err)
}
p.Files = []*fsnode.File{kickstartFile}
stages = append(stages, osbuild.GenFileNodesStages(p.Files)...)
return stages
}
func (p *AnacondaInstallerISOTree) tarPayloadStages() []*osbuild.Stage {
stages := make([]*osbuild.Stage, 0)
// Create the payload tarball
stages = append(stages, osbuild.NewTarStage(&osbuild.TarStageOptions{Filename: p.PayloadPath}, p.OSPipeline.name))
// If the KSPath is set, we need to add the kickstart stage to this (bootiso-tree) pipeline.
// If it's not specified here, it should have been added to the InteractiveDefaults in the anaconda-tree.
if p.KSPath != "" {
kickstartOptions, err := osbuild.NewKickstartStageOptionsWithLiveIMG(
p.KSPath,
p.Users,
p.Groups,
makeISORootPath(p.PayloadPath))
if err != nil {
panic(fmt.Sprintf("failed to create kickstart stage options: %v", err))
}
stages = append(stages, p.makeKickstartStages(kickstartOptions)...)
}
return stages
}
// Create the base kickstart stage with any options required for unattended
// installation if set and with any extra file insertion stage required for
// extra kickstart content.
func (p *AnacondaInstallerISOTree) makeKickstartStages(kickstartOptions *osbuild.KickstartStageOptions) []*osbuild.Stage {
stages := make([]*osbuild.Stage, 0)
if p.UnattendedKickstart {
// set the default options for Unattended kickstart
kickstartOptions.DisplayMode = "text"
// override options that can be configured by the image type or the user
kickstartOptions.Lang = "en_US.UTF-8"
if p.Language != nil {
kickstartOptions.Lang = *p.Language
}
kickstartOptions.Keyboard = "us"
if p.Keyboard != nil {
kickstartOptions.Keyboard = *p.Keyboard
}
kickstartOptions.Timezone = "UTC"
if p.Timezone != nil {
kickstartOptions.Timezone = *p.Timezone
}
kickstartOptions.Reboot = &osbuild.RebootOptions{Eject: true}
kickstartOptions.RootPassword = &osbuild.RootPasswordOptions{Lock: true}
kickstartOptions.ZeroMBR = true
kickstartOptions.ClearPart = &osbuild.ClearPartOptions{All: true, InitLabel: true}
kickstartOptions.AutoPart = &osbuild.AutoPartOptions{Type: "plain", FSType: "xfs", NoHome: true}
kickstartOptions.Network = []osbuild.NetworkOptions{
{BootProto: "dhcp", Device: "link", Activate: common.ToPtr(true), OnBoot: "on"},
}
}
stages = append(stages, osbuild.NewKickstartStage(kickstartOptions))
if p.WheelNoPasswd {
// Because osbuild core only supports a subset of options,
// we append to the base here with hardcoded wheel group with NOPASSWD option
hardcodedKickstartBits := `
%post
echo -e "%wheel\tALL=(ALL)\tNOPASSWD: ALL" > "/etc/sudoers.d/wheel"
chmod 0440 /etc/sudoers.d/wheel
restorecon -rvF /etc/sudoers.d
%end
`
kickstartFile, err := kickstartOptions.IncludeRaw(hardcodedKickstartBits)
if err != nil {
panic(err)
}
p.Files = []*fsnode.File{kickstartFile}
stages = append(stages, osbuild.GenFileNodesStages(p.Files)...)
}
return stages
}
// makeISORootPath return a path that can be used to address files and folders
// in the root of the iso
func makeISORootPath(p string) string {

View file

@ -152,6 +152,12 @@ type OS struct {
// OSTreeParent source spec (optional). If nil the new commit (if
// applicable) will have no parent
OSTreeParent *ostree.SourceSpec
// Enabling Bootupd runs bootupctl generate-update-metadata in the tree to
// transform /usr/lib/ostree-boot into a bootupd-compatible update
// payload. Only works with ostree-based images.
Bootupd bool
// Partition table, if nil the tree cannot be put on a partitioned disk
PartitionTable *disk.PartitionTable
@ -605,7 +611,6 @@ func (p *OS) serialize() osbuild.Pipeline {
Kernel: []string{p.kernelVer},
AddModules: []string{"fips"},
}))
p.Files = append(p.Files, osbuild.GenFIPSFiles()...)
}
if !p.KernelOptionsBootloader {
@ -727,6 +732,7 @@ func (p *OS) serialize() osbuild.Pipeline {
}
if p.FIPS {
p.Files = append(p.Files, osbuild.GenFIPSFiles()...)
for _, stage := range osbuild.GenFIPSStages() {
pipeline.AddStage(stage)
}
@ -768,6 +774,13 @@ func (p *OS) serialize() osbuild.Pipeline {
"wheel", "docker",
},
}))
if p.Bootupd {
pipeline.AddStage(osbuild.NewBootupdGenMetadataStage())
}
} else {
if p.Bootupd {
panic("bootupd is only compatible with ostree-based images, this is a programming error")
}
}
return pipeline

View file

@ -292,7 +292,6 @@ func (p *OSTreeDeployment) serialize() osbuild.Pipeline {
if p.FIPS {
kernelOpts = append(kernelOpts, osbuild.GenFIPSKernelOptions(p.PartitionTable)...)
p.Files = append(p.Files, osbuild.GenFIPSFiles()...)
}
var ref string
@ -408,6 +407,7 @@ func (p *OSTreeDeployment) serialize() osbuild.Pipeline {
}
if p.FIPS {
p.Files = append(p.Files, osbuild.GenFIPSFiles()...)
for _, stage := range osbuild.GenFIPSStages() {
stage.MountOSTree(p.osName, ref, 0)
pipeline.AddStage(stage)

View file

@ -122,7 +122,10 @@ func (p *RawOSTreeImage) serialize() osbuild.Pipeline {
func (p *RawOSTreeImage) addBootupdStage(pipeline *osbuild.Pipeline) {
pt := p.treePipeline.PartitionTable
treeBootupdDevices, treeBootupdMounts := osbuild.GenBootupdDevicesMounts(p.Filename(), pt)
treeBootupdDevices, treeBootupdMounts, err := osbuild.GenBootupdDevicesMounts(p.Filename(), pt)
if err != nil {
panic(err)
}
opts := &osbuild.BootupdStageOptions{
Deployment: &osbuild.OSTreeDeployment{
OSName: p.treePipeline.osName,

View file

@ -12,6 +12,7 @@ type Tar struct {
Format osbuild.TarArchiveFormat
RootNode osbuild.TarRootNode
Paths []string
ACLs *bool
SELinux *bool
Xattrs *bool
@ -50,6 +51,7 @@ func (p *Tar) serialize() osbuild.Pipeline {
SELinux: p.SELinux,
Xattrs: p.Xattrs,
RootNode: p.RootNode,
Paths: p.Paths,
}
tarStage := osbuild.NewTarStage(tarOptions, p.inputPipeline.Name())
pipeline.AddStage(tarStage)

View file

@ -0,0 +1,30 @@
package osbuild
import (
"fmt"
)
// NewBootcInstallToFilesystem creates a new stage for the
// org.osbuild.bootc.install-to-filesystem stage.
//
// It requires a mount setup so that bootupd can be run by bootc. I.e
// "/", "/boot" and "/boot/efi" need to be set up so that
// bootc/bootupd find and install all required bootloader bits.
//
// The mounts input should be generated with GenBootupdDevicesMounts.
func NewBootcInstallToFilesystemStage(inputs ContainersInput, devices map[string]Device, mounts []Mount) (*Stage, error) {
if err := validateBootupdMounts(mounts); err != nil {
return nil, err
}
if len(inputs.References) != 1 {
return nil, fmt.Errorf("expected exactly one container input but got: %v (%v)", len(inputs.References), inputs.References)
}
return &Stage{
Type: "org.osbuild.bootc.install-to-filesystem",
Inputs: inputs,
Devices: devices,
Mounts: mounts,
}, nil
}

View file

@ -0,0 +1,7 @@
package osbuild
func NewBootupdGenMetadataStage() *Stage {
return &Stage{
Type: "org.osbuild.bootupd.gen-metadata",
}
}

View file

@ -76,17 +76,56 @@ func NewBootupdStage(opts *BootupdStageOptions, devices map[string]Device, mount
}, nil
}
func GenBootupdDevicesMounts(filename string, pt *disk.PartitionTable) (map[string]Device, []Mount) {
_, mounts, devices, err := genMountsDevicesFromPt(filename, pt)
if err != nil {
panic(err)
func genMountsForBootupd(source string, pt *disk.PartitionTable) ([]Mount, error) {
mounts := make([]Mount, 0, len(pt.Partitions))
// note that we are not using pt.forEachMountable() here because we
// need to keep track of the partition number (even if it's not
// mountable)
for idx, part := range pt.Partitions {
if part.Payload == nil {
continue
}
// TODO: support things like LVM here via supporting "disk.Container"
mnt, ok := part.Payload.(disk.Mountable)
if !ok {
return nil, fmt.Errorf("type %v not supported by bootupd handling yet", mnt)
}
partNum := idx + 1
name := fmt.Sprintf("part%v", partNum)
mount, err := genOsbuildMount(name, source, mnt)
if err != nil {
return nil, err
}
mount.Partition = &partNum
mounts = append(mounts, *mount)
}
devices["disk"] = Device{
Type: "org.osbuild.loopback",
Options: &LoopbackDeviceOptions{
Filename: filename,
// this must be sorted in so that mounts do not shadow each other
sort.Slice(mounts, func(i, j int) bool {
return mounts[i].Target < mounts[j].Target
})
return mounts, nil
}
func GenBootupdDevicesMounts(filename string, pt *disk.PartitionTable) (map[string]Device, []Mount, error) {
devName := "disk"
devices := map[string]Device{
devName: Device{
Type: "org.osbuild.loopback",
Options: &LoopbackDeviceOptions{
Filename: filename,
Partscan: true,
},
},
}
mounts, err := genMountsForBootupd(devName, pt)
if err != nil {
return nil, nil, err
}
if err := validateBootupdMounts(mounts); err != nil {
return nil, nil, err
}
return devices, mounts
return devices, mounts, nil
}

View file

@ -225,6 +225,23 @@ func pathEscape(path string) string {
return strings.ReplaceAll(path, "/", "-")
}
func genOsbuildMount(name, source string, mnt disk.Mountable) (*Mount, error) {
mountpoint := mnt.GetMountpoint()
t := mnt.GetFSType()
switch t {
case "xfs":
return NewXfsMount(name, source, mountpoint), nil
case "vfat":
return NewFATMount(name, source, mountpoint), nil
case "ext4":
return NewExt4Mount(name, source, mountpoint), nil
case "btrfs":
return NewBtrfsMount(name, source, mountpoint), nil
default:
return nil, fmt.Errorf("unknown fs type " + t)
}
}
func genMountsDevicesFromPt(filename string, pt *disk.PartitionTable) (string, []Mount, map[string]Device, error) {
devices := make(map[string]Device, len(pt.Partitions))
mounts := make([]Mount, 0, len(pt.Partitions))
@ -232,24 +249,13 @@ func genMountsDevicesFromPt(filename string, pt *disk.PartitionTable) (string, [
genMounts := func(mnt disk.Mountable, path []disk.Entity) error {
stageDevices, name := getDevices(path, filename, false)
mountpoint := mnt.GetMountpoint()
if mountpoint == "/" {
fsRootMntName = name
}
var mount *Mount
t := mnt.GetFSType()
switch t {
case "xfs":
mount = NewXfsMount(name, name, mountpoint)
case "vfat":
mount = NewFATMount(name, name, mountpoint)
case "ext4":
mount = NewExt4Mount(name, name, mountpoint)
case "btrfs":
mount = NewBtrfsMount(name, name, mountpoint)
default:
return fmt.Errorf("unknown fs type " + t)
mount, err := genOsbuildMount(name, name, mnt)
if err != nil {
return err
}
mounts = append(mounts, *mount)

View file

@ -36,13 +36,5 @@ func NewGroupsStageOptions(groups []users.Group) *GroupsStageOptions {
}
func GenGroupsStage(groups []users.Group) *Stage {
options := &GroupsStageOptions{
Groups: make(map[string]GroupsStageOptionsGroup, len(groups)),
}
for _, group := range groups {
options.Groups[group.Name] = GroupsStageOptionsGroup{
GID: group.GID,
}
}
return NewGroupsStage(options)
return NewGroupsStage(NewGroupsStageOptions(groups))
}

View file

@ -83,8 +83,7 @@ type GRUB2BIOS struct {
type GRUB2LegacyConfig struct {
GRUB2Config
CmdLine string `json:"cmdline,omitempty"`
Distributor string `json:"distributor,omitempty"`
CmdLine string `json:"cmdline,omitempty"`
}
type GRUB2LegacyStageOptions struct {
@ -145,8 +144,7 @@ func NewGrub2LegacyStageOptions(cfg *GRUB2Config,
RootFS: GRUB2FSDesc{UUID: &rootFsUUID},
Entries: entries,
Config: &GRUB2LegacyConfig{
CmdLine: kopts,
Distributor: "$(sed 's, release .*$,,g' /etc/system-release)",
CmdLine: kopts,
},
}
@ -154,6 +152,13 @@ func NewGrub2LegacyStageOptions(cfg *GRUB2Config,
stageOptions.Config.GRUB2Config = *cfg
}
// NB: previously, the distributor was part of the GRUB2LegacyConfig struct and
// was always set. Now it is part of GRUB2Config, which could override it above.
// Set it here if it is not set.
if stageOptions.Config.Distributor == "" {
stageOptions.Config.Distributor = "$(sed 's, release .*$,,g' /etc/system-release)"
}
bootFs := pt.FindMountable("/boot")
if bootFs != nil {
bootFsUUID := uuid.MustParse(bootFs.GetFSSpec().UUID)

View file

@ -34,12 +34,25 @@ type GRUB2UEFI struct {
Unified bool `json:"unified,omitempty"`
}
type GRUB2ConfigTimeoutStyle string
const (
GRUB2ConfigTimeoutStyleCountdown GRUB2ConfigTimeoutStyle = "countdown"
GRUB2ConfigTimeoutStyleHidden GRUB2ConfigTimeoutStyle = "hidden"
GRUB2ConfigTimeoutStyleMenu GRUB2ConfigTimeoutStyle = "menu"
)
type GRUB2Config struct {
Default string `json:"default,omitempty"`
TerminalInput []string `json:"terminal_input,omitempty"`
TerminalOutput []string `json:"terminal_output,omitempty"`
Timeout int `json:"timeout,omitempty"`
Serial string `json:"serial,omitempty"`
Default string `json:"default,omitempty"`
DisableRecovery *bool `json:"disable_recovery,omitempty"`
DisableSubmenu *bool `json:"disable_submenu,omitempty"`
Distributor string `json:"distributor,omitempty"`
Terminal []string `json:"terminal,omitempty"`
TerminalInput []string `json:"terminal_input,omitempty"`
TerminalOutput []string `json:"terminal_output,omitempty"`
Timeout int `json:"timeout,omitempty"`
TimeoutStyle GRUB2ConfigTimeoutStyle `json:"timeout_style,omitempty"`
Serial string `json:"serial,omitempty"`
}
func (GRUB2StageOptions) isStageOptions() {}

View file

@ -1,6 +1,18 @@
package osbuild
import "github.com/osbuild/images/pkg/customizations/users"
import (
"fmt"
"path/filepath"
"strings"
"github.com/osbuild/images/pkg/customizations/fsnode"
"github.com/osbuild/images/pkg/customizations/users"
)
const (
KickstartPathInteractiveDefaults = "/usr/share/anaconda/interactive-defaults.ks"
KickstartPathOSBuild = "/osbuild.ks"
)
type KickstartStageOptions struct {
// Where to place the kickstart file
@ -14,6 +26,17 @@ type KickstartStageOptions struct {
Users map[string]UsersStageOptionsUser `json:"users,omitempty"`
Groups map[string]GroupsStageOptionsGroup `json:"groups,omitempty"`
Lang string `json:"lang,omitempty"`
Keyboard string `json:"keyboard,omitempty"`
Timezone string `json:"timezone,omitempty"`
DisplayMode string `json:"display_mode,omitempty"`
Reboot *RebootOptions `json:"reboot,omitempty"`
RootPassword *RootPasswordOptions `json:"rootpw,omitempty"`
ZeroMBR bool `json:"zerombr,omitempty"`
ClearPart *ClearPartOptions `json:"clearpart,omitempty"`
AutoPart *AutoPartOptions `json:"autopart,omitempty"`
Network []NetworkOptions `json:"network,omitempty"`
}
type LiveIMGOptions struct {
@ -36,6 +59,60 @@ type OSTreeContainerOptions struct {
SignatureVerification bool `json:"signatureverification"`
}
type RebootOptions struct {
Eject bool `json:"eject,omitempty"`
KExec bool `json:"kexec,omitempty"`
}
type ClearPartOptions struct {
All bool `json:"all,omitempty"`
InitLabel bool `json:"initlabel,omitempty"`
Drives []string `json:"drives,omitempty"`
List []string `json:"list,omitempty"`
Linux bool `json:"linux,omitempty"`
}
type AutoPartOptions struct {
Type string `json:"type,omitempty"`
FSType string `json:"fstype,omitempty"`
NoLVM bool `json:"nolvm,omitempty"`
Encrypted bool `json:"encrypted,omitempty"`
PassPhrase string `json:"passphrase,omitempty"`
EscrowCert string `json:"escrowcert,omitempty"`
BackupPassPhrase bool `json:"backuppassphrase,omitempty"`
Cipher string `json:"cipher,omitempty"`
LuksVersion string `json:"luks-version,omitempty"`
PBKdf string `json:"pbkdf,omitempty"`
PBKdfMemory int `json:"pbkdf-memory,omitempty"`
PBKdfTime int `json:"pbkdf-time,omitempty"`
PBKdfIterations int `json:"pbkdf-iterations,omitempty"`
NoHome bool `json:"nohome,omitempty"`
}
type NetworkOptions struct {
Activate *bool `json:"activate,omitempty"`
BootProto string `json:"bootproto,omitempty"`
Device string `json:"device,omitempty"`
OnBoot string `json:"onboot,omitempty"`
IP string `json:"ip,omitempty"`
IPV6 string `json:"ipv6,omitempty"`
Gateway string `json:"gateway,omitempty"`
IPV6Gateway string `json:"ipv6gateway,omitempty"`
Nameservers []string `json:"nameservers,omitempty"`
Netmask string `json:"netmask,omitempty"`
Hostname string `json:"hostname,omitempty"`
ESSid string `json:"essid,omitempty"`
WPAKey string `json:"wpakey,omitempty"`
}
type RootPasswordOptions struct {
Lock bool `json:"lock,omitempty"`
PlainText bool `json:"plaintext,omitempty"`
IsCrypted bool `json:"iscrypted,omitempty"`
AllowSSH bool `json:"allow_ssh,omitempty"`
Password string `json:"password,omitempty"`
}
func (KickstartStageOptions) isStageOptions() {}
// Creates an Anaconda kickstart file
@ -153,3 +230,26 @@ func NewKickstartStageOptionsWithLiveIMG(
return options, nil
}
// IncludeRaw is used for adding raw text as an extension to the kickstart
// file. First it changes the filename of the existing kickstart stage options
// and then creates a new file with the given raw content and an %include
// statement at the top that points to the renamed file. The new raw content is
// generated in place of the original file and is returned as an fsnode.File.
// The raw content *should not* contain the %include statement.
func (options *KickstartStageOptions) IncludeRaw(raw string) (*fsnode.File, error) {
origPath := options.Path
origName := filepath.Base(origPath)
ext := filepath.Ext(origName)
// file.ext -> file-base.ext
newBaseName := strings.TrimSuffix(origName, ext) + "-base" + ext
options.Path = filepath.Join("/", newBaseName)
// include must point to full path when booted
includePath := filepath.Join("/run/install/repo", newBaseName)
rawBits := fmt.Sprintf("%%include %s\n%s", includePath, raw)
return fsnode.NewFile(origPath, nil, nil, nil, []byte(rawBits))
}

View file

@ -17,6 +17,9 @@ type LoopbackDeviceOptions struct {
// Lock (bsd lock) the device after opening it
Lock bool `json:"lock,omitempty"`
// Enable partition scanning as an option
Partscan bool `json:"partscan,omitempty"`
}
func (LoopbackDeviceOptions) isDeviceOptions() {}

View file

@ -1,11 +1,12 @@
package osbuild
type Mount struct {
Name string `json:"name"`
Type string `json:"type"`
Source string `json:"source,omitempty"`
Target string `json:"target,omitempty"`
Options MountOptions `json:"options,omitempty"`
Name string `json:"name"`
Type string `json:"type"`
Source string `json:"source,omitempty"`
Target string `json:"target,omitempty"`
Options MountOptions `json:"options,omitempty"`
Partition *int `json:"partition,omitempty"`
}
type MountOptions interface {

View file

@ -1,24 +0,0 @@
package osbuild
// The ScriptStageOptions specifies a custom script to run in the image
type ScriptStageOptions struct {
Script string `json:"script"`
}
func (ScriptStageOptions) isStageOptions() {}
// NewScriptStageOptions creates a new script stage options object, with
// the mandatory fields set.
func NewScriptStageOptions(script string) *ScriptStageOptions {
return &ScriptStageOptions{
Script: script,
}
}
// NewScriptStage creates a new Script Stage object.
func NewScriptStage(options *ScriptStageOptions) *Stage {
return &Stage{
Type: "org.osbuild.script",
Options: options,
}
}

View file

@ -39,6 +39,9 @@ type TarStageOptions struct {
// How to handle the root node: include or omit
RootNode TarRootNode `json:"root-node,omitempty"`
// List of paths to include, instead of the whole tree
Paths []string `json:"paths,omitempty"`
}
func (TarStageOptions) isStageOptions() {}
@ -81,6 +84,10 @@ func (o TarStageOptions) validate() error {
}
}
if len(o.Paths) > 0 && o.RootNode != "" {
return fmt.Errorf("'paths' cannot be combined with 'root-node'")
}
return nil
}

View file

@ -71,41 +71,9 @@ func NewUsersStageOptions(userCustomizations []users.User, omitKey bool) (*Users
}
func GenUsersStage(users []users.User, omitKey bool) (*Stage, error) {
options := &UsersStageOptions{
Users: make(map[string]UsersStageOptionsUser, len(users)),
options, err := NewUsersStageOptions(users, omitKey)
if err != nil {
return nil, err
}
for _, user := range users {
// Don't hash empty passwords, set to nil to lock account
if user.Password != nil && len(*user.Password) == 0 {
user.Password = nil
}
// Hash non-empty un-hashed passwords
if user.Password != nil && !crypt.PasswordIsCrypted(*user.Password) {
cryptedPassword, err := crypt.CryptSHA512(*user.Password)
if err != nil {
return nil, err
}
user.Password = &cryptedPassword
}
userOptions := UsersStageOptionsUser{
UID: user.UID,
GID: user.GID,
Groups: user.Groups,
Description: user.Description,
Home: user.Home,
Shell: user.Shell,
Password: user.Password,
Key: nil,
}
if !omitKey {
userOptions.Key = user.Key
}
options.Users[user.Name] = userOptions
}
return NewUsersStage(options), nil
}

View file

@ -9,13 +9,6 @@ type ZiplStageOptions struct {
func (ZiplStageOptions) isStageOptions() {}
// NewZiplStageOptions creates a new ZiplStageOptions object with no timeout
func NewZiplStageOptions() *ZiplStageOptions {
return &ZiplStageOptions{
Timeout: 0,
}
}
// NewZiplStage creates a new zipl Stage object.
func NewZiplStage(options *ZiplStageOptions) *Stage {
return &Stage{