[MIC ISO generation] Allow adding kernel parameters to the LiveOS iso grub.cfg. (#7744)

This commit is contained in:
George Mileka 2024-02-12 17:28:48 -08:00 коммит произвёл GitHub
Родитель 8474fd4bb1
Коммит 9346d073c8
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
11 изменённых файлов: 363 добавлений и 47 удалений

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

@ -240,6 +240,10 @@ The partitions to provision on the disk.
Specifies the configuration for the generated ISO media.
### KernelExtraCommandLine [string]
- See [ExtraCommandLine](#extracommandline-string).
### AdditionalFiles
- See [AdditionalFiles](#additionalfiles-mapstring-fileconfig).

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

@ -35,8 +35,64 @@ The current implementation for the LiveOS ISO does not support the following:
- disk layout.
- There is always one disk generated when an `iso` output format is
specified.
- SELinux
- No SELinux configuration is supported for the generated ISO image.
## ISO Specific Customizations
## ISO Specific Customizations
- The user can specify one or more files to be copied to the iso media.
See MIC's iso configuration [Config.ISO](./configuration.md#iso-type).
- The user can specify one or more files to be copied to the iso media.
- The user can add kernel parameters.
For a full list of capabilities, see Mariner Image Customizer's iso
configuration section: [Config.ISO](./configuration.md#iso-type).
## cloud-init Support
In some user scenarios, it desired to embed the cloud-init data files into the
iso media. The easiest way is to include the data files on the media, and then
the cloud-init `ds` kernel parameter to where the files are.
The files can be placed directly within the iso file system or they can be
placed within the LiveOS root file system.
Placing those files directly on the iso file system will allow a more efficient
replacement flow in the future (i.e. when it is desired to only replace the
cloud-init data files).
#### Example 1
If cloud-init data is to be placed directly within the iso file system:
```yaml
Iso:
AdditionalFiles:
cloud-init-data/user-data: /cloud-init-data/user-data
cloud-init-data/network-config: /cloud-init-data/network-config
cloud-init-data/meta-data: /cloud-init-data/meta-data
KernelCommandLine:
ExtraCommandLine: "'ds=nocloud;s=file://run/initramfs/live/cloud-init-data'"
SystemConfig:
Users:
- Name: test
Password: testpassword
PrimaryGroup: sudo
```
#### Example 2
If cloud-init data is to be placed within the LiveOS root file system:
```yaml
Iso:
KernelCommandLine:
ExtraCommandLine: "'ds=nocloud;s=file://cloud-init-data'"
SystemConfig:
Users:
- Name: test
Password: testpassword
PrimaryGroup: sudo
AdditionalFiles:
cloud-init-data/user-data: /cloud-init-data/user-data
cloud-init-data/network-config: /cloud-init-data/network-config
cloud-init-data/meta-data: /cloud-init-data/meta-data
```

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

@ -9,13 +9,24 @@ import (
// Iso defines how the generated iso media should be configured.
type Iso struct {
AdditionalFiles AdditionalFilesMap `yaml:"AdditionalFiles"`
KernelCommandLine KernelCommandLine `yaml:"KernelCommandLine"`
AdditionalFiles AdditionalFilesMap `yaml:"AdditionalFiles"`
}
func (i *Iso) IsValid() error {
err := i.AdditionalFiles.IsValid()
var err error
err = i.KernelCommandLine.IsValid()
if err != nil {
return fmt.Errorf("invalid AdditionalFiles: %w", err)
return fmt.Errorf("invalid KernelCommandLine: %w", err)
}
if i.AdditionalFiles != nil {
err := i.AdditionalFiles.IsValid()
if err != nil {
return fmt.Errorf("invalid AdditionalFiles: %w", err)
}
}
return nil
}

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

@ -3,16 +3,11 @@
package imagecustomizerapi
import (
"fmt"
"strings"
)
type KernelCommandLine struct {
// SELinux specifies whether or not to enable SELinux on the image (and what mode SELinux should be in).
SELinux SELinux `yaml:"SELinux"`
// Extra kernel command line args.
ExtraCommandLine string `yaml:"ExtraCommandLine"`
ExtraCommandLine KernelExtraArguments `yaml:"ExtraCommandLine"`
}
func (s *KernelCommandLine) IsValid() error {
@ -21,21 +16,10 @@ func (s *KernelCommandLine) IsValid() error {
return err
}
err = commandLineIsValid(s.ExtraCommandLine, "ExtraCommandLine")
err = s.ExtraCommandLine.IsValid()
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
}

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

@ -0,0 +1,148 @@
package imagecustomizerapi
import (
"bytes"
"fmt"
"strings"
)
// KernelExtraArguments defines one or more extra kernel arguments.
type KernelExtraArguments string
func (e KernelExtraArguments) IsValid() error {
err := validateKernelArgumentsFormat(string(e))
if err != nil {
return err
}
return nil
}
/*
The following code is based on the 'quoting' section in grub
documentation: https://www.gnu.org/software/grub/manual/grub/grub.html#Quoting
Here is a copy for convenience:
There are three quoting mechanisms: the escape character, single quotes, and
double quotes.
(1)
A non-quoted backslash (\) is the escape character. It preserves the literal
value of the next character that follows, with the exception of newline.
(2)
Enclosing characters in single quotes preserves the literal value of each
character within the quotes.
(3)
A single quote may not occur between single
quotes, even when preceded by a backslash.
(4)
Enclosing characters in double quotes preserves the literal value of all
characters within the quotes, with the exception of $ and \.
(5)
The $ character retains its special meaning within double quotes.
(6)
The backslash retains its special meaning only when followed by one of the
following characters: $, ", \, or newline. A backslash-newline pair is
treated as a line continuation (that is, it is removed from the input stream
and effectively ignored.
(7)
A double quote may be quoted within double quotes by preceding it with a
backslash.
*/
// if escapedCharacters is empty, it means always escape the next character.
// if escapedCharacters is not empty, it means escape only if the next
// character is one of escapedCharacters.
func processEscapedCharacter(text string, start int, count int, escapedCharacters string) (lastProcessed int, err error) {
i := start + 1
if i < count {
if len(escapedCharacters) == 0 || bytes.IndexByte([]byte(escapedCharacters), text[i]) != -1 {
// character is meant to be escaped
return i, nil
} else {
// character is not meant to be escaped
// In some cases (see (6) above), the escape charater is not meant
// escape the next character - so, we should not remove the next
// character from the stream.
return start, nil
}
}
return i, fmt.Errorf("missing escaped character. '\\' must be followed by a character.")
}
func processDoubleQuotedString(text string, start int, count int) (lastProcessed int, err error) {
i := start + 1
for i < count {
switch {
case text[i] == '\\':
i, err = processEscapedCharacter(text, i, count, "$\"\\n")
if err != nil {
return i, err
}
default:
if text[i] == '"' {
return i, nil
}
}
i++
}
return i, fmt.Errorf("invalid double-quoted string. Missing closing double-quotes.")
}
func processSingleQuotedString(text string, start int, count int) (lastProcessed int, err error) {
i := start + 1
for i < count {
if text[i] == '\'' {
return i, nil
}
i++
}
return i, fmt.Errorf("invalid single-quoted string. Missing closing single-quote.")
}
func validateQuotedSubstrings(kernelArguments string) (err error) {
count := len(kernelArguments)
for i := 0; i < count; i++ {
switch {
case kernelArguments[i] == '"':
i, err = processDoubleQuotedString(kernelArguments, i, count)
if err != nil {
return err
}
case kernelArguments[i] == '\'':
i, err = processSingleQuotedString(kernelArguments, i, count)
if err != nil {
return err
}
case kernelArguments[i] == '\\':
i, err = processEscapedCharacter(kernelArguments, i, count, "" /*skip all*/)
if err != nil {
return err
}
}
}
return nil
}
func validateKernelArgumentsFormat(kernelArguments string) (err 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(kernelArguments, "$`") {
return fmt.Errorf("the ExtraCommandLine value contains invalid characters")
}
err = validateQuotedSubstrings(kernelArguments)
if err != nil {
return err
}
return nil
}

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

@ -0,0 +1,102 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package imagecustomizerapi
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestKernelExtraArgumentsIsValid(t *testing.T) {
/*
The following test cases are based on the 'quoting' section in grub
documentation: https://www.gnu.org/software/grub/manual/grub/grub.html#Quoting
Here is a copy for convenience along with section numbers to be associated
with test cases:
There are three quoting mechanisms: the escape character, single quotes, and
double quotes.
(1)
A non-quoted backslash (\) is the escape character. It preserves the literal
value of the next character that follows, with the exception of newline.
(2)
Enclosing characters in single quotes preserves the literal value of each
character within the quotes.
(3)
A single quote may not occur between single
quotes, even when preceded by a backslash.
(4)
Enclosing characters in double quotes preserves the literal value of all
characters within the quotes, with the exception of $ and \.
(5)
The $ character retains its special meaning within double quotes.
(6)
The backslash retains its special meaning only when followed by one of the
following characters: $, ", \, or newline. A backslash-newline pair is
treated as a line continuation (that is, it is removed from the input stream
and effectively ignored.
(7)
A double quote may be quoted within double quotes by preceding it with a
backslash.
*/
missingClosingDoubleQuotes := "invalid double-quoted string. Missing closing double-quotes."
missingClosingSingleQuote := "invalid single-quoted string. Missing closing single-quote."
configsToTest := map[KernelExtraArguments]*string{
// very simple cases (no quoting)
"": nil,
"a": nil,
"a=b": nil,
"a=b x=y": nil,
// enlosed in double quotes (4)
"\"a=b\"": nil,
// enclosed in single quotes (2)
"'a=b'": nil,
// single quote embedded within double quotes and vice versa (2)
"\"a='b\" 'x=\"y'": nil,
// single-quoted string embedded within a double quoted value (4)
"\"'a=b' x=y\"": nil,
// double-quoted string embedded within a double quoted value (4)(6)(7)
"\"a=b \\\"x=y\\\"\"": nil,
// \n embedded within a double quoted value (4)(6)
"\"a=b x=y\\n\"": nil,
// \ embedded within a double quoted value (4)(6)
"\"a=b x=y\\\\\"": nil,
// unmatched open double-quote - beginning of string (4)
"\"a=b x=y": &missingClosingDoubleQuotes,
// unmatched open double-quote - middle of string (4)
"a=b \"x=y": &missingClosingDoubleQuotes,
// unmatched open double-quote - end of string (4)
"a=b x=y\"": &missingClosingDoubleQuotes,
// unmatched open single-quote - beginning of string (2)
"'a=b x=y": &missingClosingSingleQuote,
// unmatched open single-quote - middle of string (2)
"a=b 'x=y": &missingClosingSingleQuote,
// unmatched open single-quote - end of string (2)
"a=b x='y": &missingClosingSingleQuote,
// attempt to use \ to escape single quotes (3)
"'a=\\'b'": &missingClosingSingleQuote,
}
// testsOk := true
for config, expectedErr := range configsToTest {
err := config.IsValid()
if expectedErr == nil {
assert.NoError(t, err)
} else {
assert.Error(t, err)
assert.ErrorContains(t, err, *expectedErr)
}
}
}

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

@ -42,18 +42,6 @@ func TestSystemConfigIsValidDuplicatePartitionID(t *testing.T) {
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")
}
func TestSystemConfigIsValidVerityInValidPartUuid(t *testing.T) {
invalidVerity := SystemConfig{
Verity: &Verity{

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

@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
"regexp"
"strings"
"github.com/microsoft/CBL-Mariner/toolkit/tools/imagecustomizerapi"
"github.com/microsoft/CBL-Mariner/toolkit/tools/imagegen/installutils"
@ -47,7 +48,7 @@ const (
selinuxConfigModeRegexSELinuxMode = 1
)
func handleKernelCommandLine(extraCommandLine string, imageChroot *safechroot.Chroot, partitionsCustomized bool) error {
func handleKernelCommandLine(kernelExtraArguments imagecustomizerapi.KernelExtraArguments, imageChroot *safechroot.Chroot, partitionsCustomized bool) error {
var err error
if partitionsCustomized {
@ -56,6 +57,7 @@ func handleKernelCommandLine(extraCommandLine string, imageChroot *safechroot.Ch
return nil
}
extraCommandLine := strings.TrimSpace(string(kernelExtraArguments))
if extraCommandLine == "" {
// Nothing to do.
return nil

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

@ -250,11 +250,23 @@ func validateIsoConfig(baseConfigPath string, config *imagecustomizerapi.Iso) er
return nil
}
err := validateAdditionalFiles(baseConfigPath, config.AdditionalFiles)
err := validateIsoKernelCommandline(config.KernelCommandLine)
if err != nil {
return err
}
err = validateAdditionalFiles(baseConfigPath, config.AdditionalFiles)
if err != nil {
return err
}
return nil
}
func validateIsoKernelCommandline(kernelCommandLine imagecustomizerapi.KernelCommandLine) error {
if kernelCommandLine.SELinux != imagecustomizerapi.SELinuxDefault {
return fmt.Errorf("unsupported SELinux configuration for the output ISO image.")
}
return nil
}

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

@ -8,6 +8,7 @@ import (
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/microsoft/CBL-Mariner/toolkit/tools/imagecustomizerapi"
"github.com/microsoft/CBL-Mariner/toolkit/tools/imagegen/configuration"
@ -26,6 +27,7 @@ menuentry "Mariner Baremetal Iso" {
search --label CDROM --set root
linux /isolinux/vmlinuz \
%s \
overlay-size=70%% \
selinux=0 \
console=tty0 \
@ -41,6 +43,7 @@ menuentry "Mariner Baremetal Iso" {
initrd /isolinux/initrd.img
}
`
liveOSDir = "liveos"
liveOSImage = "rootfs.img"
@ -261,11 +264,13 @@ func (b *LiveOSIsoBuilder) prepareRootfsForDracut(writeableRootfsDir string) err
// The folder where the artifacts needed by isoMaker will be staged before
// 'dracut' is run. 'dracut' will include this folder as-is and place it in
// the initrd image.
// - 'extraCommandLine':
// extra kernel command line arguments to add to grub.
//
// outputs
// - customized writeableRootfsDir (new files, deleted files, etc)
// - extracted artifacts
func (b *LiveOSIsoBuilder) prepareLiveOSDir(writeableRootfsDir, isoMakerArtifactsStagingDir string) error {
func (b *LiveOSIsoBuilder) prepareLiveOSDir(writeableRootfsDir string, isoMakerArtifactsStagingDir string, extraCommandLine string) error {
logger.Log.Debugf("Creating LiveOS squashfs image")
@ -296,7 +301,7 @@ func (b *LiveOSIsoBuilder) prepareLiveOSDir(writeableRootfsDir, isoMakerArtifact
b.artifacts.vmlinuzPath = targetVmLinuzPath
// create grub.cfg
targetGrubCfgContent := fmt.Sprintf(grubCfgTemplate, liveOSDir, liveOSImage)
targetGrubCfgContent := fmt.Sprintf(grubCfgTemplate, extraCommandLine, liveOSDir, liveOSImage)
targetGrubCfgPath := filepath.Join(b.workingDirs.isoArtifactsDir, "grub.cfg")
err = os.WriteFile(targetGrubCfgPath, []byte(targetGrubCfgContent), 0o644)
@ -422,13 +427,15 @@ func (b *LiveOSIsoBuilder) generateInitrdImage(rootfsSourceDir, artifactsSourceD
// - 'rawImageFile':
// path to an existing raw full disk image (i.e. image with boot
// partition and a rootfs partition).
// - 'extraCommandLine':
// extra kernel command line arguments to add to grub.
//
// outputs:
// - all the extracted/generated artifacts will be placed in the
// `LiveOSIsoBuilder.workingDirs.isoArtifactsDir` folder.
// - the paths to individual artifaces are found in the
// `LiveOSIsoBuilder.artifacts` data structure.
func (b *LiveOSIsoBuilder) prepareArtifactsFromFullImage(rawImageFile string) error {
func (b *LiveOSIsoBuilder) prepareArtifactsFromFullImage(rawImageFile string, extraCommandLine string) error {
logger.Log.Infof("Preparing iso artifacts")
@ -451,7 +458,7 @@ func (b *LiveOSIsoBuilder) prepareArtifactsFromFullImage(rawImageFile string) er
}
isoMakerArtifactsStagingDir := "/boot-staging"
err = b.prepareLiveOSDir(writeableRootfsDir, isoMakerArtifactsStagingDir)
err = b.prepareLiveOSDir(writeableRootfsDir, isoMakerArtifactsStagingDir, extraCommandLine)
if err != nil {
return fmt.Errorf("failed to convert rootfs folder to a LiveOS folder:\n%w", err)
}
@ -572,7 +579,9 @@ func (b *LiveOSIsoBuilder) createIsoImage(additionalIsoFiles []safechroot.FileTo
// outputs:
// - 'additionalIsoFiles'
// list of files to copy from the build machine to the iso media.
func micIsoConfigToIsoMakerConfig(baseConfigPath string, isoConfig *imagecustomizerapi.Iso) (additionalIsoFiles []safechroot.FileToCopy, err error) {
func micIsoConfigToIsoMakerConfig(baseConfigPath string, isoConfig *imagecustomizerapi.Iso) (additionalIsoFiles []safechroot.FileToCopy, extraCommandLine string, err error) {
extraCommandLine = strings.TrimSpace(string(isoConfig.KernelCommandLine.ExtraCommandLine))
additionalIsoFiles = []safechroot.FileToCopy{}
@ -588,7 +597,7 @@ func micIsoConfigToIsoMakerConfig(baseConfigPath string, isoConfig *imagecustomi
}
}
return additionalIsoFiles, nil
return additionalIsoFiles, extraCommandLine, nil
}
// createLiveOSIsoImage
@ -618,7 +627,7 @@ func micIsoConfigToIsoMakerConfig(baseConfigPath string, isoConfig *imagecustomi
// creates a LiveOS ISO image.
func createLiveOSIsoImage(buildDir, baseConfigPath string, isoConfig *imagecustomizerapi.Iso, rawImageFile, outputImageDir, outputImageBase string) (err error) {
additionalIsoFiles, err := micIsoConfigToIsoMakerConfig(baseConfigPath, isoConfig)
additionalIsoFiles, extraCommandLine, err := micIsoConfigToIsoMakerConfig(baseConfigPath, isoConfig)
if err != nil {
return fmt.Errorf("failed to convert iso configuration to isomaker format:\n%w", err)
}
@ -651,7 +660,7 @@ func createLiveOSIsoImage(buildDir, baseConfigPath string, isoConfig *imagecusto
}
}()
err = isoBuilder.prepareArtifactsFromFullImage(rawImageFile)
err = isoBuilder.prepareArtifactsFromFullImage(rawImageFile, extraCommandLine)
if err != nil {
return err
}

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

@ -171,7 +171,7 @@ func kernelCommandLineToImager(kernelCommandLine imagecustomizerapi.KernelComman
}
imagerKernelCommandLine := configuration.KernelCommandLine{
ExtraCommandLine: kernelCommandLine.ExtraCommandLine,
ExtraCommandLine: string(kernelCommandLine.ExtraCommandLine),
SELinux: imagerSELinux,
SELinuxPolicy: "",
}