debian-forge-composer/internal/distro/fedora32/distro.go
Tom Gundersen bbd8dc338d distro: introduce Arch and ImageType interfaces
Objects implementing these interfaces will represent the
architecture support for a given distro and the image type
support for a given architecture distro combination, respectively.

The idea is to always resolve to these objects early, and drop
the equilavent methods from the distro interface. This means that
we convert our input strings to real objects once, and then never
have to verify their correctness again.

Signed-off-by: Tom Gundersen <teg@jklm.no>
2020-03-23 15:43:55 +01:00

785 lines
20 KiB
Go

package fedora32
import (
"errors"
"sort"
"strconv"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/osbuild/osbuild-composer/internal/distro"
"github.com/osbuild/osbuild-composer/internal/osbuild"
"github.com/google/uuid"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/crypt"
"github.com/osbuild/osbuild-composer/internal/rpmmd"
)
type arch struct {
Name string
BootloaderPackages []string
BuildPackages []string
UEFI bool
}
type output struct {
Name string
MimeType string
Packages []string
ExcludedPackages []string
EnabledServices []string
DisabledServices []string
KernelOptions string
Bootable bool
DefaultSize uint64
Assembler func(uefi bool, size uint64) *osbuild.Assembler
}
const Distro = common.Fedora32
const ModulePlatformID = "platform:f32"
type Fedora32 struct {
arches map[string]arch
outputs map[string]output
buildPackages []string
}
type Fedora32Arch struct {
name string
distro *Fedora32
arch *arch
}
type Fedora32ImageType struct {
name string
arch *Fedora32Arch
output *output
}
func (d *Fedora32) GetArch(arch string) (distro.Arch, error) {
a, exists := d.arches[arch]
if !exists {
return nil, errors.New("invalid architecture: " + arch)
}
return &Fedora32Arch{
name: arch,
distro: d,
arch: &a,
}, nil
}
func (a *Fedora32Arch) Name() string {
return a.name
}
func (a *Fedora32Arch) ListImageTypes() []string {
return a.distro.ListOutputFormats()
}
func (a *Fedora32Arch) GetImageType(imageType string) (distro.ImageType, error) {
t, exists := a.distro.outputs[imageType]
if !exists {
return nil, errors.New("invalid image type: " + imageType)
}
return &Fedora32ImageType{
name: imageType,
arch: a,
output: &t,
}, nil
}
func (t *Fedora32ImageType) Name() string {
return t.name
}
func (t *Fedora32ImageType) Filename() string {
return t.output.Name
}
func (t *Fedora32ImageType) MIMEType() string {
return t.output.MimeType
}
func (t *Fedora32ImageType) Size(size uint64) uint64 {
return t.arch.distro.GetSizeForOutputType(t.name, size)
}
func (t *Fedora32ImageType) BasePackages() ([]string, []string) {
packages := t.output.Packages
if t.output.Bootable {
packages = append(packages, t.arch.arch.BootloaderPackages...)
}
return packages, t.output.ExcludedPackages
}
func (t *Fedora32ImageType) BuildPackages() []string {
return append(t.arch.distro.buildPackages, t.arch.arch.BuildPackages...)
}
func (t *Fedora32ImageType) Manifest(c *blueprint.Customizations,
repos []rpmmd.RepoConfig,
packageSpecs,
buildPackageSpecs []rpmmd.PackageSpec,
size uint64) (*osbuild.Manifest, error) {
pipeline, err := t.arch.distro.pipeline(c, repos, packageSpecs, buildPackageSpecs, t.arch.name, t.name, size)
if err != nil {
return nil, err
}
return &osbuild.Manifest{
Sources: *t.arch.distro.sources(append(packageSpecs, buildPackageSpecs...)),
Pipeline: *pipeline,
}, nil
}
func New() *Fedora32 {
const GigaByte = 1024 * 1024 * 1024
r := Fedora32{
outputs: map[string]output{},
buildPackages: []string{
"dnf",
"dosfstools",
"e2fsprogs",
"policycoreutils",
"qemu-img",
"systemd",
"tar",
},
arches: map[string]arch{
"x86_64": arch{
Name: "x86_64",
BootloaderPackages: []string{
"grub2-pc",
},
BuildPackages: []string{
"grub2-pc",
},
},
"aarch64": arch{
Name: "aarch64",
BootloaderPackages: []string{
"dracut-config-generic",
"efibootmgr",
"grub2-efi-aa64",
"grub2-tools",
"shim-aa64",
},
UEFI: true,
},
},
}
r.outputs["ami"] = output{
Name: "image.raw.xz",
MimeType: "application/octet-stream",
Packages: []string{
"@Core",
"chrony",
"kernel",
"selinux-policy-targeted",
"langpacks-en",
"libxcrypt-compat",
"xfsprogs",
"cloud-init",
"checkpolicy",
"net-tools",
},
ExcludedPackages: []string{
"dracut-config-rescue",
},
EnabledServices: []string{
"cloud-init.service",
},
KernelOptions: "ro no_timer_check console=ttyS0,115200n8 console=tty1 biosdevname=0 net.ifnames=0 console=ttyS0,115200",
Bootable: true,
DefaultSize: 6 * GigaByte,
Assembler: func(uefi bool, size uint64) *osbuild.Assembler {
return r.qemuAssembler("raw.xz", "image.raw.xz", uefi, size)
},
}
r.outputs["ext4-filesystem"] = output{
Name: "filesystem.img",
MimeType: "application/octet-stream",
Packages: []string{
"policycoreutils",
"selinux-policy-targeted",
"kernel",
"firewalld",
"chrony",
"langpacks-en",
},
ExcludedPackages: []string{
"dracut-config-rescue",
},
KernelOptions: "ro biosdevname=0 net.ifnames=0",
Bootable: false,
DefaultSize: 2 * GigaByte,
Assembler: func(uefi bool, size uint64) *osbuild.Assembler { return r.rawFSAssembler("filesystem.img", size) },
}
r.outputs["partitioned-disk"] = output{
Name: "disk.img",
MimeType: "application/octet-stream",
Packages: []string{
"@core",
"chrony",
"firewalld",
"kernel",
"langpacks-en",
"selinux-policy-targeted",
},
ExcludedPackages: []string{
"dracut-config-rescue",
},
KernelOptions: "ro biosdevname=0 net.ifnames=0",
Bootable: true,
DefaultSize: 2 * GigaByte,
Assembler: func(uefi bool, size uint64) *osbuild.Assembler {
return r.qemuAssembler("raw", "disk.img", uefi, size)
},
}
r.outputs["qcow2"] = output{
Name: "disk.qcow2",
MimeType: "application/x-qemu-disk",
Packages: []string{
"kernel-core",
"@Fedora Cloud Server",
"chrony",
"polkit",
"systemd-udev",
"selinux-policy-targeted",
"langpacks-en",
},
ExcludedPackages: []string{
"dracut-config-rescue",
"etables",
"firewalld",
"gobject-introspection",
"plymouth",
},
KernelOptions: "ro biosdevname=0 net.ifnames=0",
Bootable: true,
DefaultSize: 2 * GigaByte,
Assembler: func(uefi bool, size uint64) *osbuild.Assembler {
return r.qemuAssembler("qcow2", "disk.qcow2", uefi, size)
},
}
r.outputs["openstack"] = output{
Name: "disk.qcow2",
MimeType: "application/x-qemu-disk",
Packages: []string{
"@Core",
"chrony",
"kernel",
"selinux-policy-targeted",
"spice-vdagent",
"qemu-guest-agent",
"xen-libs",
"langpacks-en",
"cloud-init",
"libdrm",
},
ExcludedPackages: []string{
"dracut-config-rescue",
},
KernelOptions: "ro biosdevname=0 net.ifnames=0",
Bootable: true,
DefaultSize: 2 * GigaByte,
Assembler: func(uefi bool, size uint64) *osbuild.Assembler {
return r.qemuAssembler("qcow2", "disk.qcow2", uefi, size)
},
}
r.outputs["tar"] = output{
Name: "root.tar.xz",
MimeType: "application/x-tar",
Packages: []string{
"policycoreutils",
"selinux-policy-targeted",
"kernel",
"firewalld",
"chrony",
"langpacks-en",
},
ExcludedPackages: []string{
"dracut-config-rescue",
},
KernelOptions: "ro biosdevname=0 net.ifnames=0",
Bootable: false,
DefaultSize: 2 * GigaByte,
Assembler: func(uefi bool, size uint64) *osbuild.Assembler { return r.tarAssembler("root.tar.xz", "xz") },
}
r.outputs["vhd"] = output{
Name: "disk.vhd",
MimeType: "application/x-vhd",
Packages: []string{
"@Core",
"chrony",
"kernel",
"selinux-policy-targeted",
"langpacks-en",
"net-tools",
"ntfsprogs",
"WALinuxAgent",
"libxcrypt-compat",
"initscripts",
"glibc-all-langpacks",
"dracut-config-generic",
},
ExcludedPackages: []string{
"dracut-config-rescue",
},
EnabledServices: []string{
"sshd",
"waagent", // needed to run in Azure
},
DisabledServices: []string{
"proc-sys-fs-binfmt_misc.mount",
"loadmodules.service",
},
// These kernel parameters are required by Azure documentation
KernelOptions: "ro biosdevname=0 rootdelay=300 console=ttyS0 earlyprintk=ttyS0 net.ifnames=0",
Bootable: true,
DefaultSize: 2 * GigaByte,
Assembler: func(uefi bool, size uint64) *osbuild.Assembler {
return r.qemuAssembler("vpc", "disk.vhd", uefi, size)
},
}
r.outputs["vmdk"] = output{
Name: "disk.vmdk",
MimeType: "application/x-vmdk",
Packages: []string{
"@core",
"chrony",
"firewalld",
"kernel",
"langpacks-en",
"open-vm-tools",
"selinux-policy-targeted",
},
ExcludedPackages: []string{
"dracut-config-rescue",
},
KernelOptions: "ro biosdevname=0 net.ifnames=0",
Bootable: true,
DefaultSize: 2 * GigaByte,
Assembler: func(uefi bool, size uint64) *osbuild.Assembler {
return r.qemuAssembler("vmdk", "disk.vmdk", uefi, size)
},
}
return &r
}
func (r *Fedora32) Name() string {
name, exists := Distro.ToString()
if !exists {
panic("Fatal error, hardcoded distro value in fedora32 package is not valid!")
}
return name
}
func (r *Fedora32) Distribution() common.Distribution {
return Distro
}
func (r *Fedora32) ModulePlatformID() string {
return ModulePlatformID
}
func (r *Fedora32) ListOutputFormats() []string {
formats := make([]string, 0, len(r.outputs))
for name := range r.outputs {
formats = append(formats, name)
}
sort.Strings(formats)
return formats
}
func (r *Fedora32) FilenameFromType(outputFormat string) (string, string, error) {
if output, exists := r.outputs[outputFormat]; exists {
return output.Name, output.MimeType, nil
}
return "", "", errors.New("invalid output format: " + outputFormat)
}
func (r *Fedora32) GetSizeForOutputType(outputFormat string, size uint64) uint64 {
const MegaByte = 1024 * 1024
// Microsoft Azure requires vhd images to be rounded up to the nearest MB
if outputFormat == "vhd" && size%MegaByte != 0 {
size = (size/MegaByte + 1) * MegaByte
}
if size == 0 {
size = r.outputs[outputFormat].DefaultSize
}
return size
}
func (r *Fedora32) BasePackages(outputFormat string, outputArchitecture string) ([]string, []string, error) {
output, exists := r.outputs[outputFormat]
if !exists {
return nil, nil, errors.New("invalid output format: " + outputFormat)
}
packages := output.Packages
if output.Bootable {
arch, exists := r.arches[outputArchitecture]
if !exists {
return nil, nil, errors.New("invalid architecture: " + outputArchitecture)
}
packages = append(packages, arch.BootloaderPackages...)
}
return packages, output.ExcludedPackages, nil
}
func (r *Fedora32) BuildPackages(outputArchitecture string) ([]string, error) {
arch, exists := r.arches[outputArchitecture]
if !exists {
return nil, errors.New("invalid architecture: " + outputArchitecture)
}
return append(r.buildPackages, arch.BuildPackages...), nil
}
func (r *Fedora32) pipeline(c *blueprint.Customizations, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, outputFormat string, size uint64) (*osbuild.Pipeline, error) {
output, exists := r.outputs[outputFormat]
if !exists {
return nil, errors.New("invalid output format: " + outputFormat)
}
arch, exists := r.arches[outputArchitecture]
if !exists {
return nil, errors.New("invalid architecture: " + outputArchitecture)
}
p := &osbuild.Pipeline{}
p.SetBuild(r.buildPipeline(repos, arch, buildPackageSpecs), "org.osbuild.fedora32")
p.AddStage(osbuild.NewRPMStage(r.rpmStageOptions(arch, repos, packageSpecs)))
p.AddStage(osbuild.NewFixBLSStage())
// TODO support setting all languages and install corresponding langpack-* package
language, keyboard := c.GetPrimaryLocale()
if language != nil {
p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{*language}))
} else {
p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{"en_US"}))
}
if keyboard != nil {
p.AddStage(osbuild.NewKeymapStage(&osbuild.KeymapStageOptions{*keyboard}))
}
if hostname := c.GetHostname(); hostname != nil {
p.AddStage(osbuild.NewHostnameStage(&osbuild.HostnameStageOptions{*hostname}))
}
timezone, ntpServers := c.GetTimezoneSettings()
// TODO install chrony when this is set?
if timezone != nil {
p.AddStage(osbuild.NewTimezoneStage(&osbuild.TimezoneStageOptions{*timezone}))
}
if len(ntpServers) > 0 {
p.AddStage(osbuild.NewChronyStage(&osbuild.ChronyStageOptions{ntpServers}))
}
if users := c.GetUsers(); len(users) > 0 {
options, err := r.userStageOptions(users)
if err != nil {
return nil, err
}
p.AddStage(osbuild.NewUsersStage(options))
}
if groups := c.GetGroups(); len(groups) > 0 {
p.AddStage(osbuild.NewGroupsStage(r.groupStageOptions(groups)))
}
if output.Bootable {
p.AddStage(osbuild.NewFSTabStage(r.fsTabStageOptions(arch.UEFI)))
}
p.AddStage(osbuild.NewGRUB2Stage(r.grub2StageOptions(output.KernelOptions, c.GetKernel(), arch.UEFI)))
if services := c.GetServices(); services != nil || output.EnabledServices != nil {
p.AddStage(osbuild.NewSystemdStage(r.systemdStageOptions(output.EnabledServices, output.DisabledServices, services)))
}
if firewall := c.GetFirewall(); firewall != nil {
p.AddStage(osbuild.NewFirewallStage(r.firewallStageOptions(firewall)))
}
p.AddStage(osbuild.NewSELinuxStage(r.selinuxStageOptions()))
p.Assembler = output.Assembler(arch.UEFI, size)
return p, nil
}
func (r *Fedora32) sources(packages []rpmmd.PackageSpec) *osbuild.Sources {
files := &osbuild.FilesSource{
URLs: make(map[string]string),
}
for _, pkg := range packages {
files.URLs[pkg.Checksum] = pkg.RemoteLocation
}
return &osbuild.Sources{
"org.osbuild.files": files,
}
}
func (r *Fedora32) Manifest(c *blueprint.Customizations, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, outputFormat string, size uint64) (*osbuild.Manifest, error) {
pipeline, err := r.pipeline(c, repos, packageSpecs, buildPackageSpecs, outputArchitecture, outputFormat, size)
if err != nil {
return nil, err
}
return &osbuild.Manifest{
Sources: *r.sources(append(packageSpecs, buildPackageSpecs...)),
Pipeline: *pipeline,
}, nil
}
func (r *Fedora32) Runner() string {
return "org.osbuild.fedora32"
}
func (r *Fedora32) buildPipeline(repos []rpmmd.RepoConfig, arch arch, buildPackageSpecs []rpmmd.PackageSpec) *osbuild.Pipeline {
p := &osbuild.Pipeline{}
p.AddStage(osbuild.NewRPMStage(r.rpmStageOptions(arch, repos, buildPackageSpecs)))
return p
}
func (r *Fedora32) rpmStageOptions(arch arch, repos []rpmmd.RepoConfig, specs []rpmmd.PackageSpec) *osbuild.RPMStageOptions {
var gpgKeys []string
for _, repo := range repos {
if repo.GPGKey == "" {
continue
}
gpgKeys = append(gpgKeys, repo.GPGKey)
}
var packages []string
for _, spec := range specs {
packages = append(packages, spec.Checksum)
}
return &osbuild.RPMStageOptions{
GPGKeys: gpgKeys,
Packages: packages,
}
}
func (r *Fedora32) userStageOptions(users []blueprint.UserCustomization) (*osbuild.UsersStageOptions, error) {
options := osbuild.UsersStageOptions{
Users: make(map[string]osbuild.UsersStageOptionsUser),
}
for _, c := range users {
if c.Password != nil && !crypt.PasswordIsCrypted(*c.Password) {
cryptedPassword, err := crypt.CryptSHA512(*c.Password)
if err != nil {
return nil, err
}
c.Password = &cryptedPassword
}
user := osbuild.UsersStageOptionsUser{
Groups: c.Groups,
Description: c.Description,
Home: c.Home,
Shell: c.Shell,
Password: c.Password,
Key: c.Key,
}
if c.UID != nil {
uid := strconv.Itoa(*c.UID)
user.UID = &uid
}
if c.GID != nil {
gid := strconv.Itoa(*c.GID)
user.GID = &gid
}
options.Users[c.Name] = user
}
return &options, nil
}
func (r *Fedora32) groupStageOptions(groups []blueprint.GroupCustomization) *osbuild.GroupsStageOptions {
options := osbuild.GroupsStageOptions{
Groups: map[string]osbuild.GroupsStageOptionsGroup{},
}
for _, group := range groups {
groupData := osbuild.GroupsStageOptionsGroup{
Name: group.Name,
}
if group.GID != nil {
gid := strconv.Itoa(*group.GID)
groupData.GID = &gid
}
options.Groups[group.Name] = groupData
}
return &options
}
func (r *Fedora32) firewallStageOptions(firewall *blueprint.FirewallCustomization) *osbuild.FirewallStageOptions {
options := osbuild.FirewallStageOptions{
Ports: firewall.Ports,
}
if firewall.Services != nil {
options.EnabledServices = firewall.Services.Enabled
options.DisabledServices = firewall.Services.Disabled
}
return &options
}
func (r *Fedora32) systemdStageOptions(enabledServices, disabledServices []string, s *blueprint.ServicesCustomization) *osbuild.SystemdStageOptions {
if s != nil {
enabledServices = append(enabledServices, s.Enabled...)
disabledServices = append(disabledServices, s.Disabled...)
}
return &osbuild.SystemdStageOptions{
EnabledServices: enabledServices,
DisabledServices: disabledServices,
}
}
func (r *Fedora32) fsTabStageOptions(uefi bool) *osbuild.FSTabStageOptions {
options := osbuild.FSTabStageOptions{}
options.AddFilesystem("76a22bf4-f153-4541-b6c7-0332c0dfaeac", "ext4", "/", "defaults", 1, 1)
if uefi {
options.AddFilesystem("46BB-8120", "vfat", "/boot/efi", "umask=0077,shortname=winnt", 0, 2)
}
return &options
}
func (r *Fedora32) grub2StageOptions(kernelOptions string, kernel *blueprint.KernelCustomization, uefi bool) *osbuild.GRUB2StageOptions {
id, err := uuid.Parse("76a22bf4-f153-4541-b6c7-0332c0dfaeac")
if err != nil {
panic("invalid UUID")
}
if kernel != nil {
kernelOptions += " " + kernel.Append
}
var uefiOptions *osbuild.GRUB2UEFI
if uefi {
uefiOptions = &osbuild.GRUB2UEFI{
Vendor: "fedora",
}
}
return &osbuild.GRUB2StageOptions{
RootFilesystemUUID: id,
KernelOptions: kernelOptions,
Legacy: !uefi,
UEFI: uefiOptions,
}
}
func (r *Fedora32) selinuxStageOptions() *osbuild.SELinuxStageOptions {
return &osbuild.SELinuxStageOptions{
FileContexts: "etc/selinux/targeted/contexts/files/file_contexts",
}
}
func (r *Fedora32) qemuAssembler(format string, filename string, uefi bool, size uint64) *osbuild.Assembler {
var options osbuild.QEMUAssemblerOptions
if uefi {
fstype := uuid.MustParse("C12A7328-F81F-11D2-BA4B-00A0C93EC93B")
options = osbuild.QEMUAssemblerOptions{
Format: format,
Filename: filename,
Size: size,
PTUUID: "8DFDFF87-C96E-EA48-A3A6-9408F1F6B1EF",
PTType: "gpt",
Partitions: []osbuild.QEMUPartition{
{
Start: 2048,
Size: 972800,
Type: &fstype,
Filesystem: osbuild.QEMUFilesystem{
Type: "vfat",
UUID: "46BB-8120",
Label: "EFI System Partition",
Mountpoint: "/boot/efi",
},
},
{
Start: 976896,
Filesystem: osbuild.QEMUFilesystem{
Type: "ext4",
UUID: "76a22bf4-f153-4541-b6c7-0332c0dfaeac",
Mountpoint: "/",
},
},
},
}
} else {
options = osbuild.QEMUAssemblerOptions{
Format: format,
Filename: filename,
Size: size,
PTUUID: "0x14fc63d2",
PTType: "mbr",
Partitions: []osbuild.QEMUPartition{
{
Start: 2048,
Bootable: true,
Filesystem: osbuild.QEMUFilesystem{
Type: "ext4",
UUID: "76a22bf4-f153-4541-b6c7-0332c0dfaeac",
Mountpoint: "/",
},
},
},
}
}
return osbuild.NewQEMUAssembler(&options)
}
func (r *Fedora32) tarAssembler(filename, compression string) *osbuild.Assembler {
return osbuild.NewTarAssembler(
&osbuild.TarAssemblerOptions{
Filename: filename,
Compression: compression,
})
}
func (r *Fedora32) rawFSAssembler(filename string, size uint64) *osbuild.Assembler {
id, err := uuid.Parse("76a22bf4-f153-4541-b6c7-0332c0dfaeac")
if err != nil {
panic("invalid UUID")
}
return osbuild.NewRawFSAssembler(
&osbuild.RawFSAssemblerOptions{
Filename: filename,
RootFilesystemUUDI: id,
Size: size,
})
}