diff --git a/toolkit/tools/imagecustomizer/docs/iso.md b/toolkit/tools/imagecustomizer/docs/iso.md index 48343b593..cc47f5869 100644 --- a/toolkit/tools/imagecustomizer/docs/iso.md +++ b/toolkit/tools/imagecustomizer/docs/iso.md @@ -25,7 +25,6 @@ almost all the artifacts unchanged - some artifacts are changed as follows: - the root is updated to the LiveOS root file system image. - the LiveOS dracut parameters are appended. - the user-specified new parameters are appended. - - SELinux is disabled. - `/etc/fstab` is dropped from the rootfs as it typically conflicts with the overlay setup required by the LiveOS. - `initrd.img` is regenerated to serve the LiveOS boot flow. This should have @@ -42,8 +41,6 @@ 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. - non-"Azure Linux toolkit" images - images generated by tools other than the Azure Linux toolkit may fail to be converted to a LiveOS iso. This is due to certain assumptions on the input diff --git a/toolkit/tools/pkg/imagecustomizerlib/customizepackages.go b/toolkit/tools/pkg/imagecustomizerlib/customizepackages.go index 4b00cc28b..3a3f7372a 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/customizepackages.go +++ b/toolkit/tools/pkg/imagecustomizerlib/customizepackages.go @@ -6,7 +6,6 @@ package imagecustomizerlib import ( "fmt" "regexp" - "strconv" "strings" "github.com/microsoft/azurelinux/toolkit/tools/imagecustomizerapi" @@ -26,13 +25,6 @@ var ( tdnfTransactionError = regexp.MustCompile(`^Found \d+ problems$`) ) -type packageInformation struct { - packageVersion string - packageRelease uint32 - distroName string - distroVersion uint32 -} - func addRemoveAndUpdatePackages(buildDir string, baseConfigPath string, config *imagecustomizerapi.OS, imageChroot *safechroot.Chroot, rpmsSources []string, useBaseImageRpmRepos bool, ) error { @@ -242,79 +234,6 @@ func isPackageInstalled(imageChroot *safechroot.Chroot, packageName string) bool return true } -func parseReleaseString(releaseInfo string) (packageRelease uint32, distroName string, distroVersion uint32, err error) { - pattern := `([0-9]+)\.([a-zA-Z]+)([0-9]+)` - re := regexp.MustCompile(pattern) - matches := re.FindStringSubmatch(releaseInfo) - - if matches == nil { - return 0, "", 0, fmt.Errorf("failed to parse package release information (%s)\n%w", releaseInfo, err) - } - - // package release - packageReleaseString := matches[1] - packageReleaseUint64, err := strconv.ParseUint(packageReleaseString, 10 /*base*/, 32 /*size*/) - if err != nil { - return 0, "", 0, fmt.Errorf("failed to parse package release version (%s) into an unsigned integer:\n%w", packageReleaseString, err) - } - packageRelease = uint32(packageReleaseUint64) - - // distro name - distroName = matches[2] - - // distro version - distroVersionString := matches[3] - distroVersionUint64, err := strconv.ParseUint(distroVersionString, 10 /*base*/, 32 /*size*/) - if err != nil { - return 0, "", 0, fmt.Errorf("failed to parse distro version (%s) into an unsigned integer:\n%w", distroVersionString, err) - } - distroVersion = uint32(distroVersionUint64) - - return packageRelease, distroName, distroVersion, nil -} - -func getPackageInformation(imageChroot *safechroot.Chroot, packageName string) (info packageInformation, err error) { - var packageInfo string - err = imageChroot.UnsafeRun(func() error { - packageInfo, _, err = shell.Execute("tdnf", "info", packageName, "--repo", "@system") - return err - }) - if err != nil { - return info, fmt.Errorf("failed to query (%s) package information:\n%w", packageName, err) - } - - // Regular expressions to match Version and Release - versionRegex := regexp.MustCompile(`(?m)^Version\s+:\s+(\S+)`) - versionMatch := versionRegex.FindStringSubmatch(packageInfo) - var packageVersion string - if len(versionMatch) != 2 { - return info, fmt.Errorf("failed to extract version information from the (%s) package information (\n%s\n):\n%w", packageName, packageInfo, err) - } - packageVersion = versionMatch[1] - - // Extract Release - releaseRegex := regexp.MustCompile(`(?m)^Release\s+:\s+(\S+)`) - releaseMatch := releaseRegex.FindStringSubmatch(packageInfo) - var releaseInfo string - if len(releaseMatch) != 2 { - return info, fmt.Errorf("failed to extract release information from the (%s) package information (\n%s\n):\n%w", packageName, packageInfo, err) - } - releaseInfo = releaseMatch[1] - - packageRelease, distroName, distroVersion, err := parseReleaseString(releaseInfo) - if err != nil { - return info, fmt.Errorf("failed to parse release information for package (%s)\n%w", packageName, err) - } - - // Set return values - info.packageVersion = packageVersion - info.packageRelease = packageRelease - info.distroName = distroName - info.distroVersion = distroVersion - - return info, nil -} - func cleanTdnfCache(imageChroot *safechroot.Chroot) error { logger.Log.Infof("Cleaning up RPM cache") // Run all cleanup tasks inside the chroot environment diff --git a/toolkit/tools/pkg/imagecustomizerlib/dracututils.go b/toolkit/tools/pkg/imagecustomizerlib/dracututils.go index 97996ff80..bed64c89d 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/dracututils.go +++ b/toolkit/tools/pkg/imagecustomizerlib/dracututils.go @@ -7,26 +7,11 @@ import ( "fmt" "os" "path/filepath" - "strconv" "github.com/microsoft/azurelinux/toolkit/tools/internal/file" "github.com/microsoft/azurelinux/toolkit/tools/internal/safechroot" ) -const ( - PxeDracutMinVersion = 102 - PxeDracutMinPackageRelease = 7 - PxeDracutDistroName = "azl" - PxeDracutMinDistroVersion = 3 -) - -type DracutPackageInformation struct { - PackageVersion uint32 `yaml:"packageVersion"` - PackageRelease uint32 `yaml:"packageRelease"` - DistroName string `yaml:"distroName"` - DistroVersion uint32 `yaml:"distroVersion"` -} - func addDracutConfig(dracutConfigFile string, lines []string) error { if _, err := os.Stat(dracutConfigFile); os.IsNotExist(err) { err := file.WriteLines(lines, dracutConfigFile) @@ -63,66 +48,3 @@ func addDracutDriver(dracutDriverName string, imageChroot *safechroot.Chroot) er } return addDracutConfig(dracutConfigFile, lines) } - -func getDracutVersion(rootfsSourceDir string) (dracutPackageInfo *DracutPackageInformation, err error) { - chroot := safechroot.NewChroot(rootfsSourceDir, true /*isExistingDir*/) - if chroot == nil { - return nil, fmt.Errorf("failed to create a new chroot object for %s.", rootfsSourceDir) - } - defer chroot.Close(true /*leaveOnDisk*/) - - err = chroot.Initialize("", nil, nil, true /*includeDefaultMounts*/) - if err != nil { - return nil, fmt.Errorf("failed to initialize chroot object for %s:\n%w", rootfsSourceDir, err) - } - - packageName := "dracut" - packageInfo, err := getPackageInformation(chroot, packageName) - if err != nil { - return nil, fmt.Errorf("failed to get package version for (%s):\n%w", packageName, err) - } - versionUint64, err := strconv.ParseUint(packageInfo.packageVersion, 10 /*base*/, 32 /*size*/) - if err != nil { - return nil, fmt.Errorf("failed to parse package version (%s) for (%s) into an unsigned integer:\n%w", packageInfo.packageVersion, packageName, err) - } - - dracutPackageInfo = &DracutPackageInformation{ - PackageVersion: uint32(versionUint64), - PackageRelease: packageInfo.packageRelease, - DistroName: packageInfo.distroName, - DistroVersion: packageInfo.distroVersion, - } - - return dracutPackageInfo, nil -} - -func verifyDracutPXESupport(packageInfo *DracutPackageInformation) error { - if packageInfo == nil { - return fmt.Errorf("no dracut package information provided") - } - - if packageInfo.DistroName != PxeDracutDistroName { - return fmt.Errorf("did not find required Azure Linux distro (%s) - found (%s)", PxeDracutDistroName, packageInfo.DistroName) - } - - if packageInfo.DistroVersion < PxeDracutMinDistroVersion { - return fmt.Errorf("did not find required Azure Linux distro version (%d) - found (%d)", PxeDracutMinDistroVersion, packageInfo.DistroVersion) - } - - // Note that, theoretically, an new distro version could still have an older package version. - // So, it is not sufficient to check that packageInfo.DistroVersion > PxeDracutMinDistroVersion. - // We need to check the package version number. - - if packageInfo.PackageVersion < PxeDracutMinVersion { - return fmt.Errorf("did not find required Dracut package version (%d-%d) - found (%d-%d)", - PxeDracutMinVersion, PxeDracutMinPackageRelease, packageInfo.PackageVersion, packageInfo.PackageRelease) - } else if packageInfo.PackageVersion > PxeDracutMinVersion { - return nil - } - - if packageInfo.PackageRelease < PxeDracutMinPackageRelease { - return fmt.Errorf("did not find required Dracut package release (%d-%d) - found (%d-%d)", - PxeDracutMinVersion, PxeDracutMinPackageRelease, packageInfo.PackageVersion, packageInfo.PackageRelease) - } - return nil -} diff --git a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go index bfda51b49..4d757ead0 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go +++ b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go @@ -423,8 +423,12 @@ func convertWriteableFormatToOutputImage(ic *ImageCustomizerParameters, inputIso case ImageFormatIso: if ic.customizeOSPartitions || inputIsoArtifacts == nil { - err := createLiveOSIsoImage(ic.buildDir, ic.configPath, inputIsoArtifacts, ic.config.Iso, ic.config.Pxe, ic.rawImageFile, - ic.outputImageDir, ic.outputImageBase, ic.outputPXEArtifactsDir) + requestedSELinuxMode := imagecustomizerapi.SELinuxModeDefault + if ic.config.OS != nil { + requestedSELinuxMode = ic.config.OS.SELinux.Mode + } + err := createLiveOSIsoImage(ic.buildDir, ic.configPath, inputIsoArtifacts, requestedSELinuxMode, ic.config.Iso, ic.config.Pxe, + ic.rawImageFile, ic.outputImageDir, ic.outputImageBase, ic.outputPXEArtifactsDir) if err != nil { return fmt.Errorf("failed to create LiveOS iso image:\n%w", err) } diff --git a/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go b/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go index 92d40ee39..045e4c495 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go +++ b/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go @@ -80,7 +80,7 @@ const ( savedConfigsFileName = "saved-configs.yaml" savedConfigsFileNamePath = "/" + savedConfigsDir + "/" + savedConfigsFileName - dracutConfig = `add_dracutmodules+=" dmsquash-live livenet " + dracutConfig = `add_dracutmodules+=" dmsquash-live livenet selinux " add_drivers+=" overlay " hostonly="no" ` @@ -105,17 +105,18 @@ type IsoWorkingDirs struct { // `IsoArtifacts` holds the extracted/generated artifacts necessary to build // a LiveOS ISO image. type IsoArtifacts struct { - kernelVersion string - dracutPackageInfo *DracutPackageInformation - bootx64EfiPath string - grubx64EfiPath string - isoGrubCfgPath string - pxeGrubCfgPath string - savedConfigsFilePath string - vmlinuzPath string - initrdImagePath string - squashfsImagePath string - additionalFiles map[string]string // local-build-path -> iso-media-path + kernelVersion string + dracutPackageInfo *PackageVersionInformation + selinuxPolicyPackageInfo *PackageVersionInformation + bootx64EfiPath string + grubx64EfiPath string + isoGrubCfgPath string + pxeGrubCfgPath string + savedConfigsFilePath string + vmlinuzPath string + initrdImagePath string + squashfsImagePath string + additionalFiles map[string]string // local-build-path -> iso-media-path } type LiveOSIsoBuilder struct { @@ -310,18 +311,26 @@ func (b *LiveOSIsoBuilder) prepareRootfsForDracut(writeableRootfsDir string) err // kernel argument specified by the user in this run. // - newPxeIsoImageUrl: // PXE ISO image URL specified by the user in this run. -// - newOSDracutVersion: -// Dracut package version of the rootfs provided by the user. +// - newDracutPackageInfo: +// Dracut package version in the rootfs provided by the user. +// - requestedSelinuxMode: +// selinux mode as specified by the user. +// - newSELinuxPackageInfo: +// selinux-policy package version in the rootfs provided by the user. // // outputs: // - returns a SavedConfigs objects with the new merged values. func updateSavedConfigs(savedConfigsFilePath string, newKernelArgs []string, - newPxeIsoImageBaseUrl string, newPxeIsoImageFileUrl string, newDracutPackageInfo *DracutPackageInformation) (updatedSavedConfigs *SavedConfigs, err error) { + newPxeIsoImageBaseUrl string, newPxeIsoImageFileUrl string, newDracutPackageInfo *PackageVersionInformation, + requestedSelinuxMode imagecustomizerapi.SELinuxMode, newSELinuxPackageInfo *PackageVersionInformation, +) (updatedSavedConfigs *SavedConfigs, err error) { updatedSavedConfigs = &SavedConfigs{} updatedSavedConfigs.Iso.KernelCommandLine.ExtraCommandLine = newKernelArgs updatedSavedConfigs.Pxe.IsoImageBaseUrl = newPxeIsoImageBaseUrl updatedSavedConfigs.Pxe.IsoImageFileUrl = newPxeIsoImageFileUrl updatedSavedConfigs.OS.DracutPackageInfo = newDracutPackageInfo + updatedSavedConfigs.OS.RequestedSELinuxMode = requestedSelinuxMode + updatedSavedConfigs.OS.SELinuxPolicyPackageInfo = newSELinuxPackageInfo savedConfigs, err := loadSavedConfigs(savedConfigsFilePath) if err != nil { @@ -369,6 +378,12 @@ func updateSavedConfigs(savedConfigsFilePath string, newKernelArgs []string, if newDracutPackageInfo == nil { updatedSavedConfigs.OS.DracutPackageInfo = savedConfigs.OS.DracutPackageInfo } + if requestedSelinuxMode != imagecustomizerapi.SELinuxModeDefault { + updatedSavedConfigs.OS.RequestedSELinuxMode = savedConfigs.OS.RequestedSELinuxMode + } + if newSELinuxPackageInfo == nil { + updatedSavedConfigs.OS.SELinuxPolicyPackageInfo = savedConfigs.OS.SELinuxPolicyPackageInfo + } } err = updatedSavedConfigs.persistSavedConfigs(savedConfigsFilePath) @@ -380,7 +395,7 @@ func updateSavedConfigs(savedConfigsFilePath string, newKernelArgs []string, } func (b *LiveOSIsoBuilder) updateGrubCfg(isoGrubCfgFileName string, pxeGrubCfgFileName string, - savedConfigs *SavedConfigs, outputImageBase string) error { + disableSELinux bool, savedConfigs *SavedConfigs, outputImageBase string) error { inputContentString, err := file.Read(isoGrubCfgFileName) if err != nil { @@ -434,10 +449,12 @@ func (b *LiveOSIsoBuilder) updateGrubCfg(isoGrubCfgFileName string, pxeGrubCfgFi return fmt.Errorf("failed to update the root kernel argument in the iso grub.cfg:\n%w", err) } - inputContentString, err = updateSELinuxCommandLineHelperAll(inputContentString, imagecustomizerapi.SELinuxModeDisabled, - true /*allowMultiple*/, false /*requireKernelOpts*/) - if err != nil { - return fmt.Errorf("failed to set SELinux mode:\n%w", err) + if disableSELinux { + inputContentString, err = updateSELinuxCommandLineHelperAll(inputContentString, imagecustomizerapi.SELinuxModeDisabled, + true /*allowMultiple*/, false /*requireKernelOpts*/) + if err != nil { + return fmt.Errorf("failed to set SELinux mode:\n%w", err) + } } liveosKernelArgs := fmt.Sprintf(kernelArgsLiveOSTemplate, liveOSDir, liveOSImage) @@ -762,6 +779,20 @@ func (b *LiveOSIsoBuilder) findKernelVersion(writeableRootfsDir string) error { return nil } +func getSELinuxMode(imageChroot *safechroot.Chroot) (imagecustomizerapi.SELinuxMode, error) { + bootCustomizer, err := NewBootCustomizer(imageChroot) + if err != nil { + return imagecustomizerapi.SELinuxModeDefault, err + } + + imageSELinuxMode, err := bootCustomizer.GetSELinuxMode(imageChroot) + if err != nil { + return imagecustomizerapi.SELinuxModeDefault, fmt.Errorf("failed to get current SELinux mode:\n%w", err) + } + + return imageSELinuxMode, nil +} + // prepareLiveOSDir // // given a rootfs, this function: @@ -781,6 +812,8 @@ func (b *LiveOSIsoBuilder) findKernelVersion(writeableRootfsDir string) error { // 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. +// - 'requestedSelinuxMode' +// requested selinux mode by the user (from os.selinux.mode). // - 'extraCommandLine': // extra kernel command line arguments to add to grub. // - 'pxeIsoImageBaseUrl': @@ -796,8 +829,8 @@ func (b *LiveOSIsoBuilder) findKernelVersion(writeableRootfsDir string) error { // - customized writeableRootfsDir (new files, deleted files, etc) // - extracted artifacts func (b *LiveOSIsoBuilder) prepareLiveOSDir(inputSavedConfigsFilePath string, writeableRootfsDir string, - isoMakerArtifactsStagingDir string, extraCommandLine []string, pxeIsoImageBaseUrl string, - pxeIsoImageFileUrl string, outputImageBase string) error { + isoMakerArtifactsStagingDir string, requestedSelinuxMode imagecustomizerapi.SELinuxMode, extraCommandLine []string, + pxeIsoImageBaseUrl string, pxeIsoImageFileUrl string, outputImageBase string) error { logger.Log.Debugf("Creating LiveOS squashfs image") @@ -806,11 +839,37 @@ func (b *LiveOSIsoBuilder) prepareLiveOSDir(inputSavedConfigsFilePath string, wr return err } - b.artifacts.dracutPackageInfo, err = getDracutVersion(writeableRootfsDir) + chroot := safechroot.NewChroot(writeableRootfsDir, true /*isExistingDir*/) + if chroot == nil { + return fmt.Errorf("failed to create a new chroot object for %s.", writeableRootfsDir) + } + defer chroot.Close(true /*leaveOnDisk*/) + + err = chroot.Initialize("", nil, nil, true /*includeDefaultMounts*/) + if err != nil { + return fmt.Errorf("failed to initialize chroot object for %s:\n%w", writeableRootfsDir, err) + } + + imageSELinuxMode, err := getSELinuxMode(chroot) + if err != nil { + return err + } + + b.artifacts.dracutPackageInfo, err = getPackageInformation(chroot, "dracut") if err != nil { return err } + // Note the MIC allows the user to install other selinux policy packages. + // So, the absence of selinux-policy does not mean that there are no selinux + // policy packages. + if isPackageInstalled(chroot, "selinux-policy") { + b.artifacts.selinuxPolicyPackageInfo, err = getPackageInformation(chroot, "selinux-policy") + if err != nil { + return err + } + } + err = b.extractBootDirFiles(writeableRootfsDir) if err != nil { return err @@ -827,13 +886,37 @@ func (b *LiveOSIsoBuilder) prepareLiveOSDir(inputSavedConfigsFilePath string, wr } } + // Combine the current state updatedSavedConfigs, err := updateSavedConfigs(b.artifacts.savedConfigsFilePath, extraCommandLine, pxeIsoImageBaseUrl, - pxeIsoImageFileUrl, b.artifacts.dracutPackageInfo) + pxeIsoImageFileUrl, b.artifacts.dracutPackageInfo, requestedSelinuxMode, b.artifacts.selinuxPolicyPackageInfo) if err != nil { return fmt.Errorf("failed to combine saved configurations with new configuration:\n%w", err) } - err = b.updateGrubCfg(b.artifacts.isoGrubCfgPath, b.artifacts.pxeGrubCfgPath, updatedSavedConfigs, outputImageBase) + // Figure out the selinux situation + // Note that by now, the user selinux config has been applied to the image, + // so checking only 'imageSELinuxMode' is sufficient to determine whether + // selinux is enabled or not for this image (regardless of the source of + // that configuration). + disableSELinux := false + if imageSELinuxMode != imagecustomizerapi.SELinuxModeDisabled { + // SELinux is enabled (either in the base image, or requested by the user) + err = verifyNoLiveOsSelinuxBlockers(updatedSavedConfigs.OS.DracutPackageInfo, updatedSavedConfigs.OS.SELinuxPolicyPackageInfo) + if err != nil { + // We need to determine whether the source of enablment is user + // explicit configuration or the base image. + if updatedSavedConfigs.OS.RequestedSELinuxMode != imagecustomizerapi.SELinuxModeDisabled && + updatedSavedConfigs.OS.RequestedSELinuxMode != imagecustomizerapi.SELinuxModeDefault { + return fmt.Errorf("SELinux cannot be enabled due to older dracut and selinux-policy package versions:\n%w", err) + } else { + logger.Log.Warnf("SELinux cannot be enabled due to older dracut and selinux-policy package versions:\n%s", err) + } + + disableSELinux = true + } + } + + err = b.updateGrubCfg(b.artifacts.isoGrubCfgPath, b.artifacts.pxeGrubCfgPath, disableSELinux, updatedSavedConfigs, outputImageBase) if err != nil { return fmt.Errorf("failed to update grub.cfg:\n%w", err) } @@ -876,7 +959,8 @@ func (b *LiveOSIsoBuilder) createSquashfsImage(writeableRootfsDir string) error } } - mksquashfsParams := []string{writeableRootfsDir, squashfsImagePath} + // '-xattrs' allows SELinux labeling to be retained within the squashfs. + mksquashfsParams := []string{writeableRootfsDir, squashfsImagePath, "-xattrs"} err = shell.ExecuteLive(false, "mksquashfs", mksquashfsParams...) if err != nil { return fmt.Errorf("failed to create squashfs:\n%w", err) @@ -963,6 +1047,8 @@ 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). +// - 'requestedSelinuxMode' +// requested selinux mode by the user (from os.selinux.mode). // - 'extraCommandLine': // extra kernel command line arguments to add to grub. // - 'pxeIsoImageBaseUrl': @@ -979,9 +1065,8 @@ func (b *LiveOSIsoBuilder) generateInitrdImage(rootfsSourceDir, artifactsSourceD // `LiveOSIsoBuilder.workingDirs.isoArtifactsDir` folder. // - the paths to individual artifaces are found in the // `LiveOSIsoBuilder.artifacts` data structure. -func (b *LiveOSIsoBuilder) prepareArtifactsFromFullImage(inputSavedConfigsFilePath string, rawImageFile string, extraCommandLine []string, - pxeIsoImageBaseUrl string, pxeIsoImageFileUrl string, outputImageBase string) error { - +func (b *LiveOSIsoBuilder) prepareArtifactsFromFullImage(inputSavedConfigsFilePath string, rawImageFile string, requestedSelinuxMode imagecustomizerapi.SELinuxMode, + extraCommandLine []string, pxeIsoImageBaseUrl string, pxeIsoImageFileUrl string, outputImageBase string) error { logger.Log.Infof("Preparing iso artifacts") logger.Log.Debugf("Connecting to raw image (%s)", rawImageFile) @@ -999,7 +1084,7 @@ func (b *LiveOSIsoBuilder) prepareArtifactsFromFullImage(inputSavedConfigsFilePa isoMakerArtifactsStagingDir := "/boot-staging" err = b.prepareLiveOSDir(inputSavedConfigsFilePath, writeableRootfsDir, isoMakerArtifactsStagingDir, - extraCommandLine, pxeIsoImageBaseUrl, pxeIsoImageFileUrl, outputImageBase) + requestedSelinuxMode, extraCommandLine, pxeIsoImageBaseUrl, pxeIsoImageFileUrl, outputImageBase) if err != nil { return fmt.Errorf("failed to convert rootfs folder to a LiveOS folder:\n%w", err) } @@ -1217,8 +1302,9 @@ func micIsoConfigToIsoMakerConfig(baseConfigPath string, isoConfig *imagecustomi // outputs: // // creates a LiveOS ISO image. -func createLiveOSIsoImage(buildDir, baseConfigPath string, inputIsoArtifacts *LiveOSIsoBuilder, isoConfig *imagecustomizerapi.Iso, - pxeConfig *imagecustomizerapi.Pxe, rawImageFile, outputImageDir, outputImageBase string, outputPXEArtifactsDir string) (err error) { +func createLiveOSIsoImage(buildDir, baseConfigPath string, inputIsoArtifacts *LiveOSIsoBuilder, requestedSelinuxMode imagecustomizerapi.SELinuxMode, + isoConfig *imagecustomizerapi.Iso, pxeConfig *imagecustomizerapi.Pxe, rawImageFile, outputImageDir, outputImageBase string, + outputPXEArtifactsDir string) (err error) { additionalIsoFiles, extraCommandLine, err := micIsoConfigToIsoMakerConfig(baseConfigPath, isoConfig) if err != nil { @@ -1275,7 +1361,8 @@ func createLiveOSIsoImage(buildDir, baseConfigPath string, inputIsoArtifacts *Li inputSavedConfigsFilePath = inputIsoArtifacts.artifacts.savedConfigsFilePath } - err = isoBuilder.prepareArtifactsFromFullImage(inputSavedConfigsFilePath, rawImageFile, extraCommandLine, pxeIsoImageBaseUrl, pxeIsoImageFileUrl, outputImageBase) + err = isoBuilder.prepareArtifactsFromFullImage(inputSavedConfigsFilePath, rawImageFile, requestedSelinuxMode, extraCommandLine, + pxeIsoImageBaseUrl, pxeIsoImageFileUrl, outputImageBase) if err != nil { return err } @@ -1557,8 +1644,13 @@ func (b *LiveOSIsoBuilder) createImageFromUnchangedOS(baseConfigPath string, iso pxeIsoImageFileUrl = pxeConfig.IsoImageFileUrl } + // Note that in this ISO build flow, there is no os configuration, and hence + // no selinux configuration. So, we will set it to default (i.e. unspecified) + // and let any saved data override if present. + requestedSelinuxMode := imagecustomizerapi.SELinuxModeDefault + updatedSavedConfigs, err := updateSavedConfigs(b.artifacts.savedConfigsFilePath, extraCommandLine, pxeIsoImageBaseUrl, - pxeIsoImageFileUrl, b.artifacts.dracutPackageInfo) + pxeIsoImageFileUrl, nil /*dracut pkg info*/, requestedSelinuxMode, nil /*selinux policy pkg info*/) if err != nil { return fmt.Errorf("failed to combine saved configurations with new configuration:\n%w", err) } @@ -1567,8 +1659,16 @@ func (b *LiveOSIsoBuilder) createImageFromUnchangedOS(baseConfigPath string, iso // since we will not expand the rootfs and inspect its contents to get // such information. b.artifacts.dracutPackageInfo = updatedSavedConfigs.OS.DracutPackageInfo + b.artifacts.selinuxPolicyPackageInfo = updatedSavedConfigs.OS.SELinuxPolicyPackageInfo + + // SELinux cannot be enabled/disabled in this flow since, by definition, + // the config os.selinux is not present. As a result, we will just keep + // SELinux configuration unchanged. + // Setting disableSELinux to false tells updateGrubCfg to, well, not disable + // selinux and not enable it either. + disableSELinux := false - err = b.updateGrubCfg(b.artifacts.isoGrubCfgPath, b.artifacts.pxeGrubCfgPath, updatedSavedConfigs, outputImageBase) + err = b.updateGrubCfg(b.artifacts.isoGrubCfgPath, b.artifacts.pxeGrubCfgPath, disableSELinux, updatedSavedConfigs, outputImageBase) if err != nil { return fmt.Errorf("failed to update grub.cfg:\n%w", err) } diff --git a/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder_test.go b/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder_test.go index ce8f33f61..6d60dbb00 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder_test.go +++ b/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder_test.go @@ -172,7 +172,7 @@ func TestCustomizeImageLiveCd1(t *testing.T) { pxeArtifactsPathIsoToIso) } -func VerifyPXEArtifacts(t *testing.T, packageInfo *DracutPackageInformation, isoMountDir string, pxeKernelIpArg string, +func VerifyPXEArtifacts(t *testing.T, packageInfo *PackageVersionInformation, isoMountDir string, pxeKernelIpArg string, pxeKernelRootArgV2 string, pxeArtifactsPathIsoToIso string) { // Check if PXE support is present in the Dracut package version in use. diff --git a/toolkit/tools/pkg/imagecustomizerlib/liveosisoutils.go b/toolkit/tools/pkg/imagecustomizerlib/liveosisoutils.go new file mode 100644 index 000000000..382e39c82 --- /dev/null +++ b/toolkit/tools/pkg/imagecustomizerlib/liveosisoutils.go @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package imagecustomizerlib + +import ( + "fmt" +) + +const ( + // Minimum dracut version required to enable PXE booting. + LiveOsPxeDracutMinVersion = 102 + LiveOsPxeDracutMinPackageRelease = 7 + LiveOsPxeDracutDistroName = "azl" + LiveOsPxeDracutMinDistroVersion = 3 + + // Minumum dracut version required to enable SELinux. + LiveOsSelinuxDracutMinVersion = 102 + LiveOsSelinuxDracutMinPackageRelease = 8 + LiveOsSelinuxDracutDistroName = "azl" + LiveOsSelinuxDracutMinDistroVersion = 3 + + // Minimum selinux-poicy version required to enable SELinux. + LiveOsSelinuxPolicyMinVersion0 = 2 + LiveOsSelinuxPolicyMinVersion1 = 20240226 + LiveOsSelinuxPolicyMinPackageRelease = 9 + LiveOsSelinuxPolicyDistroName = "azl" + LiveOsSelinuxPolicyMinDistroVersion = 3 +) + +// verifies that the dracut package supports PXE booting for LiveOS images. +func verifyDracutPXESupport(dracutVersionInfo *PackageVersionInformation) error { + minimumVersionPackageInfo := &PackageVersionInformation{ + PackageVersionComponents: []uint64{LiveOsPxeDracutMinVersion}, + PackageRelease: LiveOsPxeDracutMinPackageRelease, + DistroName: LiveOsPxeDracutDistroName, + DistroVersion: LiveOsPxeDracutMinDistroVersion, + } + packageName := "dracut" + err := dracutVersionInfo.verifyMinimumVersion(minimumVersionPackageInfo) + if err != nil { + return fmt.Errorf("did not find the minimum (%s) required version to support PXE boot with LiveOS ISOs:\n%w", packageName, err) + } + return nil +} + +// verifies that the dracut package supports enabling SELinux for LiveOS images. +func verifyDracutLiveOsSELinuxSupport(dracutVersionInfo *PackageVersionInformation) error { + minimumVersionPackageInfo := &PackageVersionInformation{ + PackageVersionComponents: []uint64{LiveOsSelinuxDracutMinVersion}, + PackageRelease: LiveOsSelinuxDracutMinPackageRelease, + DistroName: LiveOsSelinuxDracutDistroName, + DistroVersion: LiveOsSelinuxDracutMinDistroVersion, + } + packageName := "dracut" + err := dracutVersionInfo.verifyMinimumVersion(minimumVersionPackageInfo) + if err != nil { + return fmt.Errorf("did not find the minimum (%s) required version to support SELinux with LiveOS ISOs:\n%w", packageName, err) + } + return nil +} + +// verifies that the selinux-policy supports LiveOS images. +func verifySelinuxPolicyLiveOsSupport(selinuxPolicyVersionInfo *PackageVersionInformation) error { + minimumVersionPackageInfo := &PackageVersionInformation{ + PackageVersionComponents: []uint64{LiveOsSelinuxPolicyMinVersion0, LiveOsSelinuxPolicyMinVersion1}, + PackageRelease: LiveOsSelinuxPolicyMinPackageRelease, + DistroName: LiveOsSelinuxPolicyDistroName, + DistroVersion: LiveOsSelinuxPolicyMinDistroVersion, + } + packageName := "selinux-policy" + err := selinuxPolicyVersionInfo.verifyMinimumVersion(minimumVersionPackageInfo) + if err != nil { + return fmt.Errorf("did not find the minimum (%s) required version to support SELinux with LiveOS ISOs:\n%w", packageName, err) + } + return nil +} + +// verifies that SELinux is can work for LiveOS images. +func verifyNoLiveOsSelinuxBlockers(dracutVersionInfo *PackageVersionInformation, selinuxPolicyVersionInfo *PackageVersionInformation) error { + if dracutVersionInfo != nil { + err := verifyDracutLiveOsSELinuxSupport(dracutVersionInfo) + if err != nil { + return err + } + } else { + return fmt.Errorf("dracut package information is missing") + } + + // selinuxPolicyVersionInfo is nil when selinux-policy is not installed. + // If selinux is enabled, and selinux-policy is not installed, it means that + // the user has a policy installed through a package unknown to us. + // We will not report an error in such cases. + if selinuxPolicyVersionInfo != nil { + err := verifySelinuxPolicyLiveOsSupport(selinuxPolicyVersionInfo) + if err != nil { + return err + } + } + + return nil +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/packageinformation.go b/toolkit/tools/pkg/imagecustomizerlib/packageinformation.go new file mode 100644 index 000000000..a8529caba --- /dev/null +++ b/toolkit/tools/pkg/imagecustomizerlib/packageinformation.go @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package imagecustomizerlib + +import ( + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/microsoft/azurelinux/toolkit/tools/internal/safechroot" + "github.com/microsoft/azurelinux/toolkit/tools/internal/shell" +) + +type PackageVersionInformation struct { + PackageVersionComponents []uint64 `yaml:"PackageVersionComponents"` + PackageRelease uint32 `yaml:"PackageRelease"` + DistroName string `yaml:"DistroName"` + DistroVersion uint32 `yaml:"DistroVersion"` +} + +func (pi *PackageVersionInformation) getVersionString() string { + var version strings.Builder + for i, versionComponent := range pi.PackageVersionComponents { + if i != 0 { + version.WriteString(".") + } + version.WriteString(strconv.FormatUint(versionComponent, 10)) + } + return version.String() +} + +func (pi *PackageVersionInformation) getFullVersionString() string { + // yy.yy.yy-zz.azl3 + return fmt.Sprintf("%s-%d.%s%d", pi.getVersionString(), pi.PackageRelease, pi.DistroName, pi.DistroVersion) +} + +func (pi *PackageVersionInformation) verifyMinimumVersion(minimumVersionInfo *PackageVersionInformation) error { + if minimumVersionInfo == nil { + panic("input package information undefined") + } + + minimumVersion := minimumVersionInfo.getFullVersionString() + currentVersion := pi.getFullVersionString() + + if pi.DistroName != minimumVersionInfo.DistroName { + return fmt.Errorf("did not find required distro (%s) - found (%s)", minimumVersion, currentVersion) + } + + if pi.DistroVersion < minimumVersionInfo.DistroVersion { + return fmt.Errorf("did not find required distro version (%s) (or newer) - found (%s)", minimumVersion, currentVersion) + } + + // Note that, theoretically, a newer distro version could still have an older package version. + // So, it is not sufficient to check that packageInfo.DistroVersion > MinDistroVersion. + // We need to check the package version number. + + if len(pi.PackageVersionComponents) != len(minimumVersionInfo.PackageVersionComponents) { + return fmt.Errorf("unexpected number of version components (%s) - found (%s)", minimumVersion, currentVersion) + } + + for i, versionComponent := range pi.PackageVersionComponents { + if versionComponent < minimumVersionInfo.PackageVersionComponents[i] { + return fmt.Errorf("did not find required package version (%s) (or newer) - found (%s)", minimumVersion, currentVersion) + } else if versionComponent > minimumVersionInfo.PackageVersionComponents[i] { + return nil + } + } + + if pi.PackageRelease < minimumVersionInfo.PackageRelease { + return fmt.Errorf("did not find required package release version (%s) (or newer) - found (%s)", minimumVersion, currentVersion) + } + + return nil +} + +func parseReleaseString(releaseInfo string) (packageRelease uint32, distroName string, distroVersion uint32, err error) { + pattern := `([0-9]+)\.([a-zA-Z]+)([0-9]+)` + re := regexp.MustCompile(pattern) + matches := re.FindStringSubmatch(releaseInfo) + + if matches == nil { + return 0, "", 0, fmt.Errorf("failed to parse package release information (%s)\n%w", releaseInfo, err) + } + + // package release + packageReleaseString := matches[1] + packageReleaseUint64, err := strconv.ParseUint(packageReleaseString, 10 /*base*/, 32 /*size*/) + if err != nil { + return 0, "", 0, fmt.Errorf("failed to parse package release version (%s) into an unsigned integer:\n%w", packageReleaseString, err) + } + packageRelease = uint32(packageReleaseUint64) + + // distro name + distroName = matches[2] + + // distro version + distroVersionString := matches[3] + distroVersionUint64, err := strconv.ParseUint(distroVersionString, 10 /*base*/, 32 /*size*/) + if err != nil { + return 0, "", 0, fmt.Errorf("failed to parse distro version (%s) into an unsigned integer:\n%w", distroVersionString, err) + } + distroVersion = uint32(distroVersionUint64) + + return packageRelease, distroName, distroVersion, nil +} + +func parseVersionString(version string) ([]uint64, error) { + // Regular expression to capture version components + // Expected patterns are: "number(.number)*" + re := regexp.MustCompile(`^(\d+)(?:\.(\d+))*$`) + + // Match the version string against the regex + matches := re.FindStringSubmatch(version) + if matches == nil { + return nil, fmt.Errorf("invalid version format: %s", version) + } + + // Extract all captured groups + var versionComponents []uint64 + for _, match := range matches[1:] { + // Given a pattern is meant to match zero or more time: + // - when it does not match (i.e. matches 0 times), golang still adds + // an empty match. + // So, for versions like "102", the second group in the regex will + // not match (i.e. no ".xyz"), and an empty match will be inserted. + if match == "" { + continue + } + versionComponent, err := strconv.ParseUint(match, 10 /*base*/, 64 /*size*/) + if err != nil { + return nil, fmt.Errorf("failed to parse package version component (%s) into an unsigned integer:\n%w", match, err) + } + versionComponents = append(versionComponents, versionComponent) + } + + return versionComponents, nil +} + +func getPackageInformation(imageChroot *safechroot.Chroot, packageName string) (info *PackageVersionInformation, err error) { + var packageInfo string + err = imageChroot.UnsafeRun(func() error { + packageInfo, _, err = shell.Execute("tdnf", "info", packageName, "--repo", "@system") + return err + }) + if err != nil { + return nil, fmt.Errorf("failed to query (%s) package information:\n%w", packageName, err) + } + + // Regular expressions to match Version and Release + versionRegex := regexp.MustCompile(`(?m)^Version\s+:\s+(\S+)`) + versionMatch := versionRegex.FindStringSubmatch(packageInfo) + var packageVersion string + if len(versionMatch) != 2 { + return nil, fmt.Errorf("failed to extract version information from the (%s) package information (\n%s\n):\n%w", packageName, packageInfo, err) + } + packageVersion = versionMatch[1] + + versionComponents, err := parseVersionString(packageVersion) + if err != nil { + return nil, fmt.Errorf("failed to parse the (%s) package version information (%s):\n%w", packageName, packageVersion, err) + } + + // Extract Release + releaseRegex := regexp.MustCompile(`(?m)^Release\s+:\s+(\S+)`) + releaseMatch := releaseRegex.FindStringSubmatch(packageInfo) + var releaseInfo string + if len(releaseMatch) != 2 { + return nil, fmt.Errorf("failed to extract release information from the (%s) package information (\n%s\n):\n%w", packageName, packageInfo, err) + } + releaseInfo = releaseMatch[1] + + packageRelease, distroName, distroVersion, err := parseReleaseString(releaseInfo) + if err != nil { + return nil, fmt.Errorf("failed to parse release information for package (%s)\n%w", packageName, err) + } + + // Set return values + info = &PackageVersionInformation{ + PackageVersionComponents: versionComponents, + PackageRelease: packageRelease, + DistroName: distroName, + DistroVersion: distroVersion, + } + + return info, nil +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/savedconfigs.go b/toolkit/tools/pkg/imagecustomizerlib/savedconfigs.go index d44ab54a2..48b79ecf4 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/savedconfigs.go +++ b/toolkit/tools/pkg/imagecustomizerlib/savedconfigs.go @@ -61,7 +61,9 @@ func (p *PxeSavedConfigs) IsValid() error { } type OSSavedConfigs struct { - DracutPackageInfo *DracutPackageInformation `yaml:"dracutPackage"` + DracutPackageInfo *PackageVersionInformation `yaml:"dracutPackage"` + RequestedSELinuxMode imagecustomizerapi.SELinuxMode `yaml:"selinuxRequestedMode"` + SELinuxPolicyPackageInfo *PackageVersionInformation `yaml:"selinuxPolicyPackage"` } func (i *OSSavedConfigs) IsValid() error {