debian-forge-composer/internal/distro/rhel7/distro.go
Tom Gundersen be5ea6a9b8 distro/ImageType: let PackageSets depend on ImageOptions
The package sets for an image can depend on the blueprint, and
by the same logic there is no reason it should not be able to
depend on the image options.

This is so far a non-functional change, but makes a follow-up
commit simpler (though still without actually depending on
the image options to compute the package sets).
2022-07-04 23:04:29 +01:00

499 lines
12 KiB
Go

package rhel7
import (
"encoding/json"
"errors"
"fmt"
"math/rand"
"path"
"sort"
"strings"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/disk"
"github.com/osbuild/osbuild-composer/internal/distro"
osbuild "github.com/osbuild/osbuild-composer/internal/osbuild2"
"github.com/osbuild/osbuild-composer/internal/rpmmd"
)
const (
// package set names
// build package set name
buildPkgsKey = "build"
// main/common os image package set name
osPkgsKey = "packages"
// blueprint package set name
blueprintPkgsKey = "blueprint"
)
var mountpointAllowList = []string{
"/", "/var", "/opt", "/srv", "/usr", "/app", "/data", "/home", "/tmp",
}
// RHEL-based OS image configuration defaults
var defaultDistroImageConfig = &distro.ImageConfig{
Timezone: "America/New_York",
Locale: "en_US.UTF-8",
GPGKeyFiles: []string{
"/etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release",
},
Sysconfig: []*osbuild.SysconfigStageOptions{
{
Kernel: &osbuild.SysconfigKernelOptions{
UpdateDefault: true,
DefaultKernel: "kernel",
},
Network: &osbuild.SysconfigNetworkOptions{
Networking: true,
NoZeroConf: true,
},
},
},
}
// distribution objects without the arches > image types
var distroMap = map[string]distribution{
"rhel-7": {
name: "rhel-7",
product: "Red Hat Enterprise Linux",
osVersion: "7.9",
nick: "Maipo",
releaseVersion: "7",
modulePlatformID: "platform:el7",
vendor: "redhat",
runner: "org.osbuild.rhel7",
defaultImageConfig: defaultDistroImageConfig,
},
}
// --- Distribution ---
type distribution struct {
name string
product string
nick string
osVersion string
releaseVersion string
modulePlatformID string
vendor string
runner string
arches map[string]distro.Arch
defaultImageConfig *distro.ImageConfig
}
func (d *distribution) Name() string {
return d.name
}
func (d *distribution) Releasever() string {
return d.releaseVersion
}
func (d *distribution) ModulePlatformID() string {
return d.modulePlatformID
}
func (d *distribution) OSTreeRef() string {
return "" // not supported
}
func (d *distribution) ListArches() []string {
archNames := make([]string, 0, len(d.arches))
for name := range d.arches {
archNames = append(archNames, name)
}
sort.Strings(archNames)
return archNames
}
func (d *distribution) GetArch(name string) (distro.Arch, error) {
arch, exists := d.arches[name]
if !exists {
return nil, errors.New("invalid architecture: " + name)
}
return arch, nil
}
func (d *distribution) addArches(arches ...architecture) {
if d.arches == nil {
d.arches = map[string]distro.Arch{}
}
// Do not make copies of architectures, as opposed to image types,
// because architecture definitions are not used by more than a single
// distro definition.
for idx := range arches {
d.arches[arches[idx].name] = &arches[idx]
}
}
func (d *distribution) isRHEL() bool {
return strings.HasPrefix(d.name, "rhel")
}
func (d *distribution) getDefaultImageConfig() *distro.ImageConfig {
return d.defaultImageConfig
}
// --- Architecture ---
type architecture struct {
distro *distribution
name string
imageTypes map[string]distro.ImageType
imageTypeAliases map[string]string
legacy string
bootType distro.BootType
}
func (a *architecture) Name() string {
return a.name
}
func (a *architecture) ListImageTypes() []string {
itNames := make([]string, 0, len(a.imageTypes))
for name := range a.imageTypes {
itNames = append(itNames, name)
}
sort.Strings(itNames)
return itNames
}
func (a *architecture) GetImageType(name string) (distro.ImageType, error) {
t, exists := a.imageTypes[name]
if !exists {
aliasForName, exists := a.imageTypeAliases[name]
if !exists {
return nil, errors.New("invalid image type: " + name)
}
t, exists = a.imageTypes[aliasForName]
if !exists {
panic(fmt.Sprintf("image type '%s' is an alias to a non-existing image type '%s'", name, aliasForName))
}
}
return t, nil
}
func (a *architecture) addImageTypes(imageTypes ...imageType) {
if a.imageTypes == nil {
a.imageTypes = map[string]distro.ImageType{}
}
for idx := range imageTypes {
it := imageTypes[idx]
it.arch = a
a.imageTypes[it.name] = &it
for _, alias := range it.nameAliases {
if a.imageTypeAliases == nil {
a.imageTypeAliases = map[string]string{}
}
if existingAliasFor, exists := a.imageTypeAliases[alias]; exists {
panic(fmt.Sprintf("image type alias '%s' for '%s' is already defined for another image type '%s'", alias, it.name, existingAliasFor))
}
a.imageTypeAliases[alias] = it.name
}
}
}
func (a *architecture) Distro() distro.Distro {
return a.distro
}
// --- Image Type ---
type pipelinesFunc func(t *imageType, customizations *blueprint.Customizations, options distro.ImageOptions, repos []rpmmd.RepoConfig, packageSetSpecs map[string][]rpmmd.PackageSpec, rng *rand.Rand) ([]osbuild.Pipeline, error)
type packageSetFunc func(t *imageType) rpmmd.PackageSet
type imageType struct {
arch *architecture
name string
nameAliases []string
filename string
mimeType string
packageSets map[string]packageSetFunc
packageSetChains map[string][]string
defaultImageConfig *distro.ImageConfig
kernelOptions string
defaultSize uint64
buildPipelines []string
payloadPipelines []string
exports []string
pipelines pipelinesFunc
// bootable image
bootable bool
// If set to a value, it is preferred over the architecture value
bootType distro.BootType
// List of valid arches for the image type
basePartitionTables distro.BasePartitionTableMap
}
func (t *imageType) Name() string {
return t.name
}
func (t *imageType) Arch() distro.Arch {
return t.arch
}
func (t *imageType) Filename() string {
return t.filename
}
func (t *imageType) MIMEType() string {
return t.mimeType
}
func (t *imageType) OSTreeRef() string {
// Not supported
return ""
}
func (t *imageType) Size(size uint64) uint64 {
if size == 0 {
size = t.defaultSize
}
return size
}
func (t *imageType) getPackages(name string) rpmmd.PackageSet {
getter := t.packageSets[name]
if getter == nil {
return rpmmd.PackageSet{}
}
return getter(t)
}
func (t *imageType) PackageSets(bp blueprint.Blueprint, options distro.ImageOptions, repos []rpmmd.RepoConfig) map[string][]rpmmd.PackageSet {
// merge package sets that appear in the image type with the package sets
// of the same name from the distro and arch
mergedSets := make(map[string]rpmmd.PackageSet)
imageSets := t.packageSets
for name := range imageSets {
mergedSets[name] = t.getPackages(name)
}
if _, hasPackages := imageSets[osPkgsKey]; !hasPackages {
// should this be possible??
mergedSets[osPkgsKey] = rpmmd.PackageSet{}
}
// every image type must define a 'build' package set
if _, hasBuild := imageSets[buildPkgsKey]; !hasBuild {
panic(fmt.Sprintf("'%s' image type has no '%s' package set defined", t.name, buildPkgsKey))
}
// blueprint packages
bpPackages := bp.GetPackages()
timezone, _ := bp.Customizations.GetTimezoneSettings()
if timezone != nil {
bpPackages = append(bpPackages, "chrony")
}
// if we have file system customization that will need to a new mount point
// the layout is converted to LVM so we need to corresponding packages
archName := t.arch.Name()
pt := t.basePartitionTables[archName]
haveNewMountpoint := false
if fs := bp.Customizations.GetFilesystems(); fs != nil {
for i := 0; !haveNewMountpoint && i < len(fs); i++ {
haveNewMountpoint = !pt.ContainsMountpoint(fs[i].Mountpoint)
}
}
if haveNewMountpoint {
bpPackages = append(bpPackages, "lvm2")
}
// depsolve bp packages separately
// bp packages aren't restricted by exclude lists
mergedSets[blueprintPkgsKey] = rpmmd.PackageSet{Include: bpPackages}
kernel := bp.Customizations.GetKernel().Name
// add bp kernel to main OS package set to avoid duplicate kernels
mergedSets[osPkgsKey] = mergedSets[osPkgsKey].Append(rpmmd.PackageSet{Include: []string{kernel}})
return distro.MakePackageSetChains(t, mergedSets, repos)
}
func (t *imageType) BuildPipelines() []string {
return t.buildPipelines
}
func (t *imageType) PayloadPipelines() []string {
return t.payloadPipelines
}
func (t *imageType) PayloadPackageSets() []string {
return []string{blueprintPkgsKey}
}
func (t *imageType) PackageSetsChains() map[string][]string {
return t.packageSetChains
}
func (t *imageType) Exports() []string {
if len(t.exports) == 0 {
panic(fmt.Sprintf("programming error: no exports for '%s'", t.name))
}
return t.exports
}
// getBootType returns the BootType which should be used for this particular
// combination of architecture and image type.
func (t *imageType) getBootType() distro.BootType {
bootType := t.arch.bootType
if t.bootType != distro.UnsetBootType {
bootType = t.bootType
}
return bootType
}
func (t *imageType) getPartitionTable(
mountpoints []blueprint.FilesystemCustomization,
options distro.ImageOptions,
rng *rand.Rand,
) (*disk.PartitionTable, error) {
archName := t.arch.Name()
basePartitionTable, exists := t.basePartitionTables[archName]
if !exists {
return nil, fmt.Errorf("unknown arch: " + archName)
}
imageSize := t.Size(options.Size)
return disk.NewPartitionTable(&basePartitionTable, mountpoints, imageSize, true, rng)
}
func (t *imageType) getDefaultImageConfig() *distro.ImageConfig {
// ensure that image always returns non-nil default config
imageConfig := t.defaultImageConfig
if imageConfig == nil {
imageConfig = &distro.ImageConfig{}
}
return imageConfig.InheritFrom(t.arch.distro.getDefaultImageConfig())
}
func (t *imageType) PartitionType() string {
archName := t.arch.Name()
basePartitionTable, exists := t.basePartitionTables[archName]
if !exists {
return ""
}
return basePartitionTable.Type
}
func (t *imageType) Manifest(customizations *blueprint.Customizations,
options distro.ImageOptions,
repos []rpmmd.RepoConfig,
packageSpecSets map[string][]rpmmd.PackageSpec,
seed int64) (distro.Manifest, error) {
if err := t.checkOptions(customizations, options); err != nil {
return distro.Manifest{}, err
}
source := rand.NewSource(seed)
// math/rand is good enough in this case
/* #nosec G404 */
rng := rand.New(source)
pipelines, err := t.pipelines(t, customizations, options, repos, packageSpecSets, rng)
if err != nil {
return distro.Manifest{}, err
}
// flatten spec sets for sources
allPackageSpecs := make([]rpmmd.PackageSpec, 0)
for _, specs := range packageSpecSets {
allPackageSpecs = append(allPackageSpecs, specs...)
}
return json.Marshal(
osbuild.Manifest{
Version: "2",
Pipelines: pipelines,
Sources: osbuild.GenSources(allPackageSpecs, nil, nil),
},
)
}
func isMountpointAllowed(mountpoint string) bool {
for _, allowed := range mountpointAllowList {
match, _ := path.Match(allowed, mountpoint)
if match {
return true
}
// ensure that only clean mountpoints
// are valid
if strings.Contains(mountpoint, "//") {
return false
}
match = strings.HasPrefix(mountpoint, allowed+"/")
if allowed != "/" && match {
return true
}
}
return false
}
// checkOptions checks the validity and compatibility of options and customizations for the image type.
func (t *imageType) checkOptions(customizations *blueprint.Customizations, options distro.ImageOptions) error {
mountpoints := customizations.GetFilesystems()
invalidMountpoints := []string{}
for _, m := range mountpoints {
if !isMountpointAllowed(m.Mountpoint) {
invalidMountpoints = append(invalidMountpoints, m.Mountpoint)
}
}
if len(invalidMountpoints) > 0 {
return fmt.Errorf("The following custom mountpoints are not supported %+q", invalidMountpoints)
}
return nil
}
// New creates a new distro object, defining the supported architectures and image types
func New() distro.Distro {
return newDistro("rhel-7")
}
func NewHostDistro(name, modulePlatformID, ostreeRef string) distro.Distro {
return newDistro("rhel-7")
}
func newDistro(distroName string) distro.Distro {
rd := distroMap[distroName]
// Architecture definitions
x86_64 := architecture{
name: distro.X86_64ArchName,
distro: &rd,
legacy: "i386-pc",
bootType: distro.HybridBootType,
}
x86_64.addImageTypes(
qcow2ImgType,
azureRhuiImgType,
)
rd.addArches(
x86_64,
)
return &rd
}