Image Customizer: Add support for kernel command-line (#6881)

This commit is contained in:
Chris Gunn 2023-12-14 16:03:38 -08:00 коммит произвёл GitHub
Родитель f4310527e5
Коммит 4742b8bf0b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
15 изменённых файлов: 369 добавлений и 49 удалений

Просмотреть файл

@ -179,6 +179,21 @@ SystemConfig:
Permissions: "664"
```
## KernelCommandLine type
Options for configuring the kernel.
### ExtraCommandLine
Additional Linux kernel command line options to add to the image.
If the partitions are customized, then the `grub.cfg` file will be reset to handle the
new partition layout.
So, any existing ExtraCommandLine value in the base image will be replaced.
If the partitions are not customized, then the `ExtraCommandLine` value will be appended
to the existing `grub.cfg` file.
## Module type
Options for configuring a kernel module.
@ -461,6 +476,11 @@ SystemConfig:
Hostname: example-image
```
### KernelCommandLine [[KernelCommandLine](#kernelcommandline-type)]
Specifies extra kernel command line options, as well as other configuration values
relating to the kernel.
### UpdateBaseImagePackages [bool]
Updates the packages that exist in the base image.

Просмотреть файл

@ -39,11 +39,16 @@ func (c *Config) IsValid() error {
hasDisks := c.Disks != nil
hasBootType := c.SystemConfig.BootType != BootTypeUnset
hasPartitionSettings := len(c.SystemConfig.PartitionSettings) > 0
if hasDisks != hasBootType {
return fmt.Errorf("SystemConfig.BootType and Disks must be specified together")
}
if hasPartitionSettings && !hasDisks {
return fmt.Errorf("the Disks and SystemConfig.BootType values must also be specified if SystemConfig.PartitionSettings is specified")
}
// Ensure the correct partitions exist to support the specified the boot type.
switch c.SystemConfig.BootType {
case BootTypeEfi:

Просмотреть файл

@ -262,3 +262,72 @@ func TestConfigIsValidInvalidPartitionId(t *testing.T) {
assert.ErrorContains(t, err, "partition")
assert.ErrorContains(t, err, "ID")
}
func TestConfigIsValidPartitionSettingsMissingDisks(t *testing.T) {
config := &Config{
SystemConfig: SystemConfig{
Hostname: "test",
PartitionSettings: []PartitionSetting{
{
ID: "esp",
MountPoint: "/boot/efi",
},
},
},
}
err := config.IsValid()
assert.Error(t, err)
assert.ErrorContains(t, err, "Disks")
assert.ErrorContains(t, err, "BootType")
assert.ErrorContains(t, err, "PartitionSettings")
}
func TestConfigIsValidBootTypeMissingDisks(t *testing.T) {
config := &Config{
SystemConfig: SystemConfig{
Hostname: "test",
BootType: BootTypeEfi,
KernelCommandLine: KernelCommandLine{
ExtraCommandLine: "console=ttyS0",
},
},
}
err := config.IsValid()
assert.Error(t, err)
assert.ErrorContains(t, err, "SystemConfig.BootType and Disks must be specified together")
}
func TestConfigIsValidKernelCLI(t *testing.T) {
config := &Config{
Disks: &[]Disk{{
PartitionTableType: "gpt",
MaxSize: 2,
Partitions: []Partition{
{
ID: "esp",
FsType: "fat32",
Start: 1,
Flags: []PartitionFlag{
"esp",
"boot",
},
},
},
}},
SystemConfig: SystemConfig{
BootType: "efi",
Hostname: "test",
PartitionSettings: []PartitionSetting{
{
ID: "esp",
MountPoint: "/boot/efi",
},
},
KernelCommandLine: KernelCommandLine{
ExtraCommandLine: "console=ttyS0",
},
},
}
err := config.IsValid()
assert.NoError(t, err)
}

Просмотреть файл

@ -0,0 +1,34 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package imagecustomizerapi
import (
"fmt"
"strings"
)
type KernelCommandLine struct {
// Extra kernel command line args.
ExtraCommandLine string `yaml:"ExtraCommandLine"`
}
func (s *KernelCommandLine) IsValid() error {
err := commandLineIsValid(s.ExtraCommandLine, "ExtraCommandLine")
if err != nil {
return err
}
return nil
}
func commandLineIsValid(commandLine string, fieldName string) error {
// Disallow special characters to avoid breaking the grub.cfg file.
// In addition, disallow the "`" character, since it is used as the sed escape character by
// `installutils.setGrubCfgAdditionalCmdLine()`.
if strings.ContainsAny(commandLine, "\n'\"\\$`") {
return fmt.Errorf("the %s value contains invalid characters", fieldName)
}
return nil
}

Просмотреть файл

@ -21,6 +21,7 @@ type SystemConfig struct {
PackagesRemove []string `yaml:"PackagesRemove"`
PackageListsUpdate []string `yaml:"PackageListsUpdate"`
PackagesUpdate []string `yaml:"PackagesUpdate"`
KernelCommandLine KernelCommandLine `yaml:"KernelCommandLine"`
AdditionalFiles map[string]FileConfigList `yaml:"AdditionalFiles"`
PartitionSettings []PartitionSetting `yaml:"PartitionSettings"`
PostInstallScripts []Script `yaml:"PostInstallScripts"`
@ -44,6 +45,11 @@ func (s *SystemConfig) IsValid() error {
}
}
err = s.KernelCommandLine.IsValid()
if err != nil {
return fmt.Errorf("invalid KernelCommandLine: %w", err)
}
for sourcePath, fileConfigList := range s.AdditionalFiles {
err = fileConfigList.IsValid()
if err != nil {

Просмотреть файл

@ -41,3 +41,15 @@ func TestSystemConfigIsValidDuplicatePartitionID(t *testing.T) {
assert.Error(t, err)
assert.ErrorContains(t, err, "duplicate PartitionSettings ID")
}
func TestSystemConfigIsValidKernelCommandLineInvalidChars(t *testing.T) {
value := SystemConfig{
KernelCommandLine: KernelCommandLine{
ExtraCommandLine: "example=\"example\"",
},
}
err := value.IsValid()
assert.Error(t, err)
assert.ErrorContains(t, err, "ExtraCommandLine")
}

Просмотреть файл

@ -0,0 +1,66 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package imagecustomizerlib
import (
"fmt"
"os"
"path/filepath"
"regexp"
"github.com/microsoft/CBL-Mariner/toolkit/tools/internal/logger"
"github.com/microsoft/CBL-Mariner/toolkit/tools/internal/safechroot"
)
var (
linuxCommandLineRegex = regexp.MustCompile(`\tlinux .* (\$kernelopts)`)
)
func handleKernelCommandLine(extraCommandLine string, imageChroot *safechroot.Chroot, partitionsCustomized bool) error {
var err error
if partitionsCustomized {
// ExtraCommandLine was handled when the new image was created and the grub.cfg file was regenerated from
// scatch.
return nil
}
if extraCommandLine == "" {
// Nothing to do.
return nil
}
logger.Log.Infof("Setting KernelCommandLine.ExtraCommandLine")
grub2ConfigFilePath := filepath.Join(imageChroot.RootDir(), "/boot/grub2/grub.cfg")
// Read the existing grub.cfg file.
grub2ConfigFileBytes, err := os.ReadFile(grub2ConfigFilePath)
if err != nil {
return fmt.Errorf("failed to read existing grub2 config file: %w", err)
}
grub2ConfigFile := string(grub2ConfigFileBytes)
// Find the point where the new command line arguments should be added.
match := linuxCommandLineRegex.FindStringSubmatchIndex(grub2ConfigFile)
if match == nil {
return fmt.Errorf("failed to find Linux kernel command line params in grub2 config file")
}
// Get the location of "$kernelopts".
// Note: regexp returns index pairs. So, [2] is the start index of the 1st group.
insertIndex := match[2]
// Insert new command line arguments.
newGrub2ConfigFile := grub2ConfigFile[:insertIndex] + extraCommandLine + " " + grub2ConfigFile[insertIndex:]
// Update grub.cfg file.
err = os.WriteFile(grub2ConfigFilePath, []byte(newGrub2ConfigFile), 0)
if err != nil {
return fmt.Errorf("failed to write new grub2 config file: %w", err)
}
return nil
}

Просмотреть файл

@ -9,7 +9,6 @@ import (
"path/filepath"
"github.com/microsoft/CBL-Mariner/toolkit/tools/imagecustomizerapi"
"github.com/microsoft/CBL-Mariner/toolkit/tools/internal/logger"
"github.com/microsoft/CBL-Mariner/toolkit/tools/internal/safechroot"
"github.com/microsoft/CBL-Mariner/toolkit/tools/internal/shell"
)
@ -30,7 +29,7 @@ func customizePartitionsUsingFileCopy(buildDir string, baseConfigPath string, co
}
newImageConnection, err := createNewImage(newBuildImageFile, diskConfig, config.SystemConfig.PartitionSettings,
config.SystemConfig.BootType, buildDir, "newimageroot", installOSFunc)
config.SystemConfig.BootType, config.SystemConfig.KernelCommandLine, buildDir, "newimageroot", installOSFunc)
if err != nil {
return err
}
@ -82,8 +81,7 @@ func copyFilesIntoNewDiskHelper(existingImageChroot *safechroot.Chroot, newImage
copyArgs = append(copyArgs, fullFileName)
}
err = shell.ExecuteLiveWithCallback(func(...interface{}) {}, logger.Log.Warn, false,
"cp", copyArgs...)
err = shell.ExecuteLiveWithErr(1, "cp", copyArgs...)
if err != nil {
return fmt.Errorf("failed to copy files:\n%w", err)
}

Просмотреть файл

@ -83,6 +83,12 @@ func doCustomizations(buildDir string, baseConfigPath string, config *imagecusto
return err
}
err = handleKernelCommandLine(config.SystemConfig.KernelCommandLine.ExtraCommandLine, imageChroot,
partitionsCustomized)
if err != nil {
return fmt.Errorf("failed to add extra kernel command line: %w", err)
}
err = runScripts(baseConfigPath, config.SystemConfig.FinalizeImageScripts, imageChroot)
if err != nil {
return err

Просмотреть файл

@ -138,9 +138,16 @@ func toQemuImageFormat(imageFormat string) (string, error) {
func validateConfig(baseConfigPath string, config *imagecustomizerapi.Config, rpmsSources []string,
useBaseImageRpmRepos bool,
) error {
// Note: This IsValid() check does duplicate the one in UnmarshalYamlFile().
// But it is useful for functions that call CustomizeImage() directly. For example, test code.
err := config.IsValid()
if err != nil {
return err
}
partitionsCustomized := hasPartitionCustomizations(config)
err := validateSystemConfig(baseConfigPath, &config.SystemConfig, rpmsSources, useBaseImageRpmRepos,
err = validateSystemConfig(baseConfigPath, &config.SystemConfig, rpmsSources, useBaseImageRpmRepos,
partitionsCustomized)
if err != nil {
return err

Просмотреть файл

@ -8,16 +8,20 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"testing"
"github.com/microsoft/CBL-Mariner/toolkit/tools/imagecustomizerapi"
"github.com/microsoft/CBL-Mariner/toolkit/tools/internal/buildpipeline"
"github.com/microsoft/CBL-Mariner/toolkit/tools/internal/ptrutils"
"github.com/microsoft/CBL-Mariner/toolkit/tools/internal/safechroot"
"github.com/microsoft/CBL-Mariner/toolkit/tools/internal/safeloopback"
"github.com/stretchr/testify/assert"
)
const (
testImageRootDirName = "testimageroot"
)
func TestCustomizeImageEmptyConfig(t *testing.T) {
var err error
@ -88,34 +92,43 @@ func TestCustomizeImageCopyFiles(t *testing.T) {
checkFileType(t, outImageFilePath, "raw")
// Mount the output disk image so that its contents can be checked.
loopback, err := safeloopback.NewLoopback(outImageFilePath)
imageConnection, err := reconnectToFakeEfiImage(buildDir, outImageFilePath)
if !assert.NoError(t, err) {
return
}
defer loopback.Close()
defer imageConnection.Close()
// Create partition mount config.
// Note: The assigned loopback device might be different from the one assigned when `createFakeEfiImage` ran.
bootPartitionDevPath := fmt.Sprintf("%sp1", loopback.DevicePath())
osPartitionDevPath := fmt.Sprintf("%sp2", loopback.DevicePath())
// Check the contents of the copied file.
file_contents, err := os.ReadFile(filepath.Join(imageConnection.Chroot().RootDir(), "a.txt"))
assert.NoError(t, err)
assert.Equal(t, "abcdefg\n", string(file_contents))
}
func reconnectToFakeEfiImage(buildDir string, imageFilePath string) (*ImageConnection, error) {
imageConnection := NewImageConnection()
err := imageConnection.ConnectLoopback(imageFilePath)
if err != nil {
imageConnection.Close()
return nil, err
}
rootDir := filepath.Join(buildDir, testImageRootDirName)
bootPartitionDevPath := fmt.Sprintf("%sp1", imageConnection.Loopback().DevicePath())
osPartitionDevPath := fmt.Sprintf("%sp2", imageConnection.Loopback().DevicePath())
newMountDirectories := []string{}
mountPoints := []*safechroot.MountPoint{
safechroot.NewPreDefaultsMountPoint(osPartitionDevPath, "/", "ext4", 0, ""),
safechroot.NewMountPoint(bootPartitionDevPath, "/boot/efi", "vfat", 0, ""),
}
imageChroot := safechroot.NewChroot(filepath.Join(buildDir, "imageroot"), false)
err = imageChroot.Initialize("", newMountDirectories, mountPoints)
if !assert.NoError(t, err) {
return
err = imageConnection.ConnectChroot(rootDir, false, []string{}, mountPoints)
if err != nil {
imageConnection.Close()
return nil, err
}
defer imageChroot.Close(false)
// Check the contents of the copied file.
file_contents, err := os.ReadFile(filepath.Join(imageChroot.RootDir(), "a.txt"))
assert.NoError(t, err)
assert.Equal(t, "abcdefg\n", string(file_contents))
return imageConnection, nil
}
func TestValidateConfigValidAdditionalFiles(t *testing.T) {
@ -189,6 +202,69 @@ func TestValidateConfigScriptNonExecutable(t *testing.T) {
assert.Error(t, err)
}
func TestCustomizeImageKernelCommandLineAdd(t *testing.T) {
var err error
if testing.Short() {
t.Skip("Short mode enabled")
}
if !buildpipeline.IsRegularBuild() {
t.Skip("loopback block device not available")
}
if os.Geteuid() != 0 {
t.Skip("Test must be run as root because it uses a chroot")
}
buildDir := filepath.Join(tmpDir, "TestCustomizeImageKernelCommandLine")
outImageFilePath := filepath.Join(buildDir, "image.vhd")
// Create fake disk.
diskFilePath, err := createFakeEfiImage(buildDir)
if !assert.NoError(t, err) {
return
}
// Customize image.
config := &imagecustomizerapi.Config{
SystemConfig: imagecustomizerapi.SystemConfig{
KernelCommandLine: imagecustomizerapi.KernelCommandLine{
ExtraCommandLine: "console=tty0 console=ttyS0",
},
},
}
err = CustomizeImage(buildDir, buildDir, config, diskFilePath, nil, outImageFilePath, "raw", false)
if !assert.NoError(t, err) {
return
}
// Mount the output disk image so that its contents can be checked.
imageConnection, err := reconnectToFakeEfiImage(buildDir, outImageFilePath)
if !assert.NoError(t, err) {
return
}
defer imageConnection.Close()
// Read the grub.cfg file.
grub2ConfigFilePath := filepath.Join(imageConnection.Chroot().RootDir(), "/boot/grub2/grub.cfg")
grub2ConfigFile, err := os.ReadFile(grub2ConfigFilePath)
if !assert.NoError(t, err) {
return
}
t.Logf("%s", grub2ConfigFile)
linuxCommandLineRegex, err := regexp.Compile(`linux .* console=tty0 console=ttyS0 `)
if !assert.NoError(t, err) {
return
}
assert.True(t, linuxCommandLineRegex.Match(grub2ConfigFile))
}
func createFakeEfiImage(buildDir string) (string, error) {
var err error
@ -241,8 +317,8 @@ func createFakeEfiImage(buildDir string) (string, error) {
return nil
}
imageConnection, err := createNewImage(rawDisk, diskConfig, partitionSettings, "efi", buildDir, "imageroot",
installOS)
imageConnection, err := createNewImage(rawDisk, diskConfig, partitionSettings, "efi",
imagecustomizerapi.KernelCommandLine{}, buildDir, testImageRootDirName, installOS)
if err != nil {
return "", err
}

Просмотреть файл

@ -58,13 +58,14 @@ func connectToExistingImageHelper(imageConnection *ImageConnection, imageFilePat
}
func createNewImage(filename string, diskConfig imagecustomizerapi.Disk,
partitionSettings []imagecustomizerapi.PartitionSetting, bootType imagecustomizerapi.BootType, buildDir string,
chrootDirName string, installOS installOSFunc,
partitionSettings []imagecustomizerapi.PartitionSetting, bootType imagecustomizerapi.BootType,
kernelCommandLine imagecustomizerapi.KernelCommandLine, buildDir string, chrootDirName string,
installOS installOSFunc,
) (*ImageConnection, error) {
imageConnection := &ImageConnection{}
err := createNewImageHelper(imageConnection, filename, diskConfig, partitionSettings, bootType, buildDir,
chrootDirName, installOS,
err := createNewImageHelper(imageConnection, filename, diskConfig, partitionSettings, bootType, kernelCommandLine,
buildDir, chrootDirName, installOS,
)
if err != nil {
imageConnection.Close()
@ -75,8 +76,9 @@ func createNewImage(filename string, diskConfig imagecustomizerapi.Disk,
}
func createNewImageHelper(imageConnection *ImageConnection, filename string, diskConfig imagecustomizerapi.Disk,
partitionSettings []imagecustomizerapi.PartitionSetting, bootType imagecustomizerapi.BootType, buildDir string,
chrootDirName string, installOS installOSFunc,
partitionSettings []imagecustomizerapi.PartitionSetting, bootType imagecustomizerapi.BootType,
kernelCommandLine imagecustomizerapi.KernelCommandLine, buildDir string, chrootDirName string,
installOS installOSFunc,
) error {
// Convert config to image config types, so that the imager's utils can be used.
imagerBootType, err := bootTypeToImager(bootType)
@ -94,13 +96,18 @@ func createNewImageHelper(imageConnection *ImageConnection, filename string, dis
return err
}
imagerKernelCommandLine, err := kernelCommandLineToImager(kernelCommandLine)
if err != nil {
return err
}
// Sort the partitions so that they are mounted in the correct oder.
sort.Slice(imagerPartitionSettings, func(i, j int) bool {
return imagerPartitionSettings[i].MountPoint < imagerPartitionSettings[j].MountPoint
})
// Create imager boilerplate.
mountPointMap, err := createImageBoilerplate(imageConnection, filename, buildDir, chrootDirName, imagerDiskConfig,
mountPointMap, tmpFstabFile, err := createImageBoilerplate(imageConnection, filename, buildDir, chrootDirName, imagerDiskConfig,
imagerPartitionSettings)
if err != nil {
return err
@ -112,9 +119,17 @@ func createNewImageHelper(imageConnection *ImageConnection, filename string, dis
return err
}
// Move the fstab file into the image.
imageFstabFilePath := filepath.Join(imageConnection.Chroot().RootDir(), "etc/fstab")
err = file.Move(tmpFstabFile, imageFstabFilePath)
if err != nil {
return fmt.Errorf("failed to move fstab into new image:\n%w", err)
}
// Configure the boot loader.
err = installutils.ConfigureDiskBootloader(imagerBootType, false, false, imagerPartitionSettings,
configuration.KernelCommandLine{}, imageConnection.Chroot(), imageConnection.Loopback().DevicePath(),
imagerKernelCommandLine, imageConnection.Chroot(), imageConnection.Loopback().DevicePath(),
mountPointMap, diskutils.EncryptedRootDevice{}, diskutils.VerityDevice{})
if err != nil {
return fmt.Errorf("failed to install bootloader:\n%w", err)
@ -125,17 +140,17 @@ func createNewImageHelper(imageConnection *ImageConnection, filename string, dis
func createImageBoilerplate(imageConnection *ImageConnection, filename string, buildDir string, chrootDirName string,
imagerDiskConfig configuration.Disk, imagerPartitionSettings []configuration.PartitionSetting,
) (map[string]string, error) {
) (map[string]string, string, error) {
// Create raw disk image file.
err := diskutils.CreateSparseDisk(filename, imagerDiskConfig.MaxSize, 0o644)
if err != nil {
return nil, fmt.Errorf("failed to create empty disk file (%s):\n%w", filename, err)
return nil, "", fmt.Errorf("failed to create empty disk file (%s):\n%w", filename, err)
}
// Connect raw disk image file.
err = imageConnection.ConnectLoopback(filename)
if err != nil {
return nil, err
return nil, "", err
}
// Set up partitions.
@ -143,13 +158,13 @@ func createImageBoilerplate(imageConnection *ImageConnection, filename string, b
imageConnection.Loopback().DevicePath(), imagerDiskConfig, configuration.RootEncryption{},
configuration.ReadOnlyVerityRoot{})
if err != nil {
return nil, fmt.Errorf("failed to create partitions on disk (%s):\n%w", imageConnection.Loopback().DevicePath(), err)
return nil, "", fmt.Errorf("failed to create partitions on disk (%s):\n%w", imageConnection.Loopback().DevicePath(), err)
}
// Read the disk partitions.
diskPartitions, err := diskutils.GetDiskPartitions(imageConnection.Loopback().DevicePath())
if err != nil {
return nil, err
return nil, "", err
}
// Create the fstab file.
@ -166,13 +181,13 @@ func createImageBoilerplate(imageConnection *ImageConnection, filename string, b
mountPointToMountArgsMap, partIDToDevPathMap, partIDToFsTypeMap, false, /*hidepidEnabled*/
)
if err != nil {
return nil, fmt.Errorf("failed to write temp fstab file:\n%w", err)
return nil, "", fmt.Errorf("failed to write temp fstab file:\n%w", err)
}
// Read back the fstab file.
mountPoints, err := findMountsFromFstabFile(tmpFstabFile, diskPartitions)
if err != nil {
return nil, err
return nil, "", err
}
// Create chroot environment.
@ -180,16 +195,8 @@ func createImageBoilerplate(imageConnection *ImageConnection, filename string, b
err = imageConnection.ConnectChroot(imageChrootDir, false, nil, mountPoints)
if err != nil {
return nil, err
return nil, "", err
}
// Move the fstab file into the image.
imageFstabFilePath := filepath.Join(imageConnection.Chroot().RootDir(), "etc/fstab")
err = file.Move(tmpFstabFile, imageFstabFilePath)
if err != nil {
return nil, fmt.Errorf("failed to move fstab into new image:\n%w", err)
}
return mountPointMap, nil
return mountPointMap, tmpFstabFile, nil
}

Просмотреть файл

@ -0,0 +1,3 @@
SystemConfig:
KernelCommandLine:
ExtraCommandLine: console=tty0 console=ttyS0

Просмотреть файл

@ -39,3 +39,6 @@ SystemConfig:
- ID: var
MountPoint: /var
KernelCommandLine:
ExtraCommandLine: console=tty0 console=ttyS0

Просмотреть файл

@ -161,3 +161,11 @@ func mountIdentifierTypeToImager(mountIdentifierType imagecustomizerapi.MountIde
return "", fmt.Errorf("unknwon MountIdentifierType value (%s)", mountIdentifierType)
}
}
func kernelCommandLineToImager(kernelCommandLine imagecustomizerapi.KernelCommandLine,
) (configuration.KernelCommandLine, error) {
imagerKernelCommandLine := configuration.KernelCommandLine{
ExtraCommandLine: kernelCommandLine.ExtraCommandLine,
}
return imagerKernelCommandLine, nil
}