diff --git a/internal/distro/fedora30/ami.go b/internal/distro/fedora30/ami.go deleted file mode 100644 index ed17f4c45..000000000 --- a/internal/distro/fedora30/ami.go +++ /dev/null @@ -1,47 +0,0 @@ -package fedora30 - -import ( - "github.com/osbuild/osbuild-composer/internal/blueprint" - "github.com/osbuild/osbuild-composer/internal/pipeline" -) - -type amiOutput struct{} - -func (t *amiOutput) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) { - packages := [...]string{ - "@Core", - "chrony", - "kernel", - "selinux-policy-targeted", - "grub2-pc", - "langpacks-en", - "libxcrypt-compat", - "xfsprogs", - "cloud-init", - "checkpolicy", - "net-tools", - } - excludedPackages := [...]string{ - "dracut-config-rescue", - } - p := newF30Pipeline(packages[:], excludedPackages[:], b) - err := customizeAll(p, b.Customizations) - if err != nil { - return nil, err - } - setFilesystems(p) - setBootloader(p, "ro no_timer_check console=ttyS0,115200n8 console=tty1 biosdevname=0 net.ifnames=0 console=ttyS0,115200", b) - setFirewall(p, nil, nil, b) - setServices(p, []string{"cloud-init.service"}, nil, b) - setQemuAssembler(p, "raw", t.getName()) - - return p, nil -} - -func (t *amiOutput) getName() string { - return "image.ami" -} - -func (t *amiOutput) getMime() string { - return "application/octet-stream" -} diff --git a/internal/distro/fedora30/customizations.go b/internal/distro/fedora30/customizations.go deleted file mode 100644 index e2362e3dd..000000000 --- a/internal/distro/fedora30/customizations.go +++ /dev/null @@ -1,234 +0,0 @@ -package fedora30 - -import ( - "log" - "strconv" - "strings" - - "github.com/osbuild/osbuild-composer/internal/blueprint" - "github.com/osbuild/osbuild-composer/internal/crypt" - "github.com/osbuild/osbuild-composer/internal/pipeline" -) - -func customizeAll(p *pipeline.Pipeline, c *blueprint.Customizations) error { - if c == nil { - c = &blueprint.Customizations{} - } - customizeHostname(p, c) - customizeGroup(p, c) - if err := customizeUserAndSSHKey(p, c); err != nil { - return err - } - customizeTimezone(p, c) - customizeNTPServers(p, c) - customizeLocale(p, c) - customizeKeyboard(p, c) - - return nil -} - -func customizeHostname(p *pipeline.Pipeline, c *blueprint.Customizations) { - if c.Hostname == nil { - return - } - - p.AddStage( - pipeline.NewHostnameStage( - &pipeline.HostnameStageOptions{Hostname: *c.Hostname}, - ), - ) -} - -func customizeGroup(p *pipeline.Pipeline, c *blueprint.Customizations) { - if len(c.Group) == 0 { - return - } - - groups := make(map[string]pipeline.GroupsStageOptionsGroup) - for _, group := range c.Group { - if userCustomizationsContainUsername(c.User, group.Name) { - log.Println("group with name ", group.Name, " was not created, because user with same name was defined!") - continue - } - - groupData := pipeline.GroupsStageOptionsGroup{} - if group.GID != nil { - gid := strconv.Itoa(*group.GID) - groupData.GID = &gid - } - - groups[group.Name] = groupData - } - - p.AddStage( - pipeline.NewGroupsStage( - &pipeline.GroupsStageOptions{Groups: groups}, - ), - ) -} - -func assertAllUsersExistForSSHCustomizations(c *blueprint.Customizations) error { - for _, sshkey := range c.SSHKey { - userFound := false - for _, user := range c.User { - if user.Name == sshkey.User { - userFound = true - } - } - - if !userFound { - return &blueprint.CustomizationError{"Cannot set SSH key for non-existing user " + sshkey.User} - } - } - return nil -} - -func customizeUserAndSSHKey(p *pipeline.Pipeline, c *blueprint.Customizations) error { - if len(c.User) == 0 { - if len(c.SSHKey) > 0 { - return &blueprint.CustomizationError{"SSH key customization defined but no user customizations are defined"} - } - - return nil - } - - // return error if ssh key customization without user defined in user customization if found - if e := assertAllUsersExistForSSHCustomizations(c); e != nil { - return e - } - - users := make(map[string]pipeline.UsersStageOptionsUser) - for _, user := range c.User { - - if user.Password != nil && !crypt.PasswordIsCrypted(*user.Password) { - cryptedPassword, err := crypt.CryptSHA512(*user.Password) - if err != nil { - return err - } - - user.Password = &cryptedPassword - } - - userData := pipeline.UsersStageOptionsUser{ - Groups: user.Groups, - Description: user.Description, - Home: user.Home, - Shell: user.Shell, - Password: user.Password, - Key: user.Key, - } - - if user.UID != nil { - uid := strconv.Itoa(*user.UID) - userData.UID = &uid - } - - if user.GID != nil { - gid := strconv.Itoa(*user.GID) - userData.GID = &gid - } - - // process sshkey customizations - if additionalKeys := findKeysForUser(c.SSHKey, user.Name); len(additionalKeys) > 0 { - joinedKeys := strings.Join(additionalKeys, "\n") - - if userData.Key != nil { - *userData.Key += "\n" + joinedKeys - } else { - userData.Key = &joinedKeys - } - } - - users[user.Name] = userData - } - - p.AddStage( - pipeline.NewUsersStage( - &pipeline.UsersStageOptions{Users: users}, - ), - ) - - return nil -} - -func customizeTimezone(p *pipeline.Pipeline, c *blueprint.Customizations) { - if c.Timezone == nil || c.Timezone.Timezone == nil { - return - } - - // TODO: lorax (anaconda) automatically installs chrony if timeservers are defined - // except for the case when chrony is explicitly removed from installed packages (using -chrony) - // this is currently not supported, no checks whether chrony is installed are not performed - - p.AddStage( - pipeline.NewTimezoneStage(&pipeline.TimezoneStageOptions{ - Zone: *c.Timezone.Timezone, - }), - ) -} - -func customizeNTPServers(p *pipeline.Pipeline, c *blueprint.Customizations) { - if c.Timezone == nil || len(c.Timezone.NTPServers) == 0 { - return - } - - p.AddStage( - pipeline.NewChronyStage(&pipeline.ChronyStageOptions{ - Timeservers: c.Timezone.NTPServers, - }), - ) -} - -func customizeLocale(p *pipeline.Pipeline, c *blueprint.Customizations) { - locale := c.Locale - if locale == nil { - locale = &blueprint.LocaleCustomization{ - Languages: []string{ - "en_US", - }, - } - } - - // TODO: you can specify more languages in customization - // The first one is the primary one, we can set in the locale stage, this should currently work - // Also, ALL the listed languages are installed using langpack-* packages - // This is currently not implemented! - // See anaconda src: pyanaconda/payload/dnfpayload.py:772 - - p.AddStage( - pipeline.NewLocaleStage(&pipeline.LocaleStageOptions{ - Language: locale.Languages[0], - }), - ) -} - -func customizeKeyboard(p *pipeline.Pipeline, c *blueprint.Customizations) { - if c.Locale == nil || c.Locale.Keyboard == nil { - return - } - - p.AddStage( - pipeline.NewKeymapStage(&pipeline.KeymapStageOptions{ - Keymap: *c.Locale.Keyboard, - }), - ) -} - -func findKeysForUser(sshKeyCustomizations []blueprint.SSHKeyCustomization, user string) (keys []string) { - for _, sshKey := range sshKeyCustomizations { - if sshKey.User == user { - keys = append(keys, sshKey.Key) - } - } - return -} - -func userCustomizationsContainUsername(userCustomizations []blueprint.UserCustomization, name string) bool { - for _, usr := range userCustomizations { - if usr.Name == name { - return true - } - } - - return false -} diff --git a/internal/distro/fedora30/disk.go b/internal/distro/fedora30/disk.go deleted file mode 100644 index 617f77f72..000000000 --- a/internal/distro/fedora30/disk.go +++ /dev/null @@ -1,43 +0,0 @@ -package fedora30 - -import ( - "github.com/osbuild/osbuild-composer/internal/blueprint" - "github.com/osbuild/osbuild-composer/internal/pipeline" -) - -type diskOutput struct{} - -func (t *diskOutput) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) { - packages := [...]string{ - "@core", - "chrony", - "firewalld", - "grub2-pc", - "kernel", - "langpacks-en", - "selinux-policy-targeted", - } - excludedPackages := [...]string{ - "dracut-config-rescue", - } - p := newF30Pipeline(packages[:], excludedPackages[:], b) - err := customizeAll(p, b.Customizations) - if err != nil { - return nil, err - } - setFilesystems(p) - setBootloader(p, "ro biosdevname=0 net.ifnames=0", b) - setFirewall(p, nil, nil, b) - setServices(p, nil, nil, b) - setQemuAssembler(p, "raw", t.getName()) - - return p, nil -} - -func (t *diskOutput) getName() string { - return "disk.img" -} - -func (t *diskOutput) getMime() string { - return "application/octet-stream" -} diff --git a/internal/distro/fedora30/distro.go b/internal/distro/fedora30/distro.go new file mode 100644 index 000000000..6452dd805 --- /dev/null +++ b/internal/distro/fedora30/distro.go @@ -0,0 +1,536 @@ +package fedora30 + +import ( + "sort" + "strconv" + + "github.com/google/uuid" + + "github.com/osbuild/osbuild-composer/internal/blueprint" + "github.com/osbuild/osbuild-composer/internal/crypt" + "github.com/osbuild/osbuild-composer/internal/distro" + "github.com/osbuild/osbuild-composer/internal/pipeline" + "github.com/osbuild/osbuild-composer/internal/rpmmd" +) + +type Fedora30 struct { + outputs map[string]output +} + +type output struct { + Name string + MimeType string + Packages []string + ExcludedPackages []string + EnabledServices []string + DisabledServices []string + KernelOptions string + IncludeFSTab bool + Assembler *pipeline.Assembler +} + +func init() { + r := Fedora30{ + outputs: map[string]output{}, + } + + r.outputs["ami"] = output{ + Name: "image.ami", + MimeType: "application/octet-stream", + Packages: []string{ + "@Core", + "chrony", + "kernel", + "selinux-policy-targeted", + "grub2-pc", + "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", + IncludeFSTab: true, + Assembler: r.qemuAssembler("raw", "image.ami"), + } + + 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", + IncludeFSTab: false, + Assembler: r.rawFSAssembler("filesystem.img"), + } + + r.outputs["partitioned-disk"] = output{ + Name: "disk.img", + MimeType: "application/octet-stream", + Packages: []string{ + "@core", + "chrony", + "firewalld", + "grub2-pc", + "kernel", + "langpacks-en", + "selinux-policy-targeted", + }, + ExcludedPackages: []string{ + "dracut-config-rescue", + }, + KernelOptions: "ro biosdevname=0 net.ifnames=0", + IncludeFSTab: true, + Assembler: r.qemuAssembler("raw", "disk.img"), + } + + r.outputs["qcow2"] = output{ + Name: "image.qcow2", + MimeType: "application/x-qemu-disk", + Packages: []string{ + "kernel-core", + "@Fedora Cloud Server", + "chrony", + "polkit", + "systemd-udev", + "selinux-policy-targeted", + "grub2-pc", + "langpacks-en", + }, + ExcludedPackages: []string{ + "dracut-config-rescue", + "etables", + "firewalld", + "gobject-introspection", + "plymouth", + }, + KernelOptions: "ro biosdevname=0 net.ifnames=0", + IncludeFSTab: true, + Assembler: r.qemuAssembler("qcow2", "image.qcow2"), + } + + r.outputs["openstack"] = output{ + Name: "image.qcow2", + MimeType: "application/x-qemu-disk", + Packages: []string{ + "@Core", + "chrony", + "kernel", + "selinux-policy-targeted", + "grub2-pc", + "spice-vdagent", + "qemu-guest-agent", + "xen-libs", + "langpacks-en", + "cloud-init", + "libdrm", + }, + ExcludedPackages: []string{ + "dracut-config-rescue", + }, + KernelOptions: "ro biosdevname=0 net.ifnames=0", + IncludeFSTab: true, + Assembler: r.qemuAssembler("qcow2", "image.qcow2"), + } + + 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", + IncludeFSTab: false, + Assembler: r.tarAssembler("root.tar.xz", "xz"), + } + + r.outputs["vhd"] = output{ + Name: "image.vhd", + MimeType: "application/x-vhd", + Packages: []string{ + "@Core", + "chrony", + "kernel", + "selinux-policy-targeted", + "grub2-pc", + "langpacks-en", + "net-tools", + "ntfsprogs", + "WALinuxAgent", + "libxcrypt-compat", + }, + ExcludedPackages: []string{ + "dracut-config-rescue", + }, + KernelOptions: "ro biosdevname=0 net.ifnames=0", + IncludeFSTab: true, + Assembler: r.qemuAssembler("vpc", "image.vhd"), + } + + r.outputs["vmdk"] = output{ + Name: "disk.vmdk", + MimeType: "application/x-vmdk", + Packages: []string{ + "@core", + "chrony", + "firewalld", + "grub2-pc", + "kernel", + "langpacks-en", + "open-vm-tools", + "selinux-policy-targeted", + }, + ExcludedPackages: []string{ + "dracut-config-rescue", + }, + KernelOptions: "ro biosdevname=0 net.ifnames=0", + IncludeFSTab: true, + Assembler: r.qemuAssembler("vmdk", "disk.vmdk"), + } + + distro.Register("fedora-30", &r) +} + +func (r *Fedora30) Repositories() []rpmmd.RepoConfig { + return []rpmmd.RepoConfig{ + { + Id: "fedora", + Name: "Fedora 30", + Metalink: "https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch", + Checksum: "sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97", + GPGKey: `-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBFturGcBEACv0xBo91V2n0uEC2vh69ywCiSyvUgN/AQH8EZpCVtM7NyjKgKm +bbY4G3R0M3ir1xXmvUDvK0493/qOiFrjkplvzXFTGpPTi0ypqGgxc5d0ohRA1M75 +L+0AIlXoOgHQ358/c4uO8X0JAA1NYxCkAW1KSJgFJ3RjukrfqSHWthS1d4o8fhHy +KJKEnirE5hHqB50dafXrBfgZdaOs3C6ppRIePFe2o4vUEapMTCHFw0woQR8Ah4/R +n7Z9G9Ln+0Cinmy0nbIDiZJ+pgLAXCOWBfDUzcOjDGKvcpoZharA07c0q1/5ojzO +4F0Fh4g/BUmtrASwHfcIbjHyCSr1j/3Iz883iy07gJY5Yhiuaqmp0o0f9fgHkG53 +2xCU1owmACqaIBNQMukvXRDtB2GJMuKa/asTZDP6R5re+iXs7+s9ohcRRAKGyAyc +YKIQKcaA+6M8T7/G+TPHZX6HJWqJJiYB+EC2ERblpvq9TPlLguEWcmvjbVc31nyq +SDoO3ncFWKFmVsbQPTbP+pKUmlLfJwtb5XqxNR5GEXSwVv4I7IqBmJz1MmRafnBZ +g0FJUtH668GnldO20XbnSVBr820F5SISMXVwCXDXEvGwwiB8Lt8PvqzXnGIFDAu3 +DlQI5sxSqpPVWSyw08ppKT2Tpmy8adiBotLfaCFl2VTHwOae48X2dMPBvQARAQAB +tDFGZWRvcmEgKDMwKSA8ZmVkb3JhLTMwLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v +cmc+iQI4BBMBAgAiBQJbbqxnAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK +CRDvPBEfz8ZZudTnD/9170LL3nyTVUCFmBjT9wZ4gYnpwtKVPa/pKnxbbS+Bmmac +g9TrT9pZbqOHrNJLiZ3Zx1Hp+8uxr3Lo6kbYwImLhkOEDrf4aP17HfQ6VYFbQZI8 +f79OFxWJ7si9+3gfzeh9UYFEqOQfzIjLWFyfnas0OnV/P+RMQ1Zr+vPRqO7AR2va +N9wg+Xl7157dhXPCGYnGMNSoxCbpRs0JNlzvJMuAea5nTTznRaJZtK/xKsqLn51D +K07k9MHVFXakOH8QtMCUglbwfTfIpO5YRq5imxlWbqsYWVQy1WGJFyW6hWC0+RcJ +Ox5zGtOfi4/dN+xJ+ibnbyvy/il7Qm+vyFhCYqIPyS5m2UVJUuao3eApE38k78/o +8aQOTnFQZ+U1Sw+6woFTxjqRQBXlQm2+7Bt3bqGATg4sXXWPbmwdL87Ic+mxn/ml +SMfQux/5k6iAu1kQhwkO2YJn9eII6HIPkW+2m5N1JsUyJQe4cbtZE5Yh3TRA0dm7 ++zoBRfCXkOW4krchbgww/ptVmzMMP7GINJdROrJnsGl5FVeid9qHzV7aZycWSma7 +CxBYB1J8HCbty5NjtD6XMYRrMLxXugvX6Q4NPPH+2NKjzX4SIDejS6JjgrP3KA3O +pMuo7ZHMfveBngv8yP+ZD/1sS6l+dfExvdaJdOdgFCnp4p3gPbw5+Lv70HrMjA== +=BfZ/ +-----END PGP PUBLIC KEY BLOCK----- +`, + }, + } +} + +func (r *Fedora30) 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 *Fedora30) FilenameFromType(outputFormat string) (string, string, error) { + if output, exists := r.outputs[outputFormat]; exists { + return output.Name, output.MimeType, nil + } + return "", "", &distro.InvalidOutputFormatError{outputFormat} +} + +func (r *Fedora30) Pipeline(b *blueprint.Blueprint, outputFormat string) (*pipeline.Pipeline, error) { + output, exists := r.outputs[outputFormat] + if !exists { + return nil, &distro.InvalidOutputFormatError{outputFormat} + } + + p := &pipeline.Pipeline{} + p.SetBuild(r.buildPipeline(), "org.osbuild.fedora30") + + packages := append(output.Packages, b.GetPackages()...) + p.AddStage(pipeline.NewDNFStage(r.dnfStageOptions(packages, output.ExcludedPackages))) + p.AddStage(pipeline.NewFixBLSStage()) + + // TODO support setting all languages and install corresponding langpack-* package + language, keyboard := b.GetPrimaryLocale() + + if language != nil { + p.AddStage(pipeline.NewLocaleStage(&pipeline.LocaleStageOptions{*language})) + } else { + p.AddStage(pipeline.NewLocaleStage(&pipeline.LocaleStageOptions{"en_US"})) + } + + if keyboard != nil { + p.AddStage(pipeline.NewKeymapStage(&pipeline.KeymapStageOptions{*keyboard})) + } + + if hostname := b.GetHostname(); hostname != nil { + p.AddStage(pipeline.NewHostnameStage(&pipeline.HostnameStageOptions{*hostname})) + } + + timezone, ntpServers := b.GetTimezoneSettings() + + // TODO install chrony when this is set? + if timezone != nil { + p.AddStage(pipeline.NewTimezoneStage(&pipeline.TimezoneStageOptions{*timezone})) + } + + if len(ntpServers) > 0 { + p.AddStage(pipeline.NewChronyStage(&pipeline.ChronyStageOptions{ntpServers})) + } + + if users := b.GetUsers(); len(users) > 0 { + options, err := r.userStageOptions(users) + if err != nil { + return nil, err + } + p.AddStage(pipeline.NewUsersStage(options)) + } + + if groups := b.GetGroups(); len(groups) > 0 { + p.AddStage(pipeline.NewGroupsStage(r.groupStageOptions(groups))) + } + + if output.IncludeFSTab { + p.AddStage(pipeline.NewFSTabStage(r.fsTabStageOptions())) + } + p.AddStage(pipeline.NewGRUB2Stage(r.grub2StageOptions(output.KernelOptions, b.GetKernel()))) + + if services := b.GetServices(); services != nil || output.EnabledServices != nil { + p.AddStage(pipeline.NewSystemdStage(r.systemdStageOptions(output.EnabledServices, output.DisabledServices, services))) + } + + if firewall := b.GetFirewall(); firewall != nil { + p.AddStage(pipeline.NewFirewallStage(r.firewallStageOptions(firewall))) + } + + p.AddStage(pipeline.NewSELinuxStage(r.selinuxStageOptions())) + p.Assembler = output.Assembler + + return p, nil +} + +func (r *Fedora30) buildPipeline() *pipeline.Pipeline { + packages := []string{ + "dnf", + "e2fsprogs", + "policycoreutils", + "qemu-img", + "systemd", + "grub2-pc", + "tar", + } + p := &pipeline.Pipeline{} + p.AddStage(pipeline.NewDNFStage(r.dnfStageOptions(packages, nil))) + return p +} + +func (r *Fedora30) dnfStageOptions(packages, excludedPackages []string) *pipeline.DNFStageOptions { + options := &pipeline.DNFStageOptions{ + ReleaseVersion: "30", + BaseArchitecture: "x86_64", + } + for _, repo := range r.Repositories() { + options.AddRepository(&pipeline.DNFRepository{ + BaseURL: repo.BaseURL, + MetaLink: repo.Metalink, + MirrorList: repo.MirrorList, + Checksum: repo.Checksum, + GPGKey: repo.GPGKey, + }) + } + + for _, pkg := range packages { + options.AddPackage(pkg) + } + + for _, pkg := range excludedPackages { + options.ExcludePackage(pkg) + } + + return options +} + +func (r *Fedora30) userStageOptions(users []blueprint.UserCustomization) (*pipeline.UsersStageOptions, error) { + options := pipeline.UsersStageOptions{ + Users: make(map[string]pipeline.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 := pipeline.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 *Fedora30) groupStageOptions(groups []blueprint.GroupCustomization) *pipeline.GroupsStageOptions { + options := pipeline.GroupsStageOptions{ + Groups: map[string]pipeline.GroupsStageOptionsGroup{}, + } + + for _, group := range groups { + groupData := pipeline.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 *Fedora30) firewallStageOptions(firewall *blueprint.FirewallCustomization) *pipeline.FirewallStageOptions { + options := pipeline.FirewallStageOptions{ + Ports: firewall.Ports, + } + + if firewall.Services != nil { + options.EnabledServices = firewall.Services.Enabled + options.DisabledServices = firewall.Services.Disabled + } + + return &options +} + +func (r *Fedora30) systemdStageOptions(enabledServices, disabledServices []string, s *blueprint.ServicesCustomization) *pipeline.SystemdStageOptions { + if s != nil { + enabledServices = append(enabledServices, s.Enabled...) + enabledServices = append(disabledServices, s.Disabled...) + } + return &pipeline.SystemdStageOptions{ + EnabledServices: enabledServices, + DisabledServices: disabledServices, + } +} + +func (r *Fedora30) fsTabStageOptions() *pipeline.FSTabStageOptions { + id, err := uuid.Parse("76a22bf4-f153-4541-b6c7-0332c0dfaeac") + if err != nil { + panic("invalid UUID") + } + options := pipeline.FSTabStageOptions{} + options.AddFilesystem(id, "ext4", "/", "defaults", 1, 1) + return &options +} + +func (r *Fedora30) grub2StageOptions(kernelOptions string, kernel *blueprint.KernelCustomization) *pipeline.GRUB2StageOptions { + id, err := uuid.Parse("76a22bf4-f153-4541-b6c7-0332c0dfaeac") + if err != nil { + panic("invalid UUID") + } + + if kernel != nil { + kernelOptions += " " + kernel.Append + } + + return &pipeline.GRUB2StageOptions{ + RootFilesystemUUID: id, + KernelOptions: kernelOptions, + } +} + +func (r *Fedora30) selinuxStageOptions() *pipeline.SELinuxStageOptions { + return &pipeline.SELinuxStageOptions{ + FileContexts: "etc/selinux/targeted/contexts/files/file_contexts", + } +} + +func (r *Fedora30) qemuAssembler(format string, filename string) *pipeline.Assembler { + id, err := uuid.Parse("76a22bf4-f153-4541-b6c7-0332c0dfaeac") + if err != nil { + panic("invalid UUID") + } + return pipeline.NewQEMUAssembler( + &pipeline.QEMUAssemblerOptions{ + Format: format, + Filename: filename, + PTUUID: "0x14fc63d2", + RootFilesystemUUDI: id, + Size: 3222274048, + }) +} + +func (r *Fedora30) tarAssembler(filename, compression string) *pipeline.Assembler { + return pipeline.NewTarAssembler( + &pipeline.TarAssemblerOptions{ + Filename: filename, + }) +} + +func (r *Fedora30) rawFSAssembler(filename string) *pipeline.Assembler { + id, err := uuid.Parse("76a22bf4-f153-4541-b6c7-0332c0dfaeac") + if err != nil { + panic("invalid UUID") + } + return pipeline.NewRawFSAssembler( + &pipeline.RawFSAssemblerOptions{ + Filename: filename, + RootFilesystemUUDI: id, + Size: 3222274048, + }) +} diff --git a/internal/distro/fedora30/os_test.go b/internal/distro/fedora30/distro_test.go similarity index 100% rename from internal/distro/fedora30/os_test.go rename to internal/distro/fedora30/distro_test.go diff --git a/internal/distro/fedora30/ext4.go b/internal/distro/fedora30/ext4.go deleted file mode 100644 index ffa381e64..000000000 --- a/internal/distro/fedora30/ext4.go +++ /dev/null @@ -1,41 +0,0 @@ -package fedora30 - -import ( - "github.com/osbuild/osbuild-composer/internal/blueprint" - "github.com/osbuild/osbuild-composer/internal/pipeline" -) - -type ext4Output struct{} - -func (t *ext4Output) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) { - packages := [...]string{ - "policycoreutils", - "selinux-policy-targeted", - "kernel", - "firewalld", - "chrony", - "langpacks-en", - } - excludedPackages := [...]string{ - "dracut-config-rescue", - } - p := newF30Pipeline(packages[:], excludedPackages[:], b) - err := customizeAll(p, b.Customizations) - if err != nil { - return nil, err - } - setBootloader(p, "ro biosdevname=0 net.ifnames=0", b) - setFirewall(p, nil, nil, b) - setServices(p, nil, nil, b) - setRawFSAssembler(p, t.getName()) - - return p, nil -} - -func (t *ext4Output) getName() string { - return "filesystem.img" -} - -func (t *ext4Output) getMime() string { - return "application/octet-stream" -} diff --git a/internal/distro/fedora30/helpers.go b/internal/distro/fedora30/helpers.go deleted file mode 100644 index 327968b98..000000000 --- a/internal/distro/fedora30/helpers.go +++ /dev/null @@ -1,221 +0,0 @@ -package fedora30 - -import ( - "github.com/osbuild/osbuild-composer/internal/blueprint" - "github.com/osbuild/osbuild-composer/internal/pipeline" - - "github.com/google/uuid" -) - -var f30GPGKey string = `-----BEGIN PGP PUBLIC KEY BLOCK----- - -mQINBFturGcBEACv0xBo91V2n0uEC2vh69ywCiSyvUgN/AQH8EZpCVtM7NyjKgKm -bbY4G3R0M3ir1xXmvUDvK0493/qOiFrjkplvzXFTGpPTi0ypqGgxc5d0ohRA1M75 -L+0AIlXoOgHQ358/c4uO8X0JAA1NYxCkAW1KSJgFJ3RjukrfqSHWthS1d4o8fhHy -KJKEnirE5hHqB50dafXrBfgZdaOs3C6ppRIePFe2o4vUEapMTCHFw0woQR8Ah4/R -n7Z9G9Ln+0Cinmy0nbIDiZJ+pgLAXCOWBfDUzcOjDGKvcpoZharA07c0q1/5ojzO -4F0Fh4g/BUmtrASwHfcIbjHyCSr1j/3Iz883iy07gJY5Yhiuaqmp0o0f9fgHkG53 -2xCU1owmACqaIBNQMukvXRDtB2GJMuKa/asTZDP6R5re+iXs7+s9ohcRRAKGyAyc -YKIQKcaA+6M8T7/G+TPHZX6HJWqJJiYB+EC2ERblpvq9TPlLguEWcmvjbVc31nyq -SDoO3ncFWKFmVsbQPTbP+pKUmlLfJwtb5XqxNR5GEXSwVv4I7IqBmJz1MmRafnBZ -g0FJUtH668GnldO20XbnSVBr820F5SISMXVwCXDXEvGwwiB8Lt8PvqzXnGIFDAu3 -DlQI5sxSqpPVWSyw08ppKT2Tpmy8adiBotLfaCFl2VTHwOae48X2dMPBvQARAQAB -tDFGZWRvcmEgKDMwKSA8ZmVkb3JhLTMwLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v -cmc+iQI4BBMBAgAiBQJbbqxnAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK -CRDvPBEfz8ZZudTnD/9170LL3nyTVUCFmBjT9wZ4gYnpwtKVPa/pKnxbbS+Bmmac -g9TrT9pZbqOHrNJLiZ3Zx1Hp+8uxr3Lo6kbYwImLhkOEDrf4aP17HfQ6VYFbQZI8 -f79OFxWJ7si9+3gfzeh9UYFEqOQfzIjLWFyfnas0OnV/P+RMQ1Zr+vPRqO7AR2va -N9wg+Xl7157dhXPCGYnGMNSoxCbpRs0JNlzvJMuAea5nTTznRaJZtK/xKsqLn51D -K07k9MHVFXakOH8QtMCUglbwfTfIpO5YRq5imxlWbqsYWVQy1WGJFyW6hWC0+RcJ -Ox5zGtOfi4/dN+xJ+ibnbyvy/il7Qm+vyFhCYqIPyS5m2UVJUuao3eApE38k78/o -8aQOTnFQZ+U1Sw+6woFTxjqRQBXlQm2+7Bt3bqGATg4sXXWPbmwdL87Ic+mxn/ml -SMfQux/5k6iAu1kQhwkO2YJn9eII6HIPkW+2m5N1JsUyJQe4cbtZE5Yh3TRA0dm7 -+zoBRfCXkOW4krchbgww/ptVmzMMP7GINJdROrJnsGl5FVeid9qHzV7aZycWSma7 -CxBYB1J8HCbty5NjtD6XMYRrMLxXugvX6Q4NPPH+2NKjzX4SIDejS6JjgrP3KA3O -pMuo7ZHMfveBngv8yP+ZD/1sS6l+dfExvdaJdOdgFCnp4p3gPbw5+Lv70HrMjA== -=BfZ/ ------END PGP PUBLIC KEY BLOCK----- -` - -func getF30Repository() *pipeline.DNFRepository { - repo := pipeline.NewDNFRepository("https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch", "", "") - repo.SetChecksum("sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97") - repo.SetGPGKey(f30GPGKey) - return repo -} - -func getF30BuildPipeline() *pipeline.Pipeline { - p := &pipeline.Pipeline{} - options := &pipeline.DNFStageOptions{ - ReleaseVersion: "30", - BaseArchitecture: "x86_64", - } - options.AddRepository(getF30Repository()) - options.AddPackage("dnf") - options.AddPackage("e2fsprogs") - options.AddPackage("policycoreutils") - options.AddPackage("qemu-img") - options.AddPackage("systemd") - options.AddPackage("grub2-pc") - options.AddPackage("tar") - p.AddStage(pipeline.NewDNFStage(options)) - return p -} - -func newF30Pipeline(packages []string, excludedPackages []string, blueprint *blueprint.Blueprint) *pipeline.Pipeline { - p := &pipeline.Pipeline{} - p.SetBuild(getF30BuildPipeline(), "org.osbuild.fedora30") - options := &pipeline.DNFStageOptions{ - ReleaseVersion: "30", - BaseArchitecture: "x86_64", - } - options.AddRepository(getF30Repository()) - for _, pkg := range packages { - options.AddPackage(pkg) - } - for _, pkg := range excludedPackages { - options.ExcludePackage(pkg) - } - - for _, pkg := range blueprint.Packages { - options.AddPackage(pkg.ToNameVersion()) - } - - for _, pkg := range blueprint.Modules { - options.AddPackage(pkg.ToNameVersion()) - } - - for _, group := range blueprint.Groups { - options.AddPackage(group.Name) - } - - p.AddStage(pipeline.NewDNFStage(options)) - - /* grub2 mangles the BLS snippets, we must fix them manually */ - p.AddStage(pipeline.NewFixBLSStage()) - - return p -} - -func setBootloader(p *pipeline.Pipeline, kernelOptions string, blueprint *blueprint.Blueprint) { - id, err := uuid.Parse("76a22bf4-f153-4541-b6c7-0332c0dfaeac") - if err != nil { - panic("invalid UUID") - } - - kernelCustomization := blueprint.GetKernel() - if kernelCustomization != nil { - kernelOptions += " " + kernelCustomization.Append - } - - p.AddStage(pipeline.NewGRUB2Stage( - &pipeline.GRUB2StageOptions{ - RootFilesystemUUID: id, - KernelOptions: kernelOptions, - }, - )) -} - -func setFilesystems(p *pipeline.Pipeline) { - id, err := uuid.Parse("76a22bf4-f153-4541-b6c7-0332c0dfaeac") - if err != nil { - panic("invalid UUID") - } - options := &pipeline.FSTabStageOptions{} - options.AddFilesystem(id, "ext4", "/", "defaults", 1, 1) - p.AddStage(pipeline.NewFSTabStage(options)) -} - -func setFirewall(p *pipeline.Pipeline, enabledServices []string, disabledServices []string, b *blueprint.Blueprint) { - f := b.GetFirewall() - ports := []string{} - - if f != nil { - if f.Services != nil { - enabledServices = append(enabledServices, f.Services.Enabled...) - disabledServices = append(disabledServices, f.Services.Disabled...) - } - ports = f.Ports - } - - if len(enabledServices) == 0 && len(disabledServices) == 0 && len(ports) == 0 { - return - } - - p.AddStage( - pipeline.NewFirewallStage(&pipeline.FirewallStageOptions{ - Ports: ports, - EnabledServices: enabledServices, - DisabledServices: disabledServices, - }), - ) -} - -func setServices(p *pipeline.Pipeline, enabledServices []string, disabledServices []string, b *blueprint.Blueprint) { - s := b.GetServices() - - if s != nil { - enabledServices = append(enabledServices, s.Enabled...) - disabledServices = append(disabledServices, s.Enabled...) - } - - if len(enabledServices) == 0 && len(disabledServices) == 0 { - return - } - - p.AddStage( - pipeline.NewSystemdStage(&pipeline.SystemdStageOptions{ - EnabledServices: enabledServices, - DisabledServices: disabledServices, - }), - ) -} - -func setQemuAssembler(p *pipeline.Pipeline, format string, filename string) { - p.AddStage(pipeline.NewSELinuxStage( - &pipeline.SELinuxStageOptions{ - FileContexts: "etc/selinux/targeted/contexts/files/file_contexts", - })) - id, err := uuid.Parse("76a22bf4-f153-4541-b6c7-0332c0dfaeac") - if err != nil { - panic("invalid UUID") - } - p.Assembler = pipeline.NewQEMUAssembler( - &pipeline.QEMUAssemblerOptions{ - Format: format, - Filename: filename, - PTUUID: "0x14fc63d2", - RootFilesystemUUDI: id, - // Azure requires this size to be a multiple of 1MB. If you change this, make sure - // the size still fulfills this requirement to prevent regressions. - Size: 3222274048, - }) -} - -func setTarAssembler(p *pipeline.Pipeline, filename, compression string) { - p.AddStage(pipeline.NewSELinuxStage( - &pipeline.SELinuxStageOptions{ - FileContexts: "etc/selinux/targeted/contexts/files/file_contexts", - })) - p.Assembler = pipeline.NewTarAssembler( - &pipeline.TarAssemblerOptions{ - Filename: filename, - }) -} - -func setRawFSAssembler(p *pipeline.Pipeline, filename string) { - p.AddStage(pipeline.NewSELinuxStage( - &pipeline.SELinuxStageOptions{ - FileContexts: "etc/selinux/targeted/contexts/files/file_contexts", - })) - id, err := uuid.Parse("76a22bf4-f153-4541-b6c7-0332c0dfaeac") - if err != nil { - panic("invalid UUID") - } - p.Assembler = pipeline.NewRawFSAssembler( - &pipeline.RawFSAssemblerOptions{ - Filename: filename, - RootFilesystemUUDI: id, - Size: 3222274048, - }) -} diff --git a/internal/distro/fedora30/openstack.go b/internal/distro/fedora30/openstack.go deleted file mode 100644 index 1b56e469f..000000000 --- a/internal/distro/fedora30/openstack.go +++ /dev/null @@ -1,47 +0,0 @@ -package fedora30 - -import ( - "github.com/osbuild/osbuild-composer/internal/blueprint" - "github.com/osbuild/osbuild-composer/internal/pipeline" -) - -type openstackOutput struct{} - -func (t *openstackOutput) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) { - packages := [...]string{ - "@Core", - "chrony", - "kernel", - "selinux-policy-targeted", - "grub2-pc", - "spice-vdagent", - "qemu-guest-agent", - "xen-libs", - "langpacks-en", - "cloud-init", - "libdrm", - } - excludedPackages := [...]string{ - "dracut-config-rescue", - } - p := newF30Pipeline(packages[:], excludedPackages[:], b) - err := customizeAll(p, b.Customizations) - if err != nil { - return nil, err - } - setFilesystems(p) - setBootloader(p, "ro biosdevname=0 net.ifnames=0", b) - setFirewall(p, nil, nil, b) - setServices(p, nil, nil, b) - setQemuAssembler(p, "qcow2", t.getName()) - - return p, nil -} - -func (t *openstackOutput) getName() string { - return "image.qcow2" -} - -func (t *openstackOutput) getMime() string { - return "application/x-qemu-disk" -} diff --git a/internal/distro/fedora30/os.go b/internal/distro/fedora30/os.go deleted file mode 100644 index 3f90a89b7..000000000 --- a/internal/distro/fedora30/os.go +++ /dev/null @@ -1,70 +0,0 @@ -package fedora30 - -import ( - "sort" - - "github.com/osbuild/osbuild-composer/internal/blueprint" - "github.com/osbuild/osbuild-composer/internal/distro" - "github.com/osbuild/osbuild-composer/internal/pipeline" - "github.com/osbuild/osbuild-composer/internal/rpmmd" -) - -type Fedora30 struct { - outputs map[string]output -} - -type output interface { - translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) - getName() string - getMime() string -} - -func init() { - distro.Register("fedora-30", &Fedora30{ - outputs: map[string]output{ - "ami": &amiOutput{}, - "ext4-filesystem": &ext4Output{}, - "partitioned-disk": &diskOutput{}, - "qcow2": &qcow2Output{}, - "openstack": &openstackOutput{}, - "tar": &tarOutput{}, - "vhd": &vhdOutput{}, - "vmdk": &vmdkOutput{}, - }, - }) -} - -func (f *Fedora30) Repositories() []rpmmd.RepoConfig { - return []rpmmd.RepoConfig{ - { - Id: "fedora", - Name: "Fedora 30", - Metalink: "https://mirrors.fedoraproject.org/metalink?repo=fedora-30&arch=x86_64", - Checksum: "sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97", - }, - } -} - -// ListOutputFormats returns a sorted list of the supported output formats -func (f *Fedora30) ListOutputFormats() []string { - formats := make([]string, 0, len(f.outputs)) - for name := range f.outputs { - formats = append(formats, name) - } - sort.Strings(formats) - return formats -} - -func (f *Fedora30) FilenameFromType(outputFormat string) (string, string, error) { - if output, exists := f.outputs[outputFormat]; exists { - return output.getName(), output.getMime(), nil - } - return "", "", &distro.InvalidOutputFormatError{outputFormat} -} - -func (f *Fedora30) Pipeline(b *blueprint.Blueprint, outputFormat string) (*pipeline.Pipeline, error) { - if output, exists := f.outputs[outputFormat]; exists { - return output.translate(b) - } - return nil, &distro.InvalidOutputFormatError{outputFormat} -} diff --git a/internal/distro/fedora30/qcow2.go b/internal/distro/fedora30/qcow2.go deleted file mode 100644 index 2d3f83fce..000000000 --- a/internal/distro/fedora30/qcow2.go +++ /dev/null @@ -1,48 +0,0 @@ -package fedora30 - -import ( - "github.com/osbuild/osbuild-composer/internal/blueprint" - "github.com/osbuild/osbuild-composer/internal/pipeline" -) - -type qcow2Output struct{} - -func (t *qcow2Output) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) { - packages := [...]string{ - "kernel-core", - "@Fedora Cloud Server", - "chrony", - "polkit", - "systemd-udev", - "selinux-policy-targeted", - "grub2-pc", - "langpacks-en", - } - excludedPackages := [...]string{ - "dracut-config-rescue", - "etables", - "firewalld", - "gobject-introspection", - "plymouth", - } - p := newF30Pipeline(packages[:], excludedPackages[:], b) - err := customizeAll(p, b.Customizations) - if err != nil { - return nil, err - } - setFilesystems(p) - setBootloader(p, "ro biosdevname=0 net.ifnames=0", b) - setFirewall(p, nil, nil, b) - setServices(p, nil, nil, b) - setQemuAssembler(p, "qcow2", t.getName()) - - return p, nil -} - -func (t *qcow2Output) getName() string { - return "image.qcow2" -} - -func (t *qcow2Output) getMime() string { - return "application/x-qemu-disk" -} diff --git a/internal/distro/fedora30/tar.go b/internal/distro/fedora30/tar.go deleted file mode 100644 index d469abcb4..000000000 --- a/internal/distro/fedora30/tar.go +++ /dev/null @@ -1,41 +0,0 @@ -package fedora30 - -import ( - "github.com/osbuild/osbuild-composer/internal/blueprint" - "github.com/osbuild/osbuild-composer/internal/pipeline" -) - -type tarOutput struct{} - -func (t *tarOutput) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) { - packages := [...]string{ - "policycoreutils", - "selinux-policy-targeted", - "kernel", - "firewalld", - "chrony", - "langpacks-en", - } - excludedPackages := [...]string{ - "dracut-config-rescue", - } - p := newF30Pipeline(packages[:], excludedPackages[:], b) - err := customizeAll(p, b.Customizations) - if err != nil { - return nil, err - } - setBootloader(p, "ro biosdevname=0 net.ifnames=0", b) - setFirewall(p, nil, nil, b) - setServices(p, nil, nil, b) - setTarAssembler(p, t.getName(), "xz") - - return p, nil -} - -func (t *tarOutput) getName() string { - return "root.tar.xz" -} - -func (t *tarOutput) getMime() string { - return "application/x-tar" -} diff --git a/internal/distro/fedora30/vhd.go b/internal/distro/fedora30/vhd.go deleted file mode 100644 index 5fb7239a4..000000000 --- a/internal/distro/fedora30/vhd.go +++ /dev/null @@ -1,46 +0,0 @@ -package fedora30 - -import ( - "github.com/osbuild/osbuild-composer/internal/blueprint" - "github.com/osbuild/osbuild-composer/internal/pipeline" -) - -type vhdOutput struct{} - -func (t *vhdOutput) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) { - packages := [...]string{ - "@Core", - "chrony", - "kernel", - "selinux-policy-targeted", - "grub2-pc", - "langpacks-en", - "net-tools", - "ntfsprogs", - "WALinuxAgent", - "libxcrypt-compat", - } - excludedPackages := [...]string{ - "dracut-config-rescue", - } - p := newF30Pipeline(packages[:], excludedPackages[:], b) - err := customizeAll(p, b.Customizations) - if err != nil { - return nil, err - } - setFilesystems(p) - setBootloader(p, "ro biosdevname=0 net.ifnames=0", b) - setFirewall(p, nil, nil, b) - setServices(p, nil, nil, b) - setQemuAssembler(p, "vpc", t.getName()) - - return p, nil -} - -func (t *vhdOutput) getName() string { - return "image.vhd" -} - -func (t *vhdOutput) getMime() string { - return "application/x-vhd" -} diff --git a/internal/distro/fedora30/vmdk.go b/internal/distro/fedora30/vmdk.go deleted file mode 100644 index adb7e8cb7..000000000 --- a/internal/distro/fedora30/vmdk.go +++ /dev/null @@ -1,44 +0,0 @@ -package fedora30 - -import ( - "github.com/osbuild/osbuild-composer/internal/blueprint" - "github.com/osbuild/osbuild-composer/internal/pipeline" -) - -type vmdkOutput struct{} - -func (t *vmdkOutput) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) { - packages := [...]string{ - "@core", - "chrony", - "firewalld", - "grub2-pc", - "kernel", - "langpacks-en", - "open-vm-tools", - "selinux-policy-targeted", - } - excludedPackages := [...]string{ - "dracut-config-rescue", - } - p := newF30Pipeline(packages[:], excludedPackages[:], b) - err := customizeAll(p, b.Customizations) - if err != nil { - return nil, err - } - setFilesystems(p) - setBootloader(p, "ro biosdevname=0 net.ifnames=0", b) - setFirewall(p, nil, nil, b) - setServices(p, nil, nil, b) - setQemuAssembler(p, "vmdk", t.getName()) - - return p, nil -} - -func (t *vmdkOutput) getName() string { - return "disk.vmdk" -} - -func (t *vmdkOutput) getMime() string { - return "application/x-vmdk" -} diff --git a/internal/rpmmd/repository.go b/internal/rpmmd/repository.go index 465d2dc35..fc07970ac 100644 --- a/internal/rpmmd/repository.go +++ b/internal/rpmmd/repository.go @@ -19,6 +19,7 @@ type RepoConfig struct { Metalink string `json:"metalink,omitempty"` MirrorList string `json:"mirrorlist,omitempty"` Checksum string `json:"checksum,omitempty"` + GPGKey string `json:"gpgkey,omitempty"` } type PackageList []Package