package rhel8 import ( "fmt" "math/rand" "path" "path/filepath" "github.com/osbuild/osbuild-composer/internal/blueprint" "github.com/osbuild/osbuild-composer/internal/common" "github.com/osbuild/osbuild-composer/internal/container" "github.com/osbuild/osbuild-composer/internal/disk" "github.com/osbuild/osbuild-composer/internal/distro" "github.com/osbuild/osbuild-composer/internal/osbuild" "github.com/osbuild/osbuild-composer/internal/rpmmd" "github.com/osbuild/osbuild-composer/internal/users" ) func prependKernelCmdlineStage(pipeline *osbuild.Pipeline, t *imageType, pt *disk.PartitionTable) *osbuild.Pipeline { if t.Arch().Name() == distro.S390xArchName { rootFs := pt.FindMountable("/") if rootFs == nil { panic("s390x image must have a root filesystem, this is a programming error") } kernelStage := osbuild.NewKernelCmdlineStage(osbuild.NewKernelCmdlineStageOptions(rootFs.GetFSSpec().UUID, t.kernelOptions)) pipeline.Stages = append([]*osbuild.Stage{kernelStage}, pipeline.Stages...) } return pipeline } func vmdkPipelines(t *imageType, customizations *blueprint.Customizations, options distro.ImageOptions, repos []rpmmd.RepoConfig, packageSetSpecs map[string][]rpmmd.PackageSpec, containers []container.Spec, rng *rand.Rand) ([]osbuild.Pipeline, error) { pipelines := make([]osbuild.Pipeline, 0) pipelines = append(pipelines, *buildPipeline(repos, packageSetSpecs[buildPkgsKey], t.arch.distro.runner.String())) partitionTable, err := t.getPartitionTable(customizations.GetFilesystems(), options, rng) if err != nil { return nil, err } treePipeline, err := osPipeline(t, repos, packageSetSpecs[osPkgsKey], containers, customizations, options, partitionTable) if err != nil { return nil, err } pipelines = append(pipelines, *treePipeline) diskfile := "disk.img" kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[osPkgsKey], customizations.GetKernel().Name) imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, partitionTable, t.arch, kernelVer) pipelines = append(pipelines, *imagePipeline) if err != nil { return nil, err } qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, osbuild.QEMUFormatVMDK, osbuild.VMDKOptions{Subformat: osbuild.VMDKSubformatStreamOptimized}) pipelines = append(pipelines, *qemuPipeline) return pipelines, nil } func ec2CommonPipelines(t *imageType, customizations *blueprint.Customizations, options distro.ImageOptions, repos []rpmmd.RepoConfig, packageSetSpecs map[string][]rpmmd.PackageSpec, containers []container.Spec, rng *rand.Rand, diskfile string) ([]osbuild.Pipeline, error) { pipelines := make([]osbuild.Pipeline, 0) pipelines = append(pipelines, *buildPipeline(repos, packageSetSpecs[buildPkgsKey], t.arch.distro.runner.String())) partitionTable, err := t.getPartitionTable(customizations.GetFilesystems(), options, rng) if err != nil { return nil, err } treePipeline, err := osPipeline(t, repos, packageSetSpecs[osPkgsKey], containers, customizations, options, partitionTable) if err != nil { return nil, err } pipelines = append(pipelines, *treePipeline) kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[osPkgsKey], customizations.GetKernel().Name) imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, partitionTable, t.arch, kernelVer) pipelines = append(pipelines, *imagePipeline) return pipelines, nil } // ec2Pipelines returns pipelines which produce uncompressed EC2 images which are expected to use RHSM for content func ec2Pipelines(t *imageType, customizations *blueprint.Customizations, options distro.ImageOptions, repos []rpmmd.RepoConfig, packageSetSpecs map[string][]rpmmd.PackageSpec, containers []container.Spec, rng *rand.Rand) ([]osbuild.Pipeline, error) { return ec2CommonPipelines(t, customizations, options, repos, packageSetSpecs, containers, rng, t.Filename()) } // rhelEc2Pipelines returns pipelines which produce XZ-compressed EC2 images which are expected to use RHUI for content func rhelEc2Pipelines(t *imageType, customizations *blueprint.Customizations, options distro.ImageOptions, repos []rpmmd.RepoConfig, packageSetSpecs map[string][]rpmmd.PackageSpec, containers []container.Spec, rng *rand.Rand) ([]osbuild.Pipeline, error) { rawImageFilename := "image.raw" pipelines, err := ec2CommonPipelines(t, customizations, options, repos, packageSetSpecs, containers, rng, rawImageFilename) if err != nil { return nil, err } lastPipeline := pipelines[len(pipelines)-1] pipelines = append(pipelines, *xzArchivePipeline(lastPipeline.Name, rawImageFilename, t.Filename())) return pipelines, nil } func gcePipelines(t *imageType, customizations *blueprint.Customizations, options distro.ImageOptions, repos []rpmmd.RepoConfig, packageSetSpecs map[string][]rpmmd.PackageSpec, containers []container.Spec, rng *rand.Rand) ([]osbuild.Pipeline, error) { pipelines := make([]osbuild.Pipeline, 0) pipelines = append(pipelines, *buildPipeline(repos, packageSetSpecs[buildPkgsKey], t.arch.distro.runner.String())) partitionTable, err := t.getPartitionTable(customizations.GetFilesystems(), options, rng) if err != nil { return nil, err } treePipeline, err := osPipeline(t, repos, packageSetSpecs[osPkgsKey], containers, customizations, options, partitionTable) if err != nil { return nil, err } pipelines = append(pipelines, *treePipeline) diskfile := "disk.raw" kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[osPkgsKey], customizations.GetKernel().Name) imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, partitionTable, t.arch, kernelVer) pipelines = append(pipelines, *imagePipeline) archivePipeline := tarArchivePipeline("archive", imagePipeline.Name, &osbuild.TarStageOptions{ Filename: t.Filename(), Format: osbuild.TarArchiveFormatOldgnu, RootNode: osbuild.TarRootNodeOmit, // import of the image to GCP fails in case the options below are enabled, which is the default ACLs: common.ToPtr(false), SELinux: common.ToPtr(false), Xattrs: common.ToPtr(false), }) pipelines = append(pipelines, *archivePipeline) return pipelines, nil } func tarPipelines(t *imageType, customizations *blueprint.Customizations, options distro.ImageOptions, repos []rpmmd.RepoConfig, packageSetSpecs map[string][]rpmmd.PackageSpec, containers []container.Spec, rng *rand.Rand) ([]osbuild.Pipeline, error) { pipelines := make([]osbuild.Pipeline, 0) pipelines = append(pipelines, *buildPipeline(repos, packageSetSpecs[buildPkgsKey], t.arch.distro.runner.String())) treePipeline, err := osPipeline(t, repos, packageSetSpecs[osPkgsKey], containers, customizations, options, nil) if err != nil { return nil, err } pipelines = append(pipelines, *treePipeline) tarPipeline := tarArchivePipeline("root-tar", treePipeline.Name, &osbuild.TarStageOptions{Filename: "root.tar.xz"}) pipelines = append(pipelines, *tarPipeline) return pipelines, nil } // makeISORootPath return a path that can be used to address files and folders in // the root of the iso func makeISORootPath(p string) string { fullpath := path.Join("/run/install/repo", p) return fmt.Sprintf("file://%s", fullpath) } func edgeInstallerPipelines(t *imageType, customizations *blueprint.Customizations, options distro.ImageOptions, repos []rpmmd.RepoConfig, packageSetSpecs map[string][]rpmmd.PackageSpec, containers []container.Spec, rng *rand.Rand) ([]osbuild.Pipeline, error) { pipelines := make([]osbuild.Pipeline, 0) pipelines = append(pipelines, *buildPipeline(repos, packageSetSpecs[buildPkgsKey], t.arch.distro.runner.String())) installerPackages := packageSetSpecs[installerPkgsKey] d := t.arch.distro archName := t.Arch().Name() kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(installerPackages, "kernel") ostreeRepoPath := "/ostree/repo" payloadStages := ostreePayloadStages(options, ostreeRepoPath) kickstartOptions, err := osbuild.NewKickstartStageOptions(kspath, "", users.UsersFromBP(customizations.GetUsers()), users.GroupsFromBP(customizations.GetGroups()), makeISORootPath(ostreeRepoPath), options.OSTree.ImageRef, "rhel") if err != nil { return nil, err } ksUsers := len(customizations.GetUsers())+len(customizations.GetGroups()) > 0 pipelines = append(pipelines, *anacondaTreePipeline(repos, installerPackages, kernelVer, archName, d.product, d.osVersion, "edge", ksUsers)) isolabel := fmt.Sprintf(d.isolabelTmpl, archName) pipelines = append(pipelines, *bootISOTreePipeline(kernelVer, archName, d.vendor, d.product, d.osVersion, isolabel, kickstartOptions, payloadStages)) pipelines = append(pipelines, *bootISOPipeline(t.Filename(), d.isolabelTmpl, archName, t.Arch().Name() == "x86_64")) return pipelines, nil } func imageInstallerPipelines(t *imageType, customizations *blueprint.Customizations, options distro.ImageOptions, repos []rpmmd.RepoConfig, packageSetSpecs map[string][]rpmmd.PackageSpec, containers []container.Spec, rng *rand.Rand) ([]osbuild.Pipeline, error) { pipelines := make([]osbuild.Pipeline, 0) pipelines = append(pipelines, *buildPipeline(repos, packageSetSpecs[buildPkgsKey], t.arch.distro.runner.String())) treePipeline, err := osPipeline(t, repos, packageSetSpecs[osPkgsKey], containers, customizations, options, nil) if err != nil { return nil, err } pipelines = append(pipelines, *treePipeline) var kernelPkg *rpmmd.PackageSpec installerPackages := packageSetSpecs[installerPkgsKey] for _, pkg := range installerPackages { if pkg.Name == "kernel" { // Implicit memory alasing doesn't couse any bug in this case /* #nosec G601 */ kernelPkg = &pkg break } } if kernelPkg == nil { return nil, fmt.Errorf("kernel package not found in installer package set") } kernelVer := fmt.Sprintf("%s-%s.%s", kernelPkg.Version, kernelPkg.Release, kernelPkg.Arch) tarPath := "/liveimg.tar" tarPayloadStages := []*osbuild.Stage{osbuild.NewTarStage(&osbuild.TarStageOptions{Filename: tarPath}, treePipeline.Name)} kickstartOptions, err := osbuild.NewKickstartStageOptions(kspath, makeISORootPath(tarPath), users.UsersFromBP(customizations.GetUsers()), users.GroupsFromBP(customizations.GetGroups()), "", "", "rhel") if err != nil { return nil, err } archName := t.Arch().Name() d := t.arch.distro pipelines = append(pipelines, *anacondaTreePipeline(repos, installerPackages, kernelVer, archName, d.product, d.osVersion, "BaseOS", true)) isolabel := fmt.Sprintf(d.isolabelTmpl, archName) pipelines = append(pipelines, *bootISOTreePipeline(kernelVer, archName, d.vendor, d.product, d.osVersion, isolabel, kickstartOptions, tarPayloadStages)) pipelines = append(pipelines, *bootISOPipeline(t.Filename(), d.isolabelTmpl, t.Arch().Name(), t.Arch().Name() == "x86_64")) return pipelines, nil } func edgeCorePipelines(t *imageType, customizations *blueprint.Customizations, options distro.ImageOptions, repos []rpmmd.RepoConfig, packageSetSpecs map[string][]rpmmd.PackageSpec, containers []container.Spec) ([]osbuild.Pipeline, error) { pipelines := make([]osbuild.Pipeline, 0) pipelines = append(pipelines, *buildPipeline(repos, packageSetSpecs[buildPkgsKey], t.arch.distro.runner.String())) treePipeline, err := osPipeline(t, repos, packageSetSpecs[osPkgsKey], containers, customizations, options, nil) if err != nil { return nil, err } pipelines = append(pipelines, *treePipeline) pipelines = append(pipelines, *ostreeCommitPipeline(options, t.arch.distro.osVersion)) return pipelines, nil } func edgeCommitPipelines(t *imageType, customizations *blueprint.Customizations, options distro.ImageOptions, repos []rpmmd.RepoConfig, packageSetSpecs map[string][]rpmmd.PackageSpec, containers []container.Spec, rng *rand.Rand) ([]osbuild.Pipeline, error) { pipelines, err := edgeCorePipelines(t, customizations, options, repos, packageSetSpecs, containers) if err != nil { return nil, err } tarPipeline := tarArchivePipeline("commit-archive", "ostree-commit", &osbuild.TarStageOptions{Filename: t.Filename()}) pipelines = append(pipelines, *tarPipeline) return pipelines, nil } func edgeContainerPipelines(t *imageType, customizations *blueprint.Customizations, options distro.ImageOptions, repos []rpmmd.RepoConfig, packageSetSpecs map[string][]rpmmd.PackageSpec, containers []container.Spec, rng *rand.Rand) ([]osbuild.Pipeline, error) { pipelines, err := edgeCorePipelines(t, customizations, options, repos, packageSetSpecs, containers) if err != nil { return nil, err } nginxConfigPath := "/etc/nginx.conf" httpPort := "8080" pipelines = append(pipelines, *containerTreePipeline(repos, packageSetSpecs[containerPkgsKey], options, customizations, nginxConfigPath, httpPort)) pipelines = append(pipelines, *containerPipeline(t, nginxConfigPath, httpPort)) return pipelines, nil } func edgeImagePipelines(t *imageType, customizations *blueprint.Customizations, filename string, options distro.ImageOptions, rng *rand.Rand) ([]osbuild.Pipeline, string, error) { pipelines := make([]osbuild.Pipeline, 0) ostreeRepoPath := "/ostree/repo" imgName := "image.raw" partitionTable, err := t.getPartitionTable(nil, options, rng) if err != nil { return nil, "", err } // prepare ostree deployment tree treePipeline := ostreeDeployPipeline(t, partitionTable, ostreeRepoPath, nil, "", rng, customizations, options) pipelines = append(pipelines, *treePipeline) // make raw image from tree imagePipeline := liveImagePipeline(treePipeline.Name, imgName, partitionTable, t.arch, "") pipelines = append(pipelines, *imagePipeline) // compress image xzPipeline := xzArchivePipeline(imagePipeline.Name, imgName, filename) pipelines = append(pipelines, *xzPipeline) return pipelines, xzPipeline.Name, nil } func edgeRawImagePipelines(t *imageType, customizations *blueprint.Customizations, options distro.ImageOptions, repos []rpmmd.RepoConfig, packageSetSpecs map[string][]rpmmd.PackageSpec, containers []container.Spec, rng *rand.Rand) ([]osbuild.Pipeline, error) { pipelines := make([]osbuild.Pipeline, 0) pipelines = append(pipelines, *buildPipeline(repos, packageSetSpecs[buildPkgsKey], t.arch.distro.runner.String())) imgName := t.filename // create the raw image imagePipelines, _, err := edgeImagePipelines(t, customizations, imgName, options, rng) if err != nil { return nil, err } pipelines = append(pipelines, imagePipelines...) return pipelines, nil } func buildPipeline(repos []rpmmd.RepoConfig, buildPackageSpecs []rpmmd.PackageSpec, runner string) *osbuild.Pipeline { p := new(osbuild.Pipeline) p.Name = "build" p.Runner = runner p.AddStage(osbuild.NewRPMStage(osbuild.NewRPMStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(buildPackageSpecs))) p.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(true))) return p } func osPipeline(t *imageType, repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpec, containers []container.Spec, c *blueprint.Customizations, options distro.ImageOptions, pt *disk.PartitionTable) (*osbuild.Pipeline, error) { imageConfig := t.getDefaultImageConfig() p := new(osbuild.Pipeline) if t.rpmOstree { p.Name = "ostree-tree" } else { p.Name = "os" } p.Build = "name:build" if t.rpmOstree && options.OSTree.FetchChecksum != "" && options.OSTree.URL != "" { p.AddStage(osbuild.NewOSTreePasswdStage("org.osbuild.source", options.OSTree.FetchChecksum)) } rpmOptions := osbuild.NewRPMStageOptions(repos) rpmOptions.GPGKeysFromTree = imageConfig.GPGKeyFiles if imageConfig.ExcludeDocs != nil && *imageConfig.ExcludeDocs { if rpmOptions.Exclude == nil { rpmOptions.Exclude = &osbuild.Exclude{} } rpmOptions.Exclude.Docs = true } p.AddStage(osbuild.NewRPMStage(rpmOptions, osbuild.NewRpmStageSourceFilesInputs(packages))) // If the /boot is on a separate partition, the prefix for the BLS stage must be "" if pt == nil || pt.FindMountable("/boot") == nil { p.AddStage(osbuild.NewFixBLSStage(&osbuild.FixBLSStageOptions{})) } else { p.AddStage(osbuild.NewFixBLSStage(&osbuild.FixBLSStageOptions{Prefix: common.ToPtr("")})) } if len(containers) > 0 { images := osbuild.NewContainersInputForSources(containers) var storagePath string // OSTree commits do not include data in `/var` since that is tied to the // deployment, rather than the commit. Therefore the containers need to be // stored in a different location, like `/usr/share`, and the container // storage engine configured accordingly. if t.rpmOstree { storagePath = "/usr/share/containers/storage" storageConf := "/etc/containers/storage.conf" containerStoreOpts := osbuild.NewContainerStorageOptions(storageConf, storagePath) p.AddStage(osbuild.NewContainersStorageConfStage(containerStoreOpts)) } skopeo := osbuild.NewSkopeoStage(images, storagePath) p.AddStage(skopeo) } language, keyboard := c.GetPrimaryLocale() if language != nil { p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: *language})) } else if imageConfig.Locale != nil { p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: *imageConfig.Locale})) } if keyboard != nil { p.AddStage(osbuild.NewKeymapStage(&osbuild.KeymapStageOptions{Keymap: *keyboard})) } else if imageConfig.Keyboard != nil { p.AddStage(osbuild.NewKeymapStage(imageConfig.Keyboard)) } if hostname := c.GetHostname(); hostname != nil { p.AddStage(osbuild.NewHostnameStage(&osbuild.HostnameStageOptions{Hostname: *hostname})) } timezone, ntpServers := c.GetTimezoneSettings() if timezone != nil { p.AddStage(osbuild.NewTimezoneStage(&osbuild.TimezoneStageOptions{Zone: *timezone})) } else if imageConfig.Timezone != nil { p.AddStage(osbuild.NewTimezoneStage(&osbuild.TimezoneStageOptions{Zone: *imageConfig.Timezone})) } if len(ntpServers) > 0 { servers := make([]osbuild.ChronyConfigServer, len(ntpServers)) for idx, server := range ntpServers { servers[idx] = osbuild.ChronyConfigServer{Hostname: server} } p.AddStage(osbuild.NewChronyStage(&osbuild.ChronyStageOptions{Servers: servers})) } else if imageConfig.TimeSynchronization != nil { p.AddStage(osbuild.NewChronyStage(imageConfig.TimeSynchronization)) } if !t.bootISO { // don't put users and groups in the payload of an installer // add them via kickstart instead if groups := c.GetGroups(); len(groups) > 0 { p.AddStage(osbuild.NewGroupsStage(osbuild.NewGroupsStageOptions(users.GroupsFromBP(groups)))) } if userOptions, err := osbuild.NewUsersStageOptions(users.UsersFromBP(c.GetUsers()), false); err != nil { return nil, err } else if userOptions != nil { if t.rpmOstree { // for ostree, writing the key during user creation is // redundant and can cause issues so create users without keys // and write them on first boot userOptionsSansKeys, err := osbuild.NewUsersStageOptions(users.UsersFromBP(c.GetUsers()), true) if err != nil { return nil, err } p.AddStage(osbuild.NewUsersStage(userOptionsSansKeys)) p.AddStage(osbuild.NewFirstBootStage(usersFirstBootOptions(userOptions))) } else { p.AddStage(osbuild.NewUsersStage(userOptions)) } } } if services := c.GetServices(); services != nil || imageConfig.EnabledServices != nil || imageConfig.DisabledServices != nil || imageConfig.DefaultTarget != nil { defaultTarget := "" if imageConfig.DefaultTarget != nil { defaultTarget = *imageConfig.DefaultTarget } p.AddStage(osbuild.NewSystemdStage(systemdStageOptions( imageConfig.EnabledServices, imageConfig.DisabledServices, services, defaultTarget, ))) } var fwStageOptions *osbuild.FirewallStageOptions if firewallCustomization := c.GetFirewall(); firewallCustomization != nil { fwStageOptions = firewallStageOptions(firewallCustomization) } if firewallConfig := imageConfig.Firewall; firewallConfig != nil { // merge the user-provided firewall config with the default one if fwStageOptions != nil { fwStageOptions = &osbuild.FirewallStageOptions{ // Prefer the firewall ports, services and zone settings provided // via BP customization. Ports: fwStageOptions.Ports, EnabledServices: fwStageOptions.EnabledServices, DisabledServices: fwStageOptions.DisabledServices, Zones: fwStageOptions.Zones, // Default zone can not be set using BP customizations, therefore // default to the one provided in the default image configuration. DefaultZone: firewallConfig.DefaultZone, } } else { fwStageOptions = firewallConfig } } if fwStageOptions != nil { p.AddStage(osbuild.NewFirewallStage(fwStageOptions)) } for _, sysconfigConfig := range imageConfig.Sysconfig { p.AddStage(osbuild.NewSysconfigStage(sysconfigConfig)) } if t.arch.distro.isRHEL() { if options.Subscription != nil { commands := []string{ fmt.Sprintf("/usr/sbin/subscription-manager register --org=%s --activationkey=%s --serverurl %s --baseurl %s", options.Subscription.Organization, options.Subscription.ActivationKey, options.Subscription.ServerUrl, options.Subscription.BaseUrl), } if options.Subscription.Insights { commands = append(commands, "/usr/bin/insights-client --register") } p.AddStage(osbuild.NewFirstBootStage(&osbuild.FirstBootStageOptions{ Commands: commands, WaitForNetwork: true, })) if rhsmConfig, exists := imageConfig.RHSMConfig[distro.RHSMConfigWithSubscription]; exists { p.AddStage(osbuild.NewRHSMStage(rhsmConfig)) } } else { if rhsmConfig, exists := imageConfig.RHSMConfig[distro.RHSMConfigNoSubscription]; exists { p.AddStage(osbuild.NewRHSMStage(rhsmConfig)) } } } for _, systemdLogindConfig := range imageConfig.SystemdLogind { p.AddStage(osbuild.NewSystemdLogindStage(systemdLogindConfig)) } for _, cloudInitConfig := range imageConfig.CloudInit { p.AddStage(osbuild.NewCloudInitStage(cloudInitConfig)) } for _, modprobeConfig := range imageConfig.Modprobe { p.AddStage(osbuild.NewModprobeStage(modprobeConfig)) } for _, dracutConfConfig := range imageConfig.DracutConf { p.AddStage(osbuild.NewDracutConfStage(dracutConfConfig)) } for _, systemdUnitConfig := range imageConfig.SystemdUnit { p.AddStage(osbuild.NewSystemdUnitStage(systemdUnitConfig)) } if authselectConfig := imageConfig.Authselect; authselectConfig != nil { p.AddStage(osbuild.NewAuthselectStage(authselectConfig)) } if seLinuxConfig := imageConfig.SELinuxConfig; seLinuxConfig != nil { p.AddStage(osbuild.NewSELinuxConfigStage(seLinuxConfig)) } if tunedConfig := imageConfig.Tuned; tunedConfig != nil { p.AddStage(osbuild.NewTunedStage(tunedConfig)) } for _, tmpfilesdConfig := range imageConfig.Tmpfilesd { p.AddStage(osbuild.NewTmpfilesdStage(tmpfilesdConfig)) } for _, pamLimitsConfConfig := range imageConfig.PamLimitsConf { p.AddStage(osbuild.NewPamLimitsConfStage(pamLimitsConfConfig)) } for _, sysctldConfig := range imageConfig.Sysctld { p.AddStage(osbuild.NewSysctldStage(sysctldConfig)) } for _, dnfConfig := range imageConfig.DNFConfig { p.AddStage(osbuild.NewDNFConfigStage(dnfConfig)) } if sshdConfig := imageConfig.SshdConfig; sshdConfig != nil { p.AddStage((osbuild.NewSshdConfigStage(sshdConfig))) } if authConfig := imageConfig.Authconfig; authConfig != nil { p.AddStage(osbuild.NewAuthconfigStage(authConfig)) } if pwQuality := imageConfig.PwQuality; pwQuality != nil { p.AddStage(osbuild.NewPwqualityConfStage(pwQuality)) } if waConfig := imageConfig.WAAgentConfig; waConfig != nil { p.AddStage(osbuild.NewWAAgentConfStage(waConfig)) } if dnfAutomaticConfig := imageConfig.DNFAutomaticConfig; dnfAutomaticConfig != nil { p.AddStage(osbuild.NewDNFAutomaticConfigStage(dnfAutomaticConfig)) } for _, yumRepo := range imageConfig.YUMRepos { p.AddStage(osbuild.NewYumReposStage(yumRepo)) } if gcpGuestAgentConfig := imageConfig.GCPGuestAgentConfig; gcpGuestAgentConfig != nil { p.AddStage(osbuild.NewGcpGuestAgentConfigStage(gcpGuestAgentConfig)) } if udevRules := imageConfig.UdevRules; udevRules != nil { p.AddStage(osbuild.NewUdevRulesStage(udevRules)) } if pt != nil { p = prependKernelCmdlineStage(p, t, pt) p.AddStage(osbuild.NewFSTabStage(osbuild.NewFSTabStageOptions(pt))) kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packages, c.GetKernel().Name) bootloader := bootloaderConfigStage(t, *pt, c.GetKernel(), kernelVer, false, false) if cfg := imageConfig.Grub2Config; cfg != nil { if grub2, ok := bootloader.Options.(*osbuild.GRUB2StageOptions); ok { // grub2.Config.Default is owned and set by `NewGrub2StageOptions` // and thus we need to preserve it if grub2.Config != nil { cfg.Default = grub2.Config.Default } grub2.Config = cfg } } p.AddStage(bootloader) } if oscapConfig := c.GetOpenSCAP(); oscapConfig != nil { if t.rpmOstree { return nil, fmt.Errorf("unexpected oscap options for ostree image type") } remediationOptions := osbuild.NewOscapRemediationStageOptions( osbuild.OscapConfig{ Datastream: oscapConfig.DataStream, ProfileID: oscapConfig.ProfileID, }, ) p.AddStage(osbuild.NewOscapRemediationStage(remediationOptions)) } if t.arch.distro.isRHEL() && options.Facts != nil { p.AddStage(osbuild.NewRHSMFactsStage(&osbuild.RHSMFactsStageOptions{ Facts: osbuild.RHSMFacts{ ApiType: options.Facts.ApiType, }, })) } if t.rpmOstree { p.AddStage(osbuild.NewSystemdJournaldStage( &osbuild.SystemdJournaldStageOptions{ Filename: "10-persistent.conf", Config: osbuild.SystemdJournaldConfigDropin{ Journal: osbuild.SystemdJournaldConfigJournalSection{ Storage: osbuild.StoragePresistent, }, }, }, )) } // Relabel the tree, unless the `NoSElinux` flag is explicitly set to `true` if imageConfig.NoSElinux == nil || imageConfig.NoSElinux != nil && !*imageConfig.NoSElinux { p.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(false))) } if t.rpmOstree { p.AddStage(osbuild.NewOSTreePrepTreeStage(&osbuild.OSTreePrepTreeStageOptions{ EtcGroupMembers: []string{ // NOTE: We may want to make this configurable. "wheel", "docker", }, })) } return p, nil } func ostreeCommitPipeline(options distro.ImageOptions, osVersion string) *osbuild.Pipeline { p := new(osbuild.Pipeline) p.Name = "ostree-commit" p.Build = "name:build" p.AddStage(osbuild.NewOSTreeInitStage(&osbuild.OSTreeInitStageOptions{Path: "/repo"})) p.AddStage( osbuild.NewOSTreeCommitStage( &osbuild.OSTreeCommitStageOptions{ Ref: options.OSTree.ImageRef, OSVersion: osVersion, Parent: options.OSTree.FetchChecksum, }, "ostree-tree", ), ) return p } func containerTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpec, options distro.ImageOptions, c *blueprint.Customizations, nginxConfigPath, listenPort string) *osbuild.Pipeline { p := new(osbuild.Pipeline) p.Name = "container-tree" p.Build = "name:build" p.AddStage(osbuild.NewRPMStage(osbuild.NewRPMStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages))) language, _ := c.GetPrimaryLocale() if language != nil { p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: *language})) } else { p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: "en_US"})) } htmlRoot := "/usr/share/nginx/html" repoPath := filepath.Join(htmlRoot, "repo") p.AddStage(osbuild.NewOSTreeInitStage(&osbuild.OSTreeInitStageOptions{Path: repoPath})) p.AddStage(osbuild.NewOSTreePullStage( &osbuild.OSTreePullStageOptions{Repo: repoPath}, osbuild.NewOstreePullStageInputs("org.osbuild.pipeline", "name:ostree-commit", options.OSTree.ImageRef), )) // make nginx log directory world writeable, otherwise nginx can't start in // an unprivileged container p.AddStage(osbuild.NewChmodStage(chmodStageOptions("/var/log/nginx", "o+w", true))) p.AddStage(osbuild.NewNginxConfigStage(nginxConfigStageOptions(nginxConfigPath, htmlRoot, listenPort))) return p } func containerPipeline(t *imageType, nginxConfigPath, listenPort string) *osbuild.Pipeline { p := new(osbuild.Pipeline) p.Name = "container" p.Build = "name:build" options := &osbuild.OCIArchiveStageOptions{ Architecture: t.arch.Name(), Filename: t.Filename(), Config: &osbuild.OCIArchiveConfig{ Cmd: []string{"nginx", "-c", nginxConfigPath}, ExposedPorts: []string{listenPort}, }, } baseInput := osbuild.NewTreeInput("name:container-tree") inputs := &osbuild.OCIArchiveStageInputs{Base: baseInput} p.AddStage(osbuild.NewOCIArchiveStage(options, inputs)) return p } func ostreePayloadStages(options distro.ImageOptions, ostreeRepoPath string) []*osbuild.Stage { stages := make([]*osbuild.Stage, 0) // ostree commit payload stages = append(stages, osbuild.NewOSTreeInitStage(&osbuild.OSTreeInitStageOptions{Path: ostreeRepoPath})) stages = append(stages, osbuild.NewOSTreePullStage( &osbuild.OSTreePullStageOptions{Repo: ostreeRepoPath}, osbuild.NewOstreePullStageInputs("org.osbuild.source", options.OSTree.FetchChecksum, options.OSTree.ImageRef), )) return stages } func edgeSimplifiedInstallerPipelines(t *imageType, customizations *blueprint.Customizations, options distro.ImageOptions, repos []rpmmd.RepoConfig, packageSetSpecs map[string][]rpmmd.PackageSpec, containers []container.Spec, rng *rand.Rand) ([]osbuild.Pipeline, error) { pipelines := make([]osbuild.Pipeline, 0) pipelines = append(pipelines, *buildPipeline(repos, packageSetSpecs[buildPkgsKey], t.arch.distro.runner.String())) installerPackages := packageSetSpecs[installerPkgsKey] kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(installerPackages, "kernel") imgName := "image.raw.xz" installDevice := customizations.GetInstallationDevice() // create the raw image imagePipelines, imgPipelineName, err := edgeImagePipelines(t, customizations, imgName, options, rng) if err != nil { return nil, err } pipelines = append(pipelines, imagePipelines...) // create boot ISO with raw image d := t.arch.distro archName := t.Arch().Name() installerTreePipeline := simplifiedInstallerTreePipeline(repos, installerPackages, kernelVer, archName, d.product, d.osVersion, "edge", customizations.GetFDO()) isolabel := fmt.Sprintf(d.isolabelTmpl, archName) efibootTreePipeline := simplifiedInstallerEFIBootTreePipeline(installDevice, kernelVer, archName, d.vendor, d.product, d.osVersion, isolabel, customizations.GetFDO()) bootISOTreePipeline := simplifiedInstallerBootISOTreePipeline(imgPipelineName, kernelVer, rng) pipelines = append(pipelines, *installerTreePipeline, *efibootTreePipeline, *bootISOTreePipeline) pipelines = append(pipelines, *bootISOPipeline(t.Filename(), d.isolabelTmpl, t.Arch().Name(), false)) return pipelines, nil } func simplifiedInstallerBootISOTreePipeline(archivePipelineName, kver string, rng *rand.Rand) *osbuild.Pipeline { p := new(osbuild.Pipeline) p.Name = "bootiso-tree" p.Build = "name:build" p.AddStage(osbuild.NewCopyStageSimple( &osbuild.CopyStageOptions{ Paths: []osbuild.CopyStagePath{ { From: "input://file/image.raw.xz", To: "tree:///image.raw.xz", }, }, }, osbuild.NewFilesInputs(osbuild.NewFilesInputReferencesPipeline(archivePipelineName, "image.raw.xz")), )) p.AddStage(osbuild.NewMkdirStage( &osbuild.MkdirStageOptions{ Paths: []osbuild.Path{ { Path: "images", }, { Path: "images/pxeboot", }, }, }, )) pt := disk.PartitionTable{ Size: 20971520, SectorSize: 512, Partitions: []disk.Partition{ { Start: 0, Size: 20971520, Payload: &disk.Filesystem{ Type: "vfat", Mountpoint: "/", UUID: disk.NewVolIDFromRand(rng), }, }, }, } filename := "images/efiboot.img" loopback := osbuild.NewLoopbackDevice(&osbuild.LoopbackDeviceOptions{Filename: filename}) p.AddStage(osbuild.NewTruncateStage(&osbuild.TruncateStageOptions{Filename: filename, Size: fmt.Sprintf("%d", pt.Size)})) for _, stage := range osbuild.GenMkfsStages(&pt, loopback) { p.AddStage(stage) } inputName := "root-tree" copyInputs := osbuild.NewPipelineTreeInputs(inputName, "efiboot-tree") copyOptions, copyDevices, copyMounts := osbuild.GenCopyFSTreeOptions(inputName, "efiboot-tree", filename, &pt) p.AddStage(osbuild.NewCopyStage(copyOptions, copyInputs, copyDevices, copyMounts)) inputName = "coi" copyInputs = osbuild.NewPipelineTreeInputs(inputName, "coi-tree") p.AddStage(osbuild.NewCopyStageSimple( &osbuild.CopyStageOptions{ Paths: []osbuild.CopyStagePath{ { From: fmt.Sprintf("input://%s/boot/vmlinuz-%s", inputName, kver), To: "tree:///images/pxeboot/vmlinuz", }, { From: fmt.Sprintf("input://%s/boot/initramfs-%s.img", inputName, kver), To: "tree:///images/pxeboot/initrd.img", }, }, }, copyInputs, )) inputName = "efi-tree" copyInputs = osbuild.NewPipelineTreeInputs(inputName, "efiboot-tree") p.AddStage(osbuild.NewCopyStageSimple( &osbuild.CopyStageOptions{ Paths: []osbuild.CopyStagePath{ { From: fmt.Sprintf("input://%s/EFI", inputName), To: "tree:///", }, }, }, copyInputs, )) return p } func simplifiedInstallerEFIBootTreePipeline(installDevice, kernelVer, arch, vendor, product, osVersion, isolabel string, fdo *blueprint.FDOCustomization) *osbuild.Pipeline { p := new(osbuild.Pipeline) p.Name = "efiboot-tree" p.Build = "name:build" p.AddStage(osbuild.NewGrubISOStage(grubISOStageOptions(installDevice, kernelVer, arch, vendor, product, osVersion, isolabel, fdo))) return p } func simplifiedInstallerTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpec, kernelVer, arch, product, osVersion, variant string, fdo *blueprint.FDOCustomization) *osbuild.Pipeline { p := new(osbuild.Pipeline) p.Name = "coi-tree" p.Build = "name:build" p.AddStage(osbuild.NewRPMStage(osbuild.NewRPMStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages))) p.AddStage(osbuild.NewBuildstampStage(buildStampStageOptions(arch, product, osVersion, variant))) p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: "en_US.UTF-8"})) dracutStageOptions := dracutStageOptions(kernelVer, arch, []string{ "coreos-installer", "fdo", }) if fdo.HasFDO() && fdo.DiunPubKeyRootCerts != "" { p.AddStage(osbuild.NewFDOStageForRootCerts(fdo.DiunPubKeyRootCerts)) dracutStageOptions.Install = []string{"/fdo_diun_pub_key_root_certs.pem"} } p.AddStage(osbuild.NewDracutStage(dracutStageOptions)) return p } func ostreeDeployPipeline( t *imageType, pt *disk.PartitionTable, repoPath string, kernel *blueprint.KernelCustomization, kernelVer string, rng *rand.Rand, c *blueprint.Customizations, options distro.ImageOptions, ) *osbuild.Pipeline { p := new(osbuild.Pipeline) p.Name = "image-tree" p.Build = "name:build" osname := "redhat" remote := "rhel-edge" p.AddStage(osbuild.OSTreeInitFsStage()) p.AddStage(osbuild.NewOSTreePullStage( &osbuild.OSTreePullStageOptions{Repo: repoPath, Remote: remote}, osbuild.NewOstreePullStageInputs("org.osbuild.source", options.OSTree.FetchChecksum, options.OSTree.ImageRef), )) p.AddStage(osbuild.NewOSTreeOsInitStage( &osbuild.OSTreeOsInitStageOptions{ OSName: osname, }, )) p.AddStage(osbuild.NewOSTreeConfigStage(ostreeConfigStageOptions(repoPath, true))) p.AddStage(osbuild.NewMkdirStage(efiMkdirStageOptions())) kernelOpts := osbuild.GenImageKernelOptions(pt) // "rw" kernel option is required when /sysroot is mounted read-only to // keep stateful parts of the filesystem writeable (/var/ and /etc) kernelOpts = append(kernelOpts, "rw") p.AddStage(osbuild.NewOSTreeDeployStage( &osbuild.OSTreeDeployStageOptions{ OsName: osname, Ref: options.OSTree.ImageRef, Remote: remote, Mounts: []string{"/boot", "/boot/efi"}, Rootfs: osbuild.Rootfs{ Label: "root", }, KernelOpts: kernelOpts, }, )) if options.OSTree.URL != "" { p.AddStage(osbuild.NewOSTreeRemotesStage( &osbuild.OSTreeRemotesStageOptions{ Repo: "/ostree/repo", Remotes: []osbuild.OSTreeRemote{ { Name: remote, URL: options.OSTree.URL, }, }, }, )) } p.AddStage(osbuild.NewOSTreeFillvarStage( &osbuild.OSTreeFillvarStageOptions{ Deployment: osbuild.OSTreeDeployment{ OSName: osname, Ref: options.OSTree.ImageRef, }, }, )) fstabOptions := osbuild.NewFSTabStageOptions(pt) fstabOptions.OSTree = &osbuild.OSTreeFstab{ Deployment: osbuild.OSTreeDeployment{ OSName: osname, Ref: options.OSTree.ImageRef, }, } p.AddStage(osbuild.NewFSTabStage(fstabOptions)) if bpUsers := c.GetUsers(); len(bpUsers) > 0 { usersStage, err := osbuild.GenUsersStage(users.UsersFromBP(bpUsers), false) if err != nil { panic(err) } usersStage.MountOSTree(osname, options.OSTree.ImageRef, 0) p.AddStage(usersStage) } if bpGroups := c.GetGroups(); len(bpGroups) > 0 { groupsStage := osbuild.GenGroupsStage(users.GroupsFromBP(bpGroups)) groupsStage.MountOSTree(osname, options.OSTree.ImageRef, 0) p.AddStage(groupsStage) } p.AddStage(bootloaderConfigStage(t, *pt, kernel, kernelVer, true, true)) p.AddStage(osbuild.NewOSTreeSelinuxStage( &osbuild.OSTreeSelinuxStageOptions{ Deployment: osbuild.OSTreeDeployment{ OSName: osname, Ref: options.OSTree.ImageRef, }, }, )) return p } func anacondaTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpec, kernelVer, arch, product, osVersion, variant string, users bool) *osbuild.Pipeline { p := new(osbuild.Pipeline) p.Name = "anaconda-tree" p.Build = "name:build" p.AddStage(osbuild.NewRPMStage(osbuild.NewRPMStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages))) p.AddStage(osbuild.NewBuildstampStage(buildStampStageOptions(arch, product, osVersion, variant))) p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: "en_US.UTF-8"})) rootPassword := "" rootUser := osbuild.UsersStageOptionsUser{ Password: &rootPassword, } installUID := 0 installGID := 0 installHome := "/root" installShell := "/usr/libexec/anaconda/run-anaconda" installPassword := "" installUser := osbuild.UsersStageOptionsUser{ UID: &installUID, GID: &installGID, Home: &installHome, Shell: &installShell, Password: &installPassword, } usersStageOptions := &osbuild.UsersStageOptions{ Users: map[string]osbuild.UsersStageOptionsUser{ "root": rootUser, "install": installUser, }, } p.AddStage(osbuild.NewUsersStage(usersStageOptions)) anacondaModules := []string{} if users { anacondaModules = []string{"org.fedoraproject.Anaconda.Modules.Users"} } p.AddStage(osbuild.NewAnacondaStage(osbuild.NewAnacondaStageOptions(anacondaModules))) p.AddStage(osbuild.NewLoraxScriptStage(loraxScriptStageOptions(arch))) p.AddStage(osbuild.NewDracutStage(dracutStageOptions(kernelVer, arch, []string{ "anaconda", "rdma", "rngd", "multipath", "fcoe", "fcoe-uefi", "iscsi", "lunmask", "nfs", }))) p.AddStage(osbuild.NewSELinuxConfigStage(&osbuild.SELinuxConfigStageOptions{State: osbuild.SELinuxStatePermissive})) return p } func bootISOTreePipeline(kernelVer, arch, vendor, product, osVersion, isolabel string, ksOptions *osbuild.KickstartStageOptions, payloadStages []*osbuild.Stage) *osbuild.Pipeline { p := new(osbuild.Pipeline) p.Name = "bootiso-tree" p.Build = "name:build" p.AddStage(osbuild.NewBootISOMonoStage(bootISOMonoStageOptions(kernelVer, arch, vendor, product, osVersion, isolabel), osbuild.NewBootISOMonoStagePipelineTreeInputs("anaconda-tree"))) p.AddStage(osbuild.NewKickstartStage(ksOptions)) p.AddStage(osbuild.NewDiscinfoStage(discinfoStageOptions(arch))) for _, stage := range payloadStages { p.AddStage(stage) } return p } func bootISOPipeline(filename, isolabel, arch string, isolinux bool) *osbuild.Pipeline { p := new(osbuild.Pipeline) p.Name = "bootiso" p.Build = "name:build" p.AddStage(osbuild.NewXorrisofsStage(xorrisofsStageOptions(filename, isolabel, arch, isolinux), "bootiso-tree")) p.AddStage(osbuild.NewImplantisomd5Stage(&osbuild.Implantisomd5StageOptions{Filename: filename})) return p } func liveImagePipeline(inputPipelineName string, outputFilename string, pt *disk.PartitionTable, arch *architecture, kernelVer string) *osbuild.Pipeline { p := new(osbuild.Pipeline) p.Name = "image" p.Build = "name:build" for _, stage := range osbuild.GenImagePrepareStages(pt, outputFilename, osbuild.PTSfdisk) { p.AddStage(stage) } inputName := "root-tree" copyOptions, copyDevices, copyMounts := osbuild.GenCopyFSTreeOptions(inputName, inputPipelineName, outputFilename, pt) copyInputs := osbuild.NewPipelineTreeInputs(inputName, inputPipelineName) p.AddStage(osbuild.NewCopyStage(copyOptions, copyInputs, copyDevices, copyMounts)) for _, stage := range osbuild.GenImageFinishStages(pt, outputFilename) { p.AddStage(stage) } loopback := osbuild.NewLoopbackDevice(&osbuild.LoopbackDeviceOptions{Filename: outputFilename}) p.AddStage(bootloaderInstStage(outputFilename, pt, arch, kernelVer, copyDevices, copyMounts, loopback)) return p } func xzArchivePipeline(inputPipelineName, inputFilename, outputFilename string) *osbuild.Pipeline { p := new(osbuild.Pipeline) p.Name = "archive" p.Build = "name:build" p.AddStage(osbuild.NewXzStage( osbuild.NewXzStageOptions(outputFilename), osbuild.NewFilesInputs(osbuild.NewFilesInputReferencesPipeline(inputPipelineName, inputFilename)), )) return p } func tarArchivePipeline(name, inputPipelineName string, tarOptions *osbuild.TarStageOptions) *osbuild.Pipeline { p := new(osbuild.Pipeline) p.Name = name p.Build = "name:build" p.AddStage(osbuild.NewTarStage(tarOptions, inputPipelineName)) return p } func qemuPipeline(inputPipelineName, inputFilename, outputFilename string, format osbuild.QEMUFormat, formatOptions osbuild.QEMUFormatOptions) *osbuild.Pipeline { p := new(osbuild.Pipeline) p.Name = string(format) p.Build = "name:build" qemuStage := osbuild.NewQEMUStage( osbuild.NewQEMUStageOptions(outputFilename, format, formatOptions), osbuild.NewQemuStagePipelineFilesInputs(inputPipelineName, inputFilename), ) p.AddStage(qemuStage) return p } func bootloaderConfigStage(t *imageType, partitionTable disk.PartitionTable, kernel *blueprint.KernelCustomization, kernelVer string, install, greenboot bool) *osbuild.Stage { if t.Arch().Name() == distro.S390xArchName { return osbuild.NewZiplStage(new(osbuild.ZiplStageOptions)) } kernelOptions := t.kernelOptions uefi := t.supportsUEFI() legacy := t.arch.legacy options := osbuild.NewGrub2StageOptions(&partitionTable, kernelOptions, kernel, kernelVer, uefi, legacy, t.arch.distro.vendor, install) options.Greenboot = greenboot // before unifying the org.osbuild.grub2 stage option generator, we didn't // set the following for RHEL 8.5, so we need to revert here to maintain // the old behaviour if uefi { options.UEFI.Unified = false } return osbuild.NewGRUB2Stage(options) } func bootloaderInstStage(filename string, pt *disk.PartitionTable, arch *architecture, kernelVer string, devices *osbuild.Devices, mounts *osbuild.Mounts, disk *osbuild.Device) *osbuild.Stage { platform := arch.legacy if platform != "" { return osbuild.NewGrub2InstStage(osbuild.NewGrub2InstStageOption(filename, pt, platform)) } if arch.name == distro.S390xArchName { return osbuild.NewZiplInstStage(osbuild.NewZiplInstStageOptions(kernelVer, pt), disk, devices, mounts) } return nil }