Skip to content

MIC: Add support for self-contained initrd for LiveOS ISO images #233

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 46 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
4b3f6ed
MIC: Add support for self-contained iso initrd.
gmileka May 6, 2025
8ded802
Added extractFilesFromInitrdImage
gmileka May 9, 2025
56300d2
Update initrd -> file system logic.
gmileka May 9, 2025
f1296d8
initrd->raw-image->initrd ok
gmileka May 12, 2025
9c31ae4
Update logging
gmileka May 13, 2025
90c16c8
Remove the boot folder from the rootfs
gmileka May 13, 2025
13f6ac9
iso-to-iso works!
gmileka May 14, 2025
8071bd2
Fix the setuid bit in initrd
gmileka May 14, 2025
77c4124
All scenarios pass!
gmileka May 15, 2025
eda2560
tdnf install now works.
gmileka May 15, 2025
85712bf
Persist the kernel version.
gmileka May 16, 2025
d8a03d8
Fix owners and file mode
gmileka May 16, 2025
83e8846
Refactor initrd code out
gmileka May 16, 2025
134c0fe
isogenerator.go clean-up
gmileka May 16, 2025
5dba4f0
customize.go clean-up
gmileka May 16, 2025
f650886
customizepackages.go clean-up
gmileka May 16, 2025
f583904
customizepartitions.go clean-up
gmileka May 16, 2025
8f0099d
resolvconf.go clean-up
gmileka May 16, 2025
63bf32e
initrdutils.go clean-up - part 1
gmileka May 16, 2025
40ad693
initrdutils.go clean-up - part 2
gmileka May 16, 2025
a9b8889
Remove debug logging
gmileka May 16, 2025
8e1841d
Update the comment of renaming initrd and vmlinux
gmileka May 16, 2025
5e25c6d
Minor refactoring
gmileka May 16, 2025
887f48d
Partial update to the new interface
gmileka May 24, 2025
0f3af82
Another fix
gmileka May 27, 2025
7e2aedb
Interface builds
gmileka May 27, 2025
a4134e9
Feature complete?
gmileka May 27, 2025
8e03ea1
First test passing
gmileka May 28, 2025
03f717f
Rename entry point methods
gmileka May 28, 2025
7858ccb
Get tests working again
gmileka May 28, 2025
bd64bfd
Tests pass... but with pxe folder disabled.
gmileka May 28, 2025
d36fdff
Refactor iso test a bit
gmileka May 29, 2025
a800b06
Phases...
gmileka May 29, 2025
147ce88
More pxe clean-up now that there are no two grub.cfgs.
gmileka May 29, 2025
db5ace0
separate pxe from iso
gmileka May 29, 2025
b54555b
First PXE test passes
gmileka May 29, 2025
8cb900d
Enabling 2nd test
gmileka May 29, 2025
09dd903
Fix ip= shouldn't be in grub.cfg if full-os
gmileka May 29, 2025
1204b23
Update log message
gmileka May 29, 2025
36ced85
Clean-up code for tar expansion
gmileka May 29, 2025
c812033
Unit tests complete
gmileka May 30, 2025
8db4158
Minor fix
gmileka May 30, 2025
55c018c
Block selinux for full os initramfs.
gmileka May 30, 2025
9e89800
Optimize the creation of iso for the pxe scenarios.
gmileka May 30, 2025
4c9650c
PR ready initrdutils.go
gmileka May 30, 2025
4d5bbc9
Use standard implementation with chmod
gmileka May 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions toolkit/tools/imagecustomizer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,20 @@ import (
type CustomizeCmd struct {
BuildDir string `name:"build-dir" help:"Directory to run build out of." required:""`
InputImageFile string `name:"image-file" help:"Path of the base Azure Linux image which the customization will be applied to."`
OutputImageFile string `name:"output-image-file" help:"Path to write the customized image to."`
OutputImageFormat string `name:"output-image-format" placeholder:"(vhd|vhd-fixed|vhdx|qcow2|raw|iso|cosi)" help:"Format of output image." enum:"${imageformat}" default:""`
OutputImageFile string `name:"output-image-file" help:"Path to write the customized image artifacts to."`
OutputPath string `name:"output-path" help: "Path to write the customized image artifacts to."`
OutputImageFormat string `name:"output-image-format" placeholder:"(vhd|vhd-fixed|vhdx|qcow2|raw|iso|pxe|cosi)" help:"Format of output image." enum:"${imageformat}" default:""`
ConfigFile string `name:"config-file" help:"Path of the image customization config file." required:""`
RpmSources []string `name:"rpm-source" help:"Path to a RPM repo config file or a directory containing RPMs."`
DisableBaseImageRpmRepos bool `name:"disable-base-image-rpm-repos" help:"Disable the base image's RPM repos as an RPM source."`
OutputPXEArtifactsDir string `name:"output-pxe-artifacts-dir" help:"Create a directory with customized image PXE booting artifacts. '--output-image-format' must be set to 'iso'."`
}

type InjectFilesCmd struct {
BuildDir string `name:"build-dir" help:"Directory to run build out of." required:""`
ConfigFile string `name:"config-file" help:"Path to the inject-files.yaml config file." required:""`
InputImageFile string `name:"image-file" help:"Path of the base image to inject files into." required:""`
OutputImageFile string `name:"output-image-file" help:"Path to write the injected image to."`
OutputPath string `name:"output-path" help: "Path to write the customized image artifacts to."`
OutputImageFormat string `name:"output-image-format" placeholder:"(vhd|vhd-fixed|vhdx|qcow2|raw|iso|cosi)" help:"Format of output image." enum:"${imageformat}" default:""`
}

Expand Down Expand Up @@ -87,9 +88,12 @@ func main() {
}

func customizeImage(cmd CustomizeCmd) error {
// Todo: either outputimagefile or outpath
if cmd.OutputImageFile == "" {
cmd.OutputImageFile = cmd.OutputPath
}
err := imagecustomizerlib.CustomizeImageWithConfigFile(cmd.BuildDir, cmd.ConfigFile, cmd.InputImageFile,
cmd.RpmSources, cmd.OutputImageFile, cmd.OutputImageFormat, cmd.OutputPXEArtifactsDir,
!cmd.DisableBaseImageRpmRepos)
cmd.RpmSources, cmd.OutputImageFile, cmd.OutputImageFormat, !cmd.DisableBaseImageRpmRepos)
if err != nil {
return err
}
Expand All @@ -98,6 +102,10 @@ func customizeImage(cmd CustomizeCmd) error {
}

func injectFiles(cmd InjectFilesCmd) error {
// Todo: either outputimagefile or outpath
if cmd.OutputImageFile == "" {
cmd.OutputImageFile = cmd.OutputPath
}
err := imagecustomizerlib.InjectFilesWithConfigFile(cmd.BuildDir, cmd.ConfigFile, cmd.InputImageFile,
cmd.OutputImageFile, cmd.OutputImageFormat)
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions toolkit/tools/imagecustomizerapi/imageFormatType.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const (
ImageFormatTypeQcow2 ImageFormatType = "qcow2"
ImageFormatTypeRaw ImageFormatType = "raw"
ImageFormatTypeIso ImageFormatType = "iso"
ImageFormatTypePxe ImageFormatType = "pxe"
ImageFormatTypeCosi ImageFormatType = "cosi"
)

Expand All @@ -30,6 +31,7 @@ var supportedImageFormatTypes = []string{
string(ImageFormatTypeQcow2),
string(ImageFormatTypeRaw),
string(ImageFormatTypeIso),
string(ImageFormatTypePxe),
string(ImageFormatTypeCosi),
}

Expand Down
37 changes: 37 additions & 0 deletions toolkit/tools/imagecustomizerapi/initramfsImageType.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package imagecustomizerapi

import (
"fmt"
"slices"
)

type InitramfsImageType string

const (
InitramfsImageTypeUnspecified InitramfsImageType = ""
InitramfsImageTypeBootstrap InitramfsImageType = "bootstrap"
InitramfsImageTypeFullOS InitramfsImageType = "full-os"
)

// supportedInitramfsImageTypes is a list of all non-empty image format types
// defined above.
var supportedInitramfsImageTypes = []string{
string(InitramfsImageTypeBootstrap),
string(InitramfsImageTypeFullOS),
}

func (ft InitramfsImageType) IsValid() error {
if ft != InitramfsImageTypeUnspecified && !slices.Contains(SupportedInitramfsImageTypes(), string(ft)) {
return fmt.Errorf("invalid initramfs image type (%s)", ft)
}

return nil
}

// SupportedImageFormatTypes returns all valid image format types.
func SupportedInitramfsImageTypes() []string {
return supportedInitramfsImageTypes
}
6 changes: 6 additions & 0 deletions toolkit/tools/imagecustomizerapi/iso.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
type Iso struct {
KernelCommandLine KernelCommandLine `yaml:"kernelCommandLine" json:"kernelCommandLine,omitempty"`
AdditionalFiles AdditionalFileList `yaml:"additionalFiles" json:"additionalFiles,omitempty"`
InitramfsType InitramfsImageType `yaml:"initramfsType" json:"initramfsType,omitempty"`
}

func (i *Iso) IsValid() error {
Expand All @@ -24,5 +25,10 @@ func (i *Iso) IsValid() error {
return fmt.Errorf("invalid additionalFiles:\n%w", err)
}

err = i.InitramfsType.IsValid()
if err != nil {
return fmt.Errorf("invalid initramfs type:\n%w", err)
}

return nil
}
32 changes: 25 additions & 7 deletions toolkit/tools/imagecustomizerapi/pxe.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ var PxeIsoDownloadProtocols = []string{"ftp://", "http://", "https://", "nfs://"

// Iso defines how the generated iso media should be configured.
type Pxe struct {
IsoImageBaseUrl string `yaml:"isoImageBaseUrl" json:"isoImageBaseUrl,omitempty"`
IsoImageFileUrl string `yaml:"isoImageFileUrl" json:"isoImageFileUrl,omitempty"`
KernelCommandLine KernelCommandLine `yaml:"kernelCommandLine" json:"kernelCommandLine,omitempty"`
AdditionalFiles AdditionalFileList `yaml:"additionalFiles" json:"additionalFiles,omitempty"`
InitramfsType InitramfsImageType `yaml:"initramfsType" json:"initramfsType,omitempty"`
BootstrapBaseUrl string `yaml:"bootstrapBaseUrl" json:"bootstrapBaseUrl,omitempty"`
BootstrapFileUrl string `yaml:"bootstrapFileUrl" json:"bootstrapFileUrl,omitempty"`
}

func IsValidPxeUrl(urlString string) error {
Expand Down Expand Up @@ -42,16 +45,31 @@ func IsValidPxeUrl(urlString string) error {
}

func (p *Pxe) IsValid() error {
if p.IsoImageBaseUrl != "" && p.IsoImageFileUrl != "" {
err := p.KernelCommandLine.IsValid()
if err != nil {
return fmt.Errorf("invalid kernelCommandLine: %w", err)
}

err = p.AdditionalFiles.IsValid()
if err != nil {
return fmt.Errorf("invalid additionalFiles:\n%w", err)
}

err = p.InitramfsType.IsValid()
if err != nil {
return fmt.Errorf("invalid initramfs type:\n%w", err)
}

if p.BootstrapBaseUrl != "" && p.BootstrapFileUrl != "" {
return fmt.Errorf("cannot specify both 'isoImageBaseUrl' and 'isoImageFileUrl' at the same time.")
}
err := IsValidPxeUrl(p.IsoImageBaseUrl)
err = IsValidPxeUrl(p.BootstrapBaseUrl)
if err != nil {
return fmt.Errorf("invalid 'isoImageBaseUrl' field value (%s):\n%w", p.IsoImageBaseUrl, err)
return fmt.Errorf("invalid 'isoImageBaseUrl' field value (%s):\n%w", p.BootstrapBaseUrl, err)
}
err = IsValidPxeUrl(p.IsoImageFileUrl)
err = IsValidPxeUrl(p.BootstrapFileUrl)
if err != nil {
return fmt.Errorf("invalid 'isoImageFileUrl' field value (%s):\n%w", p.IsoImageFileUrl, err)
return fmt.Errorf("invalid 'isoImageFileUrl' field value (%s):\n%w", p.BootstrapFileUrl, err)
}
return nil
}
221 changes: 221 additions & 0 deletions toolkit/tools/internal/initrdutils/initrdutils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
// Copyright Microsoft Corporation.
// Licensed under the MIT License.

package initrdutils

import (
"fmt"
"io"
"os"
"path/filepath"
"syscall"

"github.com/cavaliercoder/go-cpio"
"github.com/klauspost/pgzip"
)

func CreateInitrdImageFromFolder(inputDir, outputInitrdImagePath string) (err error) {
// The `inputDir` permissions will become the `/` permissions when the initrd
// is mounted. This needs to be 0755 or some processes will fail to function
// correctly.
err = os.Chmod(inputDir, 0755)
if err != nil {
return fmt.Errorf("failed to change folder permissions for (%s):\n%w", inputDir, err)
}

outputFile, err := os.Create(outputInitrdImagePath)
if err != nil {
return fmt.Errorf("failed to create image file (%s):\n%w", outputInitrdImagePath, err)
}
defer outputFile.Close()

gzipWriter := pgzip.NewWriter(outputFile)
defer gzipWriter.Close()

cpioWriter := cpio.NewWriter(gzipWriter)
defer func() {
closeErr := cpioWriter.Close()
if err != nil {
err = closeErr
}
}()

// Traverse the directory structure and add all the files/directories/links to the archive.
err = filepath.Walk(inputDir, func(path string, info os.FileInfo, fileErr error) (err error) {
if fileErr != nil {
return fmt.Errorf("encountered a file walk error on path (%s):\n%w", path, fileErr)
}
err = addFileToCpioArchive(inputDir, path, info, cpioWriter)
if err != nil {
return fmt.Errorf("failed to add (%s) to archive (%s):\n%w", path, outputInitrdImagePath, err)
}
return nil
})

return nil
}

func buildCpioHeader(inputDir, path string, info os.FileInfo, link string) (cpioHeader *cpio.Header, err error) {
// Convert the OS header into a CPIO header
cpioHeader, err = cpio.FileInfoHeader(info, link)
if err != nil {
return nil, fmt.Errorf("failed to convert OS file info into a cpio header for (%s)\n%w", path, err)
}

// Convert full path to relative path
relPath, err := filepath.Rel(inputDir, path)
if err != nil {
return nil, fmt.Errorf("failed to get relative path of (%s) using root (%s):\n%w", path, inputDir, err)
}
cpioHeader.Name = relPath

// Set owners (cpio.FileInfoHeader() does not set the owners)
stat, ok := info.Sys().(*syscall.Stat_t)
if !ok {
return nil, fmt.Errorf("failed to get file stat of (%s)", path)
}
cpioHeader.UID = int(stat.Uid)
cpioHeader.GID = int(stat.Gid)

return cpioHeader, nil
}

func addFileToCpioArchive(inputDir, path string, info os.FileInfo, cpioWriter *cpio.Writer) (err error) {
var link string
if info.Mode()&os.ModeSymlink != 0 {
link, err = os.Readlink(path)
if err != nil {
return fmt.Errorf("failed to read link information of (%s):\n%w", path, err)
}
}

cpioHeader, err := buildCpioHeader(inputDir, path, info, link)
if err != nil {
return fmt.Errorf("failed to construct cpio file header for (%s)\n%w", path, err)
}

err = cpioWriter.WriteHeader(cpioHeader)
if err != nil {
return fmt.Errorf("failed to write cpio header for (%s)\n%w", path, err)
}

if info.Mode().IsRegular() {
fileToAdd, err := os.Open(path)
if err != nil {
return fmt.Errorf("failed to open (%s)\n%w", path, err)
}
defer fileToAdd.Close()

_, err = io.Copy(cpioWriter, fileToAdd)
if err != nil {
return fmt.Errorf("failed to write (%s) to cpio archive\n%w", path, err)
}
} else {
if info.Mode()&os.ModeSymlink != 0 {
_, err = cpioWriter.Write([]byte(link))
if err != nil {
return fmt.Errorf("failed to write link (%s)\n%w", path, err)
}
}

// For all other special files, they will be of size 0 and only contain
// the header in the archive.
}

return nil
}

func updateFileOwnership(path string, fileMode os.FileMode, uid, gid int) (err error) {
err = os.Chown(path, uid, gid)
if err != nil {
return fmt.Errorf("failed to set ownership on extracted (%s) to (%d,%d):\n%w", path, uid, gid, err)
}

// The golang implementation maps the setuid/setgid/sticky flags to bits
// different from those defined in native C implementation. As a result,
// we must convert between them.
// See https://github.com/golang/go/blob/release-branch.go1.24/src/os/stat_js.go

if fileMode&cpio.ModePerm != 0 {
fileModeAdjusted := os.FileMode(fileMode).Perm()
if fileMode&syscall.S_ISUID != 0 {
fileModeAdjusted |= os.ModeSetuid
}
if fileMode&syscall.S_ISGID != 0 {
fileModeAdjusted |= os.ModeSetgid
}
if fileMode&syscall.S_ISVTX != 0 {
fileModeAdjusted |= os.ModeSticky
}

err = os.Chmod(path, fileModeAdjusted)
if err != nil {
return fmt.Errorf("failed to change mode for file (%s) to (%#o):\n%w", path, fileMode, err)
}
}
return nil
}

func CreateFolderFromInitrdImage(inputInitrdImagePath, outputDir string) (err error) {
inputInitrdImageFile, err := os.Open(inputInitrdImagePath)
if err != nil {
return fmt.Errorf("failed to open file (%s):\n%w", inputInitrdImagePath, err)
}
defer inputInitrdImageFile.Close()

pgzipReader, err := pgzip.NewReader(inputInitrdImageFile)
if err != nil {
return fmt.Errorf("failed to create a pgzip reader for (%s):\n%w", inputInitrdImagePath, err)
}
defer pgzipReader.Close()

cpioReader := cpio.NewReader(pgzipReader)
for {
cpioHeader, err := cpioReader.Next()
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("failed to read cpio header from (%s):\n%w", inputInitrdImagePath, err)
}

path := filepath.Join(outputDir, cpioHeader.Name)
fileMode := os.FileMode(cpioHeader.Mode & (cpio.ModePerm | cpio.ModeSetuid | cpio.ModeSetgid | cpio.ModeSticky))
fileType := cpioHeader.Mode & cpio.ModeType

switch fileType {
case cpio.ModeDir:
err := os.MkdirAll(path, fileMode)
if err != nil {
return fmt.Errorf("failed to create directory (%s):\n%w", path, err)
}
err = updateFileOwnership(path, fileMode, cpioHeader.UID, cpioHeader.GID)
if err != nil {
return fmt.Errorf("failed to update ownership of directory (%s)\n%w", path, err)
}
case cpio.ModeRegular:
destFile, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, fileMode)
if err != nil {
return fmt.Errorf("failed to create file (%s):\n%w", path, err)
}
_, err = io.Copy(destFile, cpioReader)
destFile.Close()
if err != nil {
return fmt.Errorf("failed to write file (%s):\n%w", path, err)
}
err = updateFileOwnership(path, fileMode, cpioHeader.UID, cpioHeader.GID)
if err != nil {
return fmt.Errorf("failed to update ownership of file (%s)\n%w", path, err)
}
case cpio.ModeSymlink:
err = os.Symlink(cpioHeader.Linkname, path)
if err != nil {
return fmt.Errorf("failed to create symbolic link (%s) to (%s)\n%w", cpioHeader.Linkname, path, err)
}
default:
return fmt.Errorf("unsupported type (%s) in cpio archive (%s)", fileType, inputInitrdImagePath)
}
}

return nil
}
Loading