Repository: osbuild/osbuild-composer
Pull request #244 (2020-02-19 09:32:44)

Requires osbuild>=7 because of the --build-env argument


Patch:
diff --git a/cmd/osbuild-pipeline/main.go b/cmd/osbuild-pipeline/main.go
index 5832a689b..467882b6e 100644
--- a/cmd/osbuild-pipeline/main.go
+++ b/cmd/osbuild-pipeline/main.go
@@ -102,7 +102,8 @@ func main() {
 		panic("Could not depsolve build packages: " + err.Error())
 	}
 
-	pipeline, err := d.Pipeline(blueprint, nil, packageSpecs, buildPackageSpecs, checksums, archArg, imageType, 0)
+	size := d.GetSizeForOutputType(imageType, 0)
+	pipeline, err := d.Pipeline(blueprint, nil, packageSpecs, buildPackageSpecs, checksums, archArg, imageType, size)
 	if err != nil {
 		panic(err.Error())
 	}
diff --git a/golang-github-osbuild-composer.spec b/golang-github-osbuild-composer.spec
index 312002b79..78a15d572 100644
--- a/golang-github-osbuild-composer.spec
+++ b/golang-github-osbuild-composer.spec
@@ -36,7 +36,7 @@ BuildRequires:  golang(github.com/google/go-cmp/cmp)
 %endif
 
 Requires: systemd
-Requires: osbuild
+Requires: osbuild>=7
 
 Provides: osbuild-composer
 Provides: lorax-composer
diff --git a/internal/common/types.go b/internal/common/types.go
index 292e9d35a..c3b2af0c2 100644
--- a/internal/common/types.go
+++ b/internal/common/types.go
@@ -236,6 +236,7 @@ type Distribution int
 const (
 	Fedora30 Distribution = iota
 	Fedora31
+	Fedora32
 	RHEL82
 )
 
@@ -245,6 +246,7 @@ func getDistributionMapping() map[string]int {
 	mapping := map[string]int{
 		"fedora-30": int(Fedora30),
 		"fedora-31": int(Fedora31),
+		"fedora-32": int(Fedora32),
 		"rhel-8.2":  int(RHEL82),
 	}
 	return mapping
diff --git a/internal/distro/distro.go b/internal/distro/distro.go
index a18d9f7f9..2b50a4b07 100644
--- a/internal/distro/distro.go
+++ b/internal/distro/distro.go
@@ -15,6 +15,7 @@ import (
 
 	"github.com/osbuild/osbuild-composer/internal/distro/fedora30"
 	"github.com/osbuild/osbuild-composer/internal/distro/fedora31"
+	"github.com/osbuild/osbuild-composer/internal/distro/fedora32"
 	"github.com/osbuild/osbuild-composer/internal/distro/rhel82"
 )
 
@@ -68,13 +69,38 @@ type Registry struct {
 	distros map[common.Distribution]Distro
 }
 
+func WithSingleDistro(dist Distro) *Registry {
+	reg := &Registry{
+		distros: make(map[common.Distribution]Distro),
+	}
+	reg.register(dist)
+	return reg
+}
+
 func NewRegistry(confPaths []string) *Registry {
 	distros := &Registry{
 		distros: make(map[common.Distribution]Distro),
 	}
-	distros.register(fedora30.New(confPaths))
-	distros.register(fedora31.New(confPaths))
-	distros.register(rhel82.New(confPaths))
+	f30 := fedora30.New(confPaths)
+	if f30 == nil {
+		panic("Attempt to register Fedora 30 failed")
+	}
+	distros.register(f30)
+	f31 := fedora31.New(confPaths)
+	if f31 == nil {
+		panic("Attempt to register Fedora 31 failed")
+	}
+	distros.register(f31)
+	f32 := fedora32.New(confPaths)
+	if f32 == nil {
+		panic("Attempt to register Fedora 32 failed")
+	}
+	distros.register(f32)
+	el82 := rhel82.New(confPaths)
+	if el82 == nil {
+		panic("Attempt to register RHEL 8.2 failed")
+	}
+	distros.register(el82)
 	return distros
 }
 
diff --git a/internal/distro/fedora32/distro.go b/internal/distro/fedora32/distro.go
new file mode 100644
index 000000000..d2a7b737e
--- /dev/null
+++ b/internal/distro/fedora32/distro.go
@@ -0,0 +1,719 @@
+package fedora32
+
+import (
+	"errors"
+	"log"
+	"sort"
+	"strconv"
+
+	"github.com/osbuild/osbuild-composer/internal/common"
+	"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 Fedora32 struct {
+	arches        map[string]arch
+	outputs       map[string]output
+	buildPackages []string
+}
+
+type arch struct {
+	Name               string
+	BootloaderPackages []string
+	BuildPackages      []string
+	UEFI               bool
+	Repositories       []rpmmd.RepoConfig
+}
+
+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"
+
+func New(confPaths []string) *Fedora32 {
+	const GigaByte = 1024 * 1024 * 1024
+
+	r := Fedora32{
+		arches:  map[string]arch{},
+		outputs: map[string]output{},
+		buildPackages: []string{
+			"dnf",
+			"dosfstools",
+			"e2fsprogs",
+			"policycoreutils",
+			"qemu-img",
+			"systemd",
+			"tar",
+		},
+	}
+
+	repoMap, err := rpmmd.LoadRepositories(confPaths, r.Name())
+	if err != nil {
+		log.Printf("Could not load repository data for %s: %s", r.Name(), err.Error())
+		return nil
+	}
+
+	repos, exists := repoMap["x86_64"]
+	if !exists {
+		log.Printf("Could not load architecture-specific repository data for x86_64 (%s): %s", r.Name(), err.Error())
+	} else {
+		r.arches["x86_64"] = arch{
+			Name: "x86_64",
+			BootloaderPackages: []string{
+				"grub2-pc",
+			},
+			BuildPackages: []string{
+				"grub2-pc",
+			},
+			Repositories: repos,
+		}
+	}
+
+	repos, exists = repoMap["aarch64"]
+	if !exists {
+		log.Printf("Could not load architecture-specific repository data for x86_64 (%s): %s", r.Name(), err.Error())
+	} else {
+		r.arches["aarch64"] = arch{
+			Name: "aarch64",
+			BootloaderPackages: []string{
+				"dracut-config-generic",
+				"efibootmgr",
+				"grub2-efi-aa64",
+				"grub2-tools",
+				"shim-aa64",
+			},
+			UEFI:         true,
+			Repositories: repos,
+		}
+	}
+
+	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) Repositories(arch string) []rpmmd.RepoConfig {
+	return r.arches[arch].Repositories
+}
+
+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(b *blueprint.Blueprint, additionalRepos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, checksums map[string]string, 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(arch, checksums), "org.osbuild.fedora32")
+
+	packages, excludedPackages, err := r.BasePackages(outputFormat, outputArchitecture)
+	if err != nil {
+		return nil, err
+	}
+	packages = append(packages, b.GetPackages()...)
+
+	p.AddStage(osbuild.NewDNFStage(r.dnfStageOptions(arch, additionalRepos, checksums, packages, excludedPackages)))
+	p.AddStage(osbuild.NewFixBLSStage())
+
+	// TODO support setting all languages and install corresponding langpack-* package
+	language, keyboard := b.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 := b.GetHostname(); hostname != nil {
+		p.AddStage(osbuild.NewHostnameStage(&osbuild.HostnameStageOptions{*hostname}))
+	}
+
+	timezone, ntpServers := b.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 := b.GetUsers(); len(users) > 0 {
+		options, err := r.userStageOptions(users)
+		if err != nil {
+			return nil, err
+		}
+		p.AddStage(osbuild.NewUsersStage(options))
+	}
+
+	if groups := b.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, b.GetKernel(), arch.UEFI)))
+
+	if services := b.GetServices(); services != nil || output.EnabledServices != nil {
+		p.AddStage(osbuild.NewSystemdStage(r.systemdStageOptions(output.EnabledServices, output.DisabledServices, services)))
+	}
+
+	if firewall := b.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 {
+	return &osbuild.Sources{}
+}
+
+func (r *Fedora32) Runner() string {
+	return "org.osbuild.fedora32"
+}
+
+func (r *Fedora32) buildPipeline(arch arch, checksums map[string]string) *osbuild.Pipeline {
+	packages, err := r.BuildPackages(arch.Name)
+	if err != nil {
+		panic("impossibly invalid arch")
+	}
+
+	p := &osbuild.Pipeline{}
+	p.AddStage(osbuild.NewDNFStage(r.dnfStageOptions(arch, nil, checksums, packages, nil)))
+	return p
+}
+
+func (r *Fedora32) dnfStageOptions(arch arch, additionalRepos []rpmmd.RepoConfig, checksums map[string]string, packages, excludedPackages []string) *osbuild.DNFStageOptions {
+	options := &osbuild.DNFStageOptions{
+		ReleaseVersion:   "32",
+		BaseArchitecture: arch.Name,
+		ModulePlatformId: ModulePlatformID,
+	}
+
+	for _, repo := range append(arch.Repositories, additionalRepos...) {
+		options.AddRepository(&osbuild.DNFRepository{
+			BaseURL:    repo.BaseURL,
+			MetaLink:   repo.Metalink,
+			MirrorList: repo.MirrorList,
+			GPGKey:     repo.GPGKey,
+			Checksum:   checksums[repo.Id],
+		})
+	}
+
+	sort.Strings(packages)
+	for _, pkg := range packages {
+		options.AddPackage(pkg)
+	}
+
+	sort.Strings(excludedPackages)
+	for _, pkg := range excludedPackages {
+		options.ExcludePackage(pkg)
+	}
+
+	return options
+}
+
+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,
+		})
+}
diff --git a/internal/mocks/distro/distro_mock.go b/internal/mocks/distro/distro_mock.go
new file mode 100644
index 000000000..178ee3e93
--- /dev/null
+++ b/internal/mocks/distro/distro_mock.go
@@ -0,0 +1,14 @@
+package distro_mock
+
+import (
+	"github.com/osbuild/osbuild-composer/internal/distro"
+	"github.com/osbuild/osbuild-composer/internal/distro/fedoratest"
+)
+
+func NewRegistry() *distro.Registry {
+	ftest := fedoratest.New()
+	if ftest == nil {
+		panic("Attempt to register Fedora test failed")
+	}
+	return distro.WithSingleDistro(ftest)
+}
diff --git a/internal/mocks/rpmmd/fixtures.go b/internal/mocks/rpmmd/fixtures.go
index 60f7b07cb..547b485d4 100644
--- a/internal/mocks/rpmmd/fixtures.go
+++ b/internal/mocks/rpmmd/fixtures.go
@@ -4,7 +4,7 @@ import (
 	"fmt"
 	"github.com/osbuild/osbuild-composer/internal/common"
 	"github.com/osbuild/osbuild-composer/internal/compose"
-	"github.com/osbuild/osbuild-composer/internal/distro"
+	distro_mock "github.com/osbuild/osbuild-composer/internal/mocks/distro"
 	"sort"
 	"time"
 
@@ -94,7 +94,7 @@ func createBaseStoreFixture() *store.Store {
 	}
 
 	d := test_distro.New()
-	r := distro.NewRegistry([]string{"."})
+	r := distro_mock.NewRegistry()
 	s := store.New(nil, d, *r)
 
 	s.Blueprints[bName] = b
@@ -184,7 +184,7 @@ func createStoreWithoutComposesFixture() *store.Store {
 	}
 
 	d := test_distro.New()
-	r := distro.NewRegistry([]string{"."})
+	r := distro_mock.NewRegistry()
 	s := store.New(nil, d, *r)
 
 	s.Blueprints[bName] = b
diff --git a/repositories/fedora-32.json b/repositories/fedora-32.json
new file mode 100644
index 000000000..d930f0d79
--- /dev/null
+++ b/repositories/fedora-32.json
@@ -0,0 +1,18 @@
+{
+    "x86_64": [
+        {
+            "id": "fedora",
+            "name": "Fedora 32",
+            "metalink": "https://mirrors.fedoraproject.org/metalink?repo=fedora-32&arch=x86_64",
+            "gpgkey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBF1RVqsBEADWMBqYv/G1r4PwyiPQCfg5fXFGXV1FCZ32qMi9gLUTv1CX7rYy\nH4Inj93oic+lt1kQ0kQCkINOwQczOkm6XDkEekmMrHknJpFLwrTK4AS28bYF2RjL\nM+QJ/dGXDMPYsP0tkLvoxaHr9WTRq89A+AmONcUAQIMJg3JxXAAafBi2UszUUEPI\nU35MyufFt2ePd1k/6hVAO8S2VT72TxXSY7Ha4X2J0pGzbqQ6Dq3AVzogsnoIi09A\n7fYutYZPVVAEGRUqavl0th8LyuZShASZ38CdAHBMvWV4bVZghd/wDV5ev3LXUE0o\nitLAqNSeiDJ3grKWN6v0qdU0l3Ya60sugABd3xaE+ROe8kDCy3WmAaO51Q880ZA2\niXOTJFObqkBTP9j9+ZeQ+KNE8SBoiH1EybKtBU8HmygZvu8ZC1TKUyL5gwGUJt8v\nergy5Bw3Q7av520sNGD3cIWr4fBAVYwdBoZT8RcsnU1PP67NmOGFcwSFJ/LpiOMC\npZ1IBvjOC7KyKEZY2/63kjW73mB7OHOd18BHtGVkA3QAdVlcSule/z68VOAy6bih\nE6mdxP28D4INsts8w6yr4G+3aEIN8u0qRQq66Ri5mOXTyle+ONudtfGg3U9lgicg\nz6oVk17RT0jV9uL6K41sGZ1sH/6yTXQKagdAYr3w1ix2L46JgzC+/+6SSwARAQAB\ntDFGZWRvcmEgKDMyKSA8ZmVkb3JhLTMyLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v\ncmc+iQI4BBMBAgAiBQJdUVarAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK\nCRBsEwJtEslE0LdAD/wKdAMtfzr7O2y06/sOPnrb3D39Y2DXbB8y0iEmRdBL29Bq\n5btxwmAka7JZRJVFxPsOVqZ6KARjS0/oCBmJc0jCRANFCtM4UjVHTSsxrJfuPkel\nvrlNE9tcR6OCRpuj/PZgUa39iifF/FTUfDgh4Q91xiQoLqfBxOJzravQHoK9VzrM\nNTOu6J6l4zeGzY/ocj6DpT+5fdUO/3HgGFNiNYPC6GVzeiA3AAVR0sCyGENuqqdg\nwUxV3BIht05M5Wcdvxg1U9x5I3yjkLQw+idvX4pevTiCh9/0u+4g80cT/21Cxsdx\n7+DVHaewXbF87QQIcOAing0S5QE67r2uPVxmWy/56TKUqDoyP8SNsV62lT2jutsj\nLevNxUky011g5w3bc61UeaeKrrurFdRs+RwBVkXmtqm/i6g0ZTWZyWGO6gJd+HWA\nqY1NYiq4+cMvNLatmA2sOoCsRNmE9q6jM/ESVgaH8hSp8GcLuzt9/r4PZZGl5CvU\neldOiD221u8rzuHmLs4dsgwJJ9pgLT0cUAsOpbMPI0JpGIPQ2SG6yK7LmO6HFOxb\nAkz7IGUt0gy1MzPTyBvnB+WgD1I+IQXXsJbhP5+d+d3mOnqsd6oDM/grKBzrhoUe\noNadc9uzjqKlOrmrdIR3Bz38SSiWlde5fu6xPqJdmGZRNjXtcyJlbSPVDIloxw==\n=QWRO\n-----END PGP PUBLIC KEY BLOCK-----\n"
+        }
+    ],
+    "aarch64": [
+        {
+            "id": "fedora",
+            "name": "Fedora 32",
+            "metalink": "https://mirrors.fedoraproject.org/metalink?repo=fedora-32&arch=aarch64",
+            "gpgkey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBF1RVqsBEADWMBqYv/G1r4PwyiPQCfg5fXFGXV1FCZ32qMi9gLUTv1CX7rYy\nH4Inj93oic+lt1kQ0kQCkINOwQczOkm6XDkEekmMrHknJpFLwrTK4AS28bYF2RjL\nM+QJ/dGXDMPYsP0tkLvoxaHr9WTRq89A+AmONcUAQIMJg3JxXAAafBi2UszUUEPI\nU35MyufFt2ePd1k/6hVAO8S2VT72TxXSY7Ha4X2J0pGzbqQ6Dq3AVzogsnoIi09A\n7fYutYZPVVAEGRUqavl0th8LyuZShASZ38CdAHBMvWV4bVZghd/wDV5ev3LXUE0o\nitLAqNSeiDJ3grKWN6v0qdU0l3Ya60sugABd3xaE+ROe8kDCy3WmAaO51Q880ZA2\niXOTJFObqkBTP9j9+ZeQ+KNE8SBoiH1EybKtBU8HmygZvu8ZC1TKUyL5gwGUJt8v\nergy5Bw3Q7av520sNGD3cIWr4fBAVYwdBoZT8RcsnU1PP67NmOGFcwSFJ/LpiOMC\npZ1IBvjOC7KyKEZY2/63kjW73mB7OHOd18BHtGVkA3QAdVlcSule/z68VOAy6bih\nE6mdxP28D4INsts8w6yr4G+3aEIN8u0qRQq66Ri5mOXTyle+ONudtfGg3U9lgicg\nz6oVk17RT0jV9uL6K41sGZ1sH/6yTXQKagdAYr3w1ix2L46JgzC+/+6SSwARAQAB\ntDFGZWRvcmEgKDMyKSA8ZmVkb3JhLTMyLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v\ncmc+iQI4BBMBAgAiBQJdUVarAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK\nCRBsEwJtEslE0LdAD/wKdAMtfzr7O2y06/sOPnrb3D39Y2DXbB8y0iEmRdBL29Bq\n5btxwmAka7JZRJVFxPsOVqZ6KARjS0/oCBmJc0jCRANFCtM4UjVHTSsxrJfuPkel\nvrlNE9tcR6OCRpuj/PZgUa39iifF/FTUfDgh4Q91xiQoLqfBxOJzravQHoK9VzrM\nNTOu6J6l4zeGzY/ocj6DpT+5fdUO/3HgGFNiNYPC6GVzeiA3AAVR0sCyGENuqqdg\nwUxV3BIht05M5Wcdvxg1U9x5I3yjkLQw+idvX4pevTiCh9/0u+4g80cT/21Cxsdx\n7+DVHaewXbF87QQIcOAing0S5QE67r2uPVxmWy/56TKUqDoyP8SNsV62lT2jutsj\nLevNxUky011g5w3bc61UeaeKrrurFdRs+RwBVkXmtqm/i6g0ZTWZyWGO6gJd+HWA\nqY1NYiq4+cMvNLatmA2sOoCsRNmE9q6jM/ESVgaH8hSp8GcLuzt9/r4PZZGl5CvU\neldOiD221u8rzuHmLs4dsgwJJ9pgLT0cUAsOpbMPI0JpGIPQ2SG6yK7LmO6HFOxb\nAkz7IGUt0gy1MzPTyBvnB+WgD1I+IQXXsJbhP5+d+d3mOnqsd6oDM/grKBzrhoUe\noNadc9uzjqKlOrmrdIR3Bz38SSiWlde5fu6xPqJdmGZRNjXtcyJlbSPVDIloxw==\n=QWRO\n-----END PGP PUBLIC KEY BLOCK-----\n"
+        }
+    ]
+}
\ No newline at end of file


Test patch:
diff --git a/internal/distro/fedora32/distro_test.go b/internal/distro/fedora32/distro_test.go
new file mode 100644
index 000000000..0ceb192ab
--- /dev/null
+++ b/internal/distro/fedora32/distro_test.go
@@ -0,0 +1,113 @@
+package fedora32_test
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/osbuild/osbuild-composer/internal/distro"
+)
+
+func TestListOutputFormats(t *testing.T) {
+	want := []string{
+		"ami",
+		"ext4-filesystem",
+		"openstack",
+		"partitioned-disk",
+		"qcow2",
+		"tar",
+		"vhd",
+		"vmdk",
+	}
+
+	distros := distro.NewRegistry([]string{"../../../"})
+	f32 := distros.GetDistro("fedora-32")
+	if got := f32.ListOutputFormats(); !reflect.DeepEqual(got, want) {
+		t.Errorf("ListOutputFormats() = %v, want %v", got, want)
+	}
+}
+
+func TestFilenameFromType(t *testing.T) {
+	type args struct {
+		outputFormat string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    string
+		want1   string
+		wantErr bool
+	}{
+		{
+			name:  "ami",
+			args:  args{"ami"},
+			want:  "image.raw.xz",
+			want1: "application/octet-stream",
+		},
+		{
+			name:  "ext4",
+			args:  args{"ext4-filesystem"},
+			want:  "filesystem.img",
+			want1: "application/octet-stream",
+		},
+		{
+			name:  "openstack",
+			args:  args{"openstack"},
+			want:  "disk.qcow2",
+			want1: "application/x-qemu-disk",
+		},
+		{
+			name:  "partitioned-disk",
+			args:  args{"partitioned-disk"},
+			want:  "disk.img",
+			want1: "application/octet-stream",
+		},
+		{
+			name:  "qcow2",
+			args:  args{"qcow2"},
+			want:  "disk.qcow2",
+			want1: "application/x-qemu-disk",
+		},
+		{
+			name:  "tar",
+			args:  args{"tar"},
+			want:  "root.tar.xz",
+			want1: "application/x-tar",
+		},
+		{
+			name:  "vhd",
+			args:  args{"vhd"},
+			want:  "disk.vhd",
+			want1: "application/x-vhd",
+		},
+		{
+			name:  "vmdk",
+			args:  args{"vmdk"},
+			want:  "disk.vmdk",
+			want1: "application/x-vmdk",
+		},
+		{
+			name:    "invalid-output-type",
+			args:    args{"foobar"},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			distros := distro.NewRegistry([]string{"../../../"})
+			f32 := distros.GetDistro("fedora-32")
+			got, got1, err := f32.FilenameFromType(tt.args.outputFormat)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("FilenameFromType() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !tt.wantErr {
+				if got != tt.want {
+					t.Errorf("FilenameFromType() got = %v, want %v", got, tt.want)
+				}
+				if got1 != tt.want1 {
+					t.Errorf("FilenameFromType() got1 = %v, want %v", got1, tt.want1)
+				}
+			}
+		})
+	}
+}
diff --git a/internal/jobqueue/api_test.go b/internal/jobqueue/api_test.go
index 8fc3ed205..69a5e1bd1 100644
--- a/internal/jobqueue/api_test.go
+++ b/internal/jobqueue/api_test.go
@@ -1,11 +1,11 @@
 package jobqueue_test
 
 import (
+	distro_mock "github.com/osbuild/osbuild-composer/internal/mocks/distro"
 	"net/http"
 	"testing"
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
-	"github.com/osbuild/osbuild-composer/internal/distro"
 	test_distro "github.com/osbuild/osbuild-composer/internal/distro/fedoratest"
 	"github.com/osbuild/osbuild-composer/internal/jobqueue"
 	"github.com/osbuild/osbuild-composer/internal/store"
@@ -34,7 +34,7 @@ func TestBasic(t *testing.T) {
 
 	for _, c := range cases {
 		distroStruct := test_distro.New()
-		registry := distro.NewRegistry([]string{"."})
+		registry := distro_mock.NewRegistry()
 		api := jobqueue.New(nil, store.New(nil, distroStruct, *registry))
 
 		test.TestNonJsonRoute(t, api, false, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedResponse)
@@ -44,7 +44,7 @@ func TestBasic(t *testing.T) {
 func TestCreate(t *testing.T) {
 	id, _ := uuid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff")
 	distroStruct := test_distro.New()
-	registry := distro.NewRegistry([]string{"."})
+	registry := distro_mock.NewRegistry()
 	store := store.New(nil, distroStruct, *registry)
 	api := jobqueue.New(nil, store)
 
@@ -60,7 +60,7 @@ func TestCreate(t *testing.T) {
 func testUpdateTransition(t *testing.T, from, to string, expectedStatus int, expectedResponse string) {
 	id, _ := uuid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff")
 	distroStruct := test_distro.New()
-	registry := distro.NewRegistry([]string{"."})
+	registry := distro_mock.NewRegistry()
 	store := store.New(nil, distroStruct, *registry)
 	api := jobqueue.New(nil, store)
 
diff --git a/internal/rcm/api_test.go b/internal/rcm/api_test.go
index 01858cd90..57ac5f5bd 100644
--- a/internal/rcm/api_test.go
+++ b/internal/rcm/api_test.go
@@ -9,8 +9,8 @@ import (
 	"testing"
 
 	"github.com/google/uuid"
-	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/distro/fedoratest"
+	distro_mock "github.com/osbuild/osbuild-composer/internal/mocks/distro"
 	"github.com/osbuild/osbuild-composer/internal/rcm"
 	"github.com/osbuild/osbuild-composer/internal/store"
 )
@@ -48,7 +48,7 @@ func TestBasicRcmAPI(t *testing.T) {
 		{"GET", "/v1/compose/7802c476-9cd1-41b7-ba81-43c1906bce73", `{"status":"RUNNING"}`, "application/json", http.StatusBadRequest, `{"error_reason":"Compose UUID does not exist"}`},
 	}
 
-	registry := distro.NewRegistry([]string{"."})
+	registry := distro_mock.NewRegistry()
 	distroStruct := fedoratest.New()
 	api := rcm.New(nil, store.New(nil, distroStruct, *registry))
 
@@ -72,7 +72,7 @@ func TestBasicRcmAPI(t *testing.T) {
 
 func TestSubmitCompose(t *testing.T) {
 	// Test the most basic use case: Submit a new job and get its status.
-	registry := distro.NewRegistry([]string{"."})
+	registry := distro_mock.NewRegistry()
 	distroStruct := fedoratest.New()
 	api := rcm.New(nil, store.New(nil, distroStruct, *registry))