diff --git a/cmd/osbuild-pipeline/main.go b/cmd/osbuild-pipeline/main.go index 8772556af..7ac3de033 100644 --- a/cmd/osbuild-pipeline/main.go +++ b/cmd/osbuild-pipeline/main.go @@ -7,6 +7,9 @@ import ( "os" "github.com/osbuild/osbuild-composer/internal/blueprint" + + "github.com/osbuild/osbuild-composer/internal/distro" + _ "github.com/osbuild/osbuild-composer/internal/distro/fedora30" ) func main() { @@ -27,7 +30,9 @@ func main() { panic("Colud not parse blueprint") } } - pipeline, err := blueprint.ToPipeline(format) + + f30 := distro.New("fedora-30") + pipeline, err := f30.Pipeline(blueprint, format) if err != nil { panic(err.Error()) } diff --git a/internal/blueprint/blueprint.go b/internal/blueprint/blueprint.go index 123a62461..3d1016666 100644 --- a/internal/blueprint/blueprint.go +++ b/internal/blueprint/blueprint.go @@ -1,13 +1,6 @@ -// Package blueprint contains primitives for representing weldr blueprints and -// translating them to OSBuild pipelines +// Package blueprint contains primitives for representing weldr blueprints package blueprint -import ( - "sort" - - "github.com/osbuild/osbuild-composer/internal/pipeline" -) - // An InvalidOutputFormatError is returned when a requested output format is // not supported. The requested format is included as the error message. type InvalidOutputFormatError struct { @@ -40,45 +33,7 @@ type Group struct { Name string `json:"name"` } -type output interface { - translate(b *Blueprint) (*pipeline.Pipeline, error) - getName() string - getMime() string -} - -var outputs = map[string]output{ - "ami": &amiOutput{}, - "ext4-filesystem": &ext4Output{}, - "live-iso": &liveIsoOutput{}, - "partitioned-disk": &diskOutput{}, - "qcow2": &qcow2Output{}, - "openstack": &openstackOutput{}, - "tar": &tarOutput{}, - "vhd": &vhdOutput{}, - "vmdk": &vmdkOutput{}, -} - -// ListOutputFormats returns a sorted list of the supported output formats -func ListOutputFormats() []string { - formats := make([]string, 0, len(outputs)) - for name := range outputs { - formats = append(formats, name) - } - sort.Strings(formats) - - return formats -} - -// ToPipeline converts the blueprint to a pipeline for a given output format. -func (b *Blueprint) ToPipeline(outputFormat string) (*pipeline.Pipeline, error) { - if output, exists := outputs[outputFormat]; exists { - return output.translate(b) - } - - return nil, &InvalidOutputFormatError{outputFormat} -} - -func (b *Blueprint) getKernelCustomization() *KernelCustomization { +func (b *Blueprint) GetKernelCustomization() *KernelCustomization { if b.Customizations == nil { return nil } @@ -86,21 +41,11 @@ func (b *Blueprint) getKernelCustomization() *KernelCustomization { return b.Customizations.Kernel } -// FilenameFromType gets the canonical filename and MIME type for a given -// output format -func FilenameFromType(outputFormat string) (string, string, error) { - if output, exists := outputs[outputFormat]; exists { - return output.getName(), output.getMime(), nil - } - - return "", "", &InvalidOutputFormatError{outputFormat} -} - func (p Package) ToNameVersion() string { - // Omit version to prevent all packages with prefix of name to be installed - if p.Version == "*" { - return p.Name - } + // Omit version to prevent all packages with prefix of name to be installed + if p.Version == "*" { + return p.Name + } - return p.Name + "-" + p.Version + return p.Name + "-" + p.Version } diff --git a/internal/blueprint/blueprint_test.go b/internal/blueprint/blueprint_test.go index 9484e975e..2d81790f0 100644 --- a/internal/blueprint/blueprint_test.go +++ b/internal/blueprint/blueprint_test.go @@ -1,13 +1,9 @@ package blueprint_test import ( - "encoding/json" - "io/ioutil" - "reflect" "testing" "github.com/osbuild/osbuild-composer/internal/blueprint" - "github.com/osbuild/osbuild-composer/internal/pipeline" ) func TestInvalidOutputFormatError_Error(t *testing.T) { @@ -29,167 +25,3 @@ func TestInvalidOutputFormatError_Error(t *testing.T) { }) } } - -func TestListOutputFormats(t *testing.T) { - tests := []struct { - name string - want []string - }{ - { - name: "basic", - want: []string{ - "ami", - "ext4-filesystem", - "live-iso", - "openstack", - "partitioned-disk", - "qcow2", - "tar", - "vhd", - "vmdk", - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := blueprint.ListOutputFormats(); !reflect.DeepEqual(got, tt.want) { - t.Errorf("ListOutputFormats() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestBlueprint_ToPipeline(t *testing.T) { - pipelinePath := "../../test/cases/" - fileInfos, err := ioutil.ReadDir(pipelinePath) - if err != nil { - t.Errorf("Could not read pipelines directory '%s': %v", pipelinePath, err) - } - for _, fileInfo := range fileInfos { - type compose struct { - OutputFormat string `json:"output-format"` - Blueprint *blueprint.Blueprint `json:"blueprint"` - } - var tt struct { - Compose *compose `json:"compose"` - Pipeline *pipeline.Pipeline `json:"pipeline,omitempty"` - } - file, err := ioutil.ReadFile(pipelinePath + fileInfo.Name()) - if err != nil { - t.Errorf("Colud not read test-case '%s': %v", fileInfo.Name(), err) - } - err = json.Unmarshal([]byte(file), &tt) - if err != nil { - t.Errorf("Colud not parse test-case '%s': %v", fileInfo.Name(), err) - } - if tt.Compose == nil || tt.Compose.Blueprint == nil { - t.Logf("Skipping '%s'.", fileInfo.Name()) - continue - } - t.Run(tt.Compose.OutputFormat, func(t *testing.T) { - got, err := tt.Compose.Blueprint.ToPipeline(tt.Compose.OutputFormat) - if (err != nil) != (tt.Pipeline == nil) { - t.Errorf("Blueprint.ToPipeline() error = %v", err) - return - } - if tt.Pipeline != nil { - if !reflect.DeepEqual(got, tt.Pipeline) { - // Without this the "difference" is just a list of pointers. - gotJson, _ := json.Marshal(got) - fileJson, _ := json.Marshal(tt.Pipeline) - t.Errorf("Blueprint.ToPipeline() =\n%v,\nwant =\n%v", string(gotJson), string(fileJson)) - } - } - }) - } -} - -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.ami", - want1: "application/x-qemu-disk", - }, - { - name: "ext4", - args: args{"ext4-filesystem"}, - want: "filesystem.img", - want1: "application/octet-stream", - }, - { - name: "live-iso", - args: args{"live-iso"}, - want: "image.iso", - want1: "application/x-iso9660-image", - }, - { - name: "openstack", - args: args{"openstack"}, - want: "image.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: "image.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: "image.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) { - got, got1, err := blueprint.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/blueprint/customizations.go b/internal/blueprint/customizations.go index 44243a5db..5d20d49be 100644 --- a/internal/blueprint/customizations.go +++ b/internal/blueprint/customizations.go @@ -1,13 +1,5 @@ package blueprint -import ( - "github.com/osbuild/osbuild-composer/internal/crypt" - "github.com/osbuild/osbuild-composer/internal/pipeline" - "log" - "strconv" - "strings" -) - type Customizations struct { Hostname *string `json:"hostname,omitempty"` Kernel *KernelCustomization `json:"kernel,omitempty"` @@ -72,260 +64,9 @@ type ServicesCustomization struct { } type CustomizationError struct { - message string + Message string } func (e *CustomizationError) Error() string { - return e.message -} - -func (c *Customizations) customizeAll(p *pipeline.Pipeline) error { - c.customizeHostname(p) - c.customizeGroup(p) - if err := c.customizeUserAndSSHKey(p); err != nil { - return err - } - c.customizeTimezone(p) - c.customizeNTPServers(p) - c.customizeLanguages(p) - c.customizeKeyboard(p) - c.customizeFirewall(p) - c.customizeServices(p) - - return nil -} - -func (c *Customizations) customizeHostname(p *pipeline.Pipeline) { - if c.Hostname == nil { - return - } - - p.AddStage( - pipeline.NewHostnameStage( - &pipeline.HostnameStageOptions{Hostname: *c.Hostname}, - ), - ) -} - -func (c *Customizations) customizeGroup(p *pipeline.Pipeline) { - 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 (c *Customizations) assertAllUsersExistForSSHCustomizations() error { - for _, sshkey := range c.SSHKey { - userFound := false - for _, user := range c.User { - if user.Name == sshkey.User { - userFound = true - } - } - - if !userFound { - return &CustomizationError{"Cannot set SSH key for non-existing user " + sshkey.User} - } - } - return nil -} - -func (c *Customizations) customizeUserAndSSHKey(p *pipeline.Pipeline) error { - if len(c.User) == 0 { - if len(c.SSHKey) > 0 { - return &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 := c.assertAllUsersExistForSSHCustomizations(); 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 (c *Customizations) customizeTimezone(p *pipeline.Pipeline) { - 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 (c *Customizations) customizeNTPServers(p *pipeline.Pipeline) { - if c.Timezone == nil || len(c.Timezone.NTPServers) == 0 { - return - } - - p.AddStage( - pipeline.NewChronyStage(&pipeline.ChronyStageOptions{ - Timeservers: c.Timezone.NTPServers, - }), - ) -} - -func (c *Customizations) customizeLanguages(p *pipeline.Pipeline) { - if c.Locale == nil || len(c.Locale.Languages) == 0 { - return - } - - // 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: c.Locale.Languages[0], - }), - ) -} - -func (c *Customizations) customizeKeyboard(p *pipeline.Pipeline) { - if c.Locale == nil || c.Locale.Keyboard == nil { - return - } - - p.AddStage( - pipeline.NewKeymapStage(&pipeline.KeymapStageOptions{ - Keymap: *c.Locale.Keyboard, - }), - ) -} - -func (c *Customizations) customizeFirewall(p *pipeline.Pipeline) { - if c.Firewall == nil { - return - } - - var enabledServices, disabledServices []string - - if c.Firewall.Services != nil { - enabledServices = c.Firewall.Services.Enabled - disabledServices = c.Firewall.Services.Disabled - } - - p.AddStage( - pipeline.NewFirewallStage(&pipeline.FirewallStageOptions{ - Ports: c.Firewall.Ports, - EnabledServices: enabledServices, - DisabledServices: disabledServices, - }), - ) -} - -func (c *Customizations) customizeServices(p *pipeline.Pipeline) { - if c.Services == nil { - return - } - - p.AddStage( - pipeline.NewSystemdStage(&pipeline.SystemdStageOptions{ - EnabledServices: c.Services.Enabled, - DisabledServices: c.Services.Disabled, - }), - ) -} - -func findKeysForUser(sshKeyCustomizations []SSHKeyCustomization, user string) (keys []string) { - for _, sshKey := range sshKeyCustomizations { - if sshKey.User == user { - keys = append(keys, sshKey.Key) - } - } - return -} - -func userCustomizationsContainUsername(userCustomizations []UserCustomization, name string) bool { - for _, usr := range userCustomizations { - if usr.Name == name { - return true - } - } - - return false + return e.Message } diff --git a/internal/distro/distro.go b/internal/distro/distro.go new file mode 100644 index 000000000..6ba2c385c --- /dev/null +++ b/internal/distro/distro.go @@ -0,0 +1,48 @@ +package distro + +import ( + "github.com/osbuild/osbuild-composer/internal/blueprint" + "github.com/osbuild/osbuild-composer/internal/pipeline" +) + +type Distro interface { + // Returns a sorted list of the output formats this distro supports. + ListOutputFormats() []string + + // Returns the canonical filename and MIME type for a given output + // format + FilenameFromType(outputFormat string) (string, string, error) + + // Returns an osbuild pipeline that generates an image in the given + // output format with all packages and customizations specified in the + // given blueprint. `outputFormat` must be one returned by + // ListOutputFormats(). + Pipeline(b *blueprint.Blueprint, outputFormat string) (*pipeline.Pipeline, error) +} + +// An InvalidOutputFormatError is returned when a requested output format is +// not supported. The requested format is included as the error message. +type InvalidOutputFormatError struct { + Format string +} + +func (e *InvalidOutputFormatError) Error() string { + return e.Format +} + +var registered = map[string]Distro{} + +func New(name string) Distro { + distro, ok := registered[name] + if !ok { + panic("unknown distro: " + name) + } + return distro +} + +func Register(name string, distro Distro) { + if _, exists := registered[name]; exists { + panic("a distro with this name already exists: " + name) + } + registered[name] = distro +} diff --git a/internal/distro/distro_test.go b/internal/distro/distro_test.go new file mode 100644 index 000000000..851bed893 --- /dev/null +++ b/internal/distro/distro_test.go @@ -0,0 +1,66 @@ +package distro_test + +import ( + "encoding/json" + "io/ioutil" + "reflect" + "testing" + + "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/distro/fedora30" +) + +func TestDistro_Pipeline(t *testing.T) { + pipelinePath := "../../test/cases/" + fileInfos, err := ioutil.ReadDir(pipelinePath) + if err != nil { + t.Errorf("Could not read pipelines directory '%s': %v", pipelinePath, err) + } + for _, fileInfo := range fileInfos { + type compose struct { + Distro string `json:"distro"` + OutputFormat string `json:"output-format"` + Blueprint *blueprint.Blueprint `json:"blueprint"` + } + var tt struct { + Compose *compose `json:"compose"` + Pipeline *pipeline.Pipeline `json:"pipeline,omitempty"` + } + file, err := ioutil.ReadFile(pipelinePath + fileInfo.Name()) + if err != nil { + t.Errorf("Colud not read test-case '%s': %v", fileInfo.Name(), err) + } + err = json.Unmarshal([]byte(file), &tt) + if err != nil { + t.Errorf("Colud not parse test-case '%s': %v", fileInfo.Name(), err) + } + if tt.Compose == nil || tt.Compose.Blueprint == nil { + t.Logf("Skipping '%s'.", fileInfo.Name()) + continue + } + t.Run(tt.Compose.OutputFormat, func(t *testing.T) { + d := distro.New(tt.Compose.Distro) + if d == nil { + t.Errorf("unknown distro: %v", tt.Compose.Distro) + return + } + got, err := d.Pipeline(tt.Compose.Blueprint, tt.Compose.OutputFormat) + if (err != nil) != (tt.Pipeline == nil) { + t.Errorf("distro.Pipeline() error = %v", err) + return + } + if tt.Pipeline != nil { + if !reflect.DeepEqual(got, tt.Pipeline) { + // Without this the "difference" is just a list of pointers. + gotJson, _ := json.Marshal(got) + fileJson, _ := json.Marshal(tt.Pipeline) + t.Errorf("d.Pipeline() =\n%v,\nwant =\n%v", string(gotJson), string(fileJson)) + } + } + }) + } +} + diff --git a/internal/blueprint/ami_output.go b/internal/distro/fedora30/ami.go similarity index 73% rename from internal/blueprint/ami_output.go rename to internal/distro/fedora30/ami.go index bdacf1201..ccd7266eb 100644 --- a/internal/blueprint/ami_output.go +++ b/internal/distro/fedora30/ami.go @@ -1,10 +1,13 @@ -package blueprint +package fedora30 -import "github.com/osbuild/osbuild-composer/internal/pipeline" +import ( + "github.com/osbuild/osbuild-composer/internal/blueprint" + "github.com/osbuild/osbuild-composer/internal/pipeline" +) type amiOutput struct{} -func (t *amiOutput) translate(b *Blueprint) (*pipeline.Pipeline, error) { +func (t *amiOutput) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) { packages := [...]string{ "@Core", "chrony", @@ -30,7 +33,7 @@ func (t *amiOutput) translate(b *Blueprint) (*pipeline.Pipeline, error) { addF30QemuAssembler(p, "qcow2", t.getName()) if b.Customizations != nil { - err := b.Customizations.customizeAll(p) + err := customizeAll(p, b.Customizations) if err != nil { return nil, err } diff --git a/internal/distro/fedora30/customizations.go b/internal/distro/fedora30/customizations.go new file mode 100644 index 000000000..7d0277d55 --- /dev/null +++ b/internal/distro/fedora30/customizations.go @@ -0,0 +1,261 @@ +package fedora30 + +import ( + "github.com/osbuild/osbuild-composer/internal/blueprint" + "github.com/osbuild/osbuild-composer/internal/crypt" + "github.com/osbuild/osbuild-composer/internal/pipeline" + "log" + "strconv" + "strings" +) + +func customizeAll(p *pipeline.Pipeline, c *blueprint.Customizations) error { + customizeHostname(p, c) + customizeGroup(p, c) + if err := customizeUserAndSSHKey(p, c); err != nil { + return err + } + customizeTimezone(p, c) + customizeNTPServers(p, c) + customizeLanguages(p, c) + customizeKeyboard(p, c) + customizeFirewall(p, c) + customizeServices(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 customizeLanguages(p *pipeline.Pipeline, c *blueprint.Customizations) { + if c.Locale == nil || len(c.Locale.Languages) == 0 { + return + } + + // 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: c.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 customizeFirewall(p *pipeline.Pipeline, c *blueprint.Customizations) { + if c.Firewall == nil { + return + } + + var enabledServices, disabledServices []string + + if c.Firewall.Services != nil { + enabledServices = c.Firewall.Services.Enabled + disabledServices = c.Firewall.Services.Disabled + } + + p.AddStage( + pipeline.NewFirewallStage(&pipeline.FirewallStageOptions{ + Ports: c.Firewall.Ports, + EnabledServices: enabledServices, + DisabledServices: disabledServices, + }), + ) +} + +func customizeServices(p *pipeline.Pipeline, c *blueprint.Customizations) { + if c.Services == nil { + return + } + + p.AddStage( + pipeline.NewSystemdStage(&pipeline.SystemdStageOptions{ + EnabledServices: c.Services.Enabled, + DisabledServices: c.Services.Disabled, + }), + ) +} + +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/blueprint/disk_output.go b/internal/distro/fedora30/disk.go similarity index 67% rename from internal/blueprint/disk_output.go rename to internal/distro/fedora30/disk.go index 713a24b85..d3a87bc84 100644 --- a/internal/blueprint/disk_output.go +++ b/internal/distro/fedora30/disk.go @@ -1,10 +1,13 @@ -package blueprint +package fedora30 -import "github.com/osbuild/osbuild-composer/internal/pipeline" +import ( + "github.com/osbuild/osbuild-composer/internal/blueprint" + "github.com/osbuild/osbuild-composer/internal/pipeline" +) type diskOutput struct{} -func (t *diskOutput) translate(b *Blueprint) (*pipeline.Pipeline, error) { +func (t *diskOutput) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) { packages := [...]string{ "@core", "chrony", @@ -20,13 +23,13 @@ func (t *diskOutput) translate(b *Blueprint) (*pipeline.Pipeline, error) { p := getCustomF30PackageSet(packages[:], excludedPackages[:], b) addF30LocaleStage(p) addF30FSTabStage(p) - addF30GRUB2Stage(p, b.getKernelCustomization()) + addF30GRUB2Stage(p, b.GetKernelCustomization()) addF30FixBlsStage(p) addF30SELinuxStage(p) addF30QemuAssembler(p, "raw", t.getName()) if b.Customizations != nil { - err := b.Customizations.customizeAll(p) + err := customizeAll(p, b.Customizations) if err != nil { return nil, err } diff --git a/internal/blueprint/ext4_output.go b/internal/distro/fedora30/ext4.go similarity index 66% rename from internal/blueprint/ext4_output.go rename to internal/distro/fedora30/ext4.go index 2f27cdcb9..b9e728d14 100644 --- a/internal/blueprint/ext4_output.go +++ b/internal/distro/fedora30/ext4.go @@ -1,10 +1,13 @@ -package blueprint +package fedora30 -import "github.com/osbuild/osbuild-composer/internal/pipeline" +import ( + "github.com/osbuild/osbuild-composer/internal/blueprint" + "github.com/osbuild/osbuild-composer/internal/pipeline" +) type ext4Output struct{} -func (t *ext4Output) translate(b *Blueprint) (*pipeline.Pipeline, error) { +func (t *ext4Output) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) { packages := [...]string{ "policycoreutils", "selinux-policy-targeted", @@ -18,13 +21,13 @@ func (t *ext4Output) translate(b *Blueprint) (*pipeline.Pipeline, error) { } p := getCustomF30PackageSet(packages[:], excludedPackages[:], b) addF30LocaleStage(p) - addF30GRUB2Stage(p, b.getKernelCustomization()) + addF30GRUB2Stage(p, b.GetKernelCustomization()) addF30FixBlsStage(p) addF30SELinuxStage(p) addF30RawFSAssembler(p, t.getName()) if b.Customizations != nil { - err := b.Customizations.customizeAll(p) + err := customizeAll(p, b.Customizations) if err != nil { return nil, err } diff --git a/internal/blueprint/f30_helpers.go b/internal/distro/fedora30/helpers.go similarity index 97% rename from internal/blueprint/f30_helpers.go rename to internal/distro/fedora30/helpers.go index ae73e9d9b..2d9810754 100644 --- a/internal/blueprint/f30_helpers.go +++ b/internal/distro/fedora30/helpers.go @@ -1,6 +1,7 @@ -package blueprint +package fedora30 import ( + "github.com/osbuild/osbuild-composer/internal/blueprint" "github.com/osbuild/osbuild-composer/internal/pipeline" "github.com/google/uuid" @@ -89,7 +90,7 @@ func getF30Pipeline() *pipeline.Pipeline { return p } -func getCustomF30PackageSet(packages []string, excludedPackages []string, blueprint *Blueprint) *pipeline.Pipeline { +func getCustomF30PackageSet(packages []string, excludedPackages []string, blueprint *blueprint.Blueprint) *pipeline.Pipeline { p := &pipeline.Pipeline{ BuildPipeline: getF30BuildPipeline(), } @@ -122,7 +123,7 @@ func getCustomF30PackageSet(packages []string, excludedPackages []string, bluepr return p } -func addF30GRUB2Stage(p *pipeline.Pipeline, kernelCustomization *KernelCustomization) { +func addF30GRUB2Stage(p *pipeline.Pipeline, kernelCustomization *blueprint.KernelCustomization) { id, err := uuid.Parse("76a22bf4-f153-4541-b6c7-0332c0dfaeac") if err != nil { panic("invalid UUID") diff --git a/internal/blueprint/liveiso_output.go b/internal/distro/fedora30/liveiso.go similarity index 57% rename from internal/blueprint/liveiso_output.go rename to internal/distro/fedora30/liveiso.go index b3c241d74..b4ac01c4b 100644 --- a/internal/blueprint/liveiso_output.go +++ b/internal/distro/fedora30/liveiso.go @@ -1,17 +1,20 @@ -package blueprint +package fedora30 -import "github.com/osbuild/osbuild-composer/internal/pipeline" +import ( + "github.com/osbuild/osbuild-composer/internal/blueprint" + "github.com/osbuild/osbuild-composer/internal/pipeline" +) type liveIsoOutput struct{} -func (t *liveIsoOutput) translate(b *Blueprint) (*pipeline.Pipeline, error) { +func (t *liveIsoOutput) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) { // TODO! p := getF30Pipeline() addF30SELinuxStage(p) addF30QemuAssembler(p, "raw", t.getName()) if b.Customizations != nil { - err := b.Customizations.customizeAll(p) + err := customizeAll(p, b.Customizations) if err != nil { return nil, err } diff --git a/internal/blueprint/openstack_output.go b/internal/distro/fedora30/openstack.go similarity index 69% rename from internal/blueprint/openstack_output.go rename to internal/distro/fedora30/openstack.go index 816a5e7be..876503817 100644 --- a/internal/blueprint/openstack_output.go +++ b/internal/distro/fedora30/openstack.go @@ -1,10 +1,13 @@ -package blueprint +package fedora30 -import "github.com/osbuild/osbuild-composer/internal/pipeline" +import ( + "github.com/osbuild/osbuild-composer/internal/blueprint" + "github.com/osbuild/osbuild-composer/internal/pipeline" +) type openstackOutput struct{} -func (t *openstackOutput) translate(b *Blueprint) (*pipeline.Pipeline, error) { +func (t *openstackOutput) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) { packages := [...]string{ "@Core", "chrony", @@ -24,13 +27,13 @@ func (t *openstackOutput) translate(b *Blueprint) (*pipeline.Pipeline, error) { p := getCustomF30PackageSet(packages[:], excludedPackages[:], b) addF30LocaleStage(p) addF30FSTabStage(p) - addF30GRUB2Stage(p, b.getKernelCustomization()) + addF30GRUB2Stage(p, b.GetKernelCustomization()) addF30FixBlsStage(p) addF30SELinuxStage(p) addF30QemuAssembler(p, "qcow2", t.getName()) if b.Customizations != nil { - err := b.Customizations.customizeAll(p) + err := customizeAll(p, b.Customizations) if err != nil { return nil, err } diff --git a/internal/distro/fedora30/os.go b/internal/distro/fedora30/os.go new file mode 100644 index 000000000..e834035b3 --- /dev/null +++ b/internal/distro/fedora30/os.go @@ -0,0 +1,59 @@ +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" +) + +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{}, + "live-iso": &liveIsoOutput{}, + "partitioned-disk": &diskOutput{}, + "qcow2": &qcow2Output{}, + "openstack": &openstackOutput{}, + "tar": &tarOutput{}, + "vhd": &vhdOutput{}, + "vmdk": &vmdkOutput{}, + }, + }) +} + +// 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/os_test.go b/internal/distro/fedora30/os_test.go new file mode 100644 index 000000000..e19576dc6 --- /dev/null +++ b/internal/distro/fedora30/os_test.go @@ -0,0 +1,119 @@ +package fedora30_test + +import ( + "reflect" + "testing" + + "github.com/osbuild/osbuild-composer/internal/distro" + _ "github.com/osbuild/osbuild-composer/internal/distro/fedora30" +) + +func TestListOutputFormats(t *testing.T) { + want := []string{ + "ami", + "ext4-filesystem", + "live-iso", + "openstack", + "partitioned-disk", + "qcow2", + "tar", + "vhd", + "vmdk", + } + + f30 := distro.New("fedora-30") + if got := f30.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.ami", + want1: "application/x-qemu-disk", + }, + { + name: "ext4", + args: args{"ext4-filesystem"}, + want: "filesystem.img", + want1: "application/octet-stream", + }, + { + name: "live-iso", + args: args{"live-iso"}, + want: "image.iso", + want1: "application/x-iso9660-image", + }, + { + name: "openstack", + args: args{"openstack"}, + want: "image.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: "image.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: "image.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) { + f30 := distro.New("fedora-30") + got, got1, err := f30.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/blueprint/qcow2_output.go b/internal/distro/fedora30/qcow2.go similarity index 70% rename from internal/blueprint/qcow2_output.go rename to internal/distro/fedora30/qcow2.go index aeef47391..0aacf8409 100644 --- a/internal/blueprint/qcow2_output.go +++ b/internal/distro/fedora30/qcow2.go @@ -1,10 +1,13 @@ -package blueprint +package fedora30 -import "github.com/osbuild/osbuild-composer/internal/pipeline" +import ( + "github.com/osbuild/osbuild-composer/internal/blueprint" + "github.com/osbuild/osbuild-composer/internal/pipeline" +) type qcow2Output struct{} -func (t *qcow2Output) translate(b *Blueprint) (*pipeline.Pipeline, error) { +func (t *qcow2Output) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) { packages := [...]string{ "kernel-core", "@Fedora Cloud Server", @@ -25,13 +28,13 @@ func (t *qcow2Output) translate(b *Blueprint) (*pipeline.Pipeline, error) { p := getCustomF30PackageSet(packages[:], excludedPackages[:], b) addF30LocaleStage(p) addF30FSTabStage(p) - addF30GRUB2Stage(p, b.getKernelCustomization()) + addF30GRUB2Stage(p, b.GetKernelCustomization()) addF30FixBlsStage(p) addF30SELinuxStage(p) addF30QemuAssembler(p, "qcow2", t.getName()) if b.Customizations != nil { - err := b.Customizations.customizeAll(p) + err := customizeAll(p, b.Customizations) if err != nil { return nil, err } diff --git a/internal/blueprint/tar_output.go b/internal/distro/fedora30/tar.go similarity index 66% rename from internal/blueprint/tar_output.go rename to internal/distro/fedora30/tar.go index adab45be8..145d278fb 100644 --- a/internal/blueprint/tar_output.go +++ b/internal/distro/fedora30/tar.go @@ -1,10 +1,13 @@ -package blueprint +package fedora30 -import "github.com/osbuild/osbuild-composer/internal/pipeline" +import ( + "github.com/osbuild/osbuild-composer/internal/blueprint" + "github.com/osbuild/osbuild-composer/internal/pipeline" +) type tarOutput struct{} -func (t *tarOutput) translate(b *Blueprint) (*pipeline.Pipeline, error) { +func (t *tarOutput) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) { packages := [...]string{ "policycoreutils", "selinux-policy-targeted", @@ -18,13 +21,13 @@ func (t *tarOutput) translate(b *Blueprint) (*pipeline.Pipeline, error) { } p := getCustomF30PackageSet(packages[:], excludedPackages[:], b) addF30LocaleStage(p) - addF30GRUB2Stage(p, b.getKernelCustomization()) + addF30GRUB2Stage(p, b.GetKernelCustomization()) addF30FixBlsStage(p) addF30SELinuxStage(p) addF30TarAssembler(p, t.getName(), "xz") if b.Customizations != nil { - err := b.Customizations.customizeAll(p) + err := customizeAll(p, b.Customizations) if err != nil { return nil, err } diff --git a/internal/blueprint/vhd_output.go b/internal/distro/fedora30/vhd.go similarity index 68% rename from internal/blueprint/vhd_output.go rename to internal/distro/fedora30/vhd.go index c033914cc..8837846a4 100644 --- a/internal/blueprint/vhd_output.go +++ b/internal/distro/fedora30/vhd.go @@ -1,10 +1,13 @@ -package blueprint +package fedora30 -import "github.com/osbuild/osbuild-composer/internal/pipeline" +import ( + "github.com/osbuild/osbuild-composer/internal/blueprint" + "github.com/osbuild/osbuild-composer/internal/pipeline" +) type vhdOutput struct{} -func (t *vhdOutput) translate(b *Blueprint) (*pipeline.Pipeline, error) { +func (t *vhdOutput) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) { packages := [...]string{ "@Core", "chrony", @@ -23,13 +26,13 @@ func (t *vhdOutput) translate(b *Blueprint) (*pipeline.Pipeline, error) { p := getCustomF30PackageSet(packages[:], excludedPackages[:], b) addF30LocaleStage(p) addF30FSTabStage(p) - addF30GRUB2Stage(p, b.getKernelCustomization()) + addF30GRUB2Stage(p, b.GetKernelCustomization()) addF30FixBlsStage(p) addF30SELinuxStage(p) addF30QemuAssembler(p, "qcow2", t.getName()) if b.Customizations != nil { - err := b.Customizations.customizeAll(p) + err := customizeAll(p, b.Customizations) if err != nil { return nil, err } diff --git a/internal/blueprint/vmdk_output.go b/internal/distro/fedora30/vmdk.go similarity index 67% rename from internal/blueprint/vmdk_output.go rename to internal/distro/fedora30/vmdk.go index b0e98535a..dec148da1 100644 --- a/internal/blueprint/vmdk_output.go +++ b/internal/distro/fedora30/vmdk.go @@ -1,10 +1,13 @@ -package blueprint +package fedora30 -import "github.com/osbuild/osbuild-composer/internal/pipeline" +import ( + "github.com/osbuild/osbuild-composer/internal/blueprint" + "github.com/osbuild/osbuild-composer/internal/pipeline" +) type vmdkOutput struct{} -func (t *vmdkOutput) translate(b *Blueprint) (*pipeline.Pipeline, error) { +func (t *vmdkOutput) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) { packages := [...]string{ "@core", "chrony", @@ -21,13 +24,13 @@ func (t *vmdkOutput) translate(b *Blueprint) (*pipeline.Pipeline, error) { p := getCustomF30PackageSet(packages[:], excludedPackages[:], b) addF30LocaleStage(p) addF30FSTabStage(p) - addF30GRUB2Stage(p, b.getKernelCustomization()) + addF30GRUB2Stage(p, b.GetKernelCustomization()) addF30FixBlsStage(p) addF30SELinuxStage(p) addF30QemuAssembler(p, "vmdk", t.getName()) if b.Customizations != nil { - err := b.Customizations.customizeAll(p) + err := customizeAll(p, b.Customizations) if err != nil { return nil, err } diff --git a/internal/store/store.go b/internal/store/store.go index bb8972ffa..102dcd301 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -16,6 +16,9 @@ import ( "github.com/osbuild/osbuild-composer/internal/pipeline" "github.com/osbuild/osbuild-composer/internal/target" + "github.com/osbuild/osbuild-composer/internal/distro" + _ "github.com/osbuild/osbuild-composer/internal/distro/fedora30" + "github.com/google/uuid" ) @@ -393,7 +396,8 @@ func (s *Store) PushCompose(composeID uuid.UUID, bp *blueprint.Blueprint, compos targets := []*target.Target{ target.NewLocalTarget(target.NewLocalTargetOptions("/var/lib/osbuild-composer/outputs/" + composeID.String())), } - pipeline, err := bp.ToPipeline(composeType) + f30 := distro.New("fedora-30") + pipeline, err := f30.Pipeline(bp, composeType) if err != nil { return err } @@ -471,7 +475,8 @@ func (s *Store) GetImage(composeID uuid.UUID) (*Image, error) { if compose.QueueStatus != "FINISHED" { return nil, &InvalidRequestError{"compose was not finished"} } - name, mime, err := blueprint.FilenameFromType(compose.OutputType) + f30 := distro.New("fedora-30") + name, mime, err := f30.FilenameFromType(compose.OutputType) if err != nil { panic("invalid output type") } diff --git a/internal/weldr/api.go b/internal/weldr/api.go index 3c6101f2e..fb451547a 100644 --- a/internal/weldr/api.go +++ b/internal/weldr/api.go @@ -16,6 +16,9 @@ import ( "github.com/osbuild/osbuild-composer/internal/blueprint" "github.com/osbuild/osbuild-composer/internal/rpmmd" "github.com/osbuild/osbuild-composer/internal/store" + + "github.com/osbuild/osbuild-composer/internal/distro" + _ "github.com/osbuild/osbuild-composer/internal/distro/fedora30" ) type API struct { @@ -847,7 +850,8 @@ func (api *API) composeTypesHandler(writer http.ResponseWriter, request *http.Re Types []composeType `json:"types"` } - for _, format := range blueprint.ListOutputFormats() { + f30 := distro.New("fedora-30") + for _, format := range f30.ListOutputFormats() { reply.Types = append(reply.Types, composeType{format, true}) } diff --git a/test/cases/ami_empty_blueprint.json b/test/cases/ami_empty_blueprint.json index 47ac638cf..1f6c46154 100644 --- a/test/cases/ami_empty_blueprint.json +++ b/test/cases/ami_empty_blueprint.json @@ -1,5 +1,6 @@ { "compose": { + "distro": "fedora-30", "filename": "image.ami", "output-format": "ami", "blueprint": {} diff --git a/test/cases/disk_empty_blueprint.json b/test/cases/disk_empty_blueprint.json index 4f8702ed4..9cc82c68e 100644 --- a/test/cases/disk_empty_blueprint.json +++ b/test/cases/disk_empty_blueprint.json @@ -1,5 +1,6 @@ { "compose": { + "distro": "fedora-30", "output-format": "partitioned-disk", "filename": "disk.img", "blueprint": {} diff --git a/test/cases/ext4_empty_blueprint.json b/test/cases/ext4_empty_blueprint.json index ff0cf858e..a4b14df13 100644 --- a/test/cases/ext4_empty_blueprint.json +++ b/test/cases/ext4_empty_blueprint.json @@ -1,5 +1,6 @@ { "compose": { + "distro": "fedora-30", "output-format": "ext4-filesystem", "filename": "filesystem.img", "blueprint": {} diff --git a/test/cases/openstack_empty_blueprint.json b/test/cases/openstack_empty_blueprint.json index c17a10549..2aadc8078 100644 --- a/test/cases/openstack_empty_blueprint.json +++ b/test/cases/openstack_empty_blueprint.json @@ -1,5 +1,6 @@ { "compose": { + "distro": "fedora-30", "output-format": "openstack", "filename": "image.qcow2", "blueprint": {} diff --git a/test/cases/qcow2_empty_blueprint.json b/test/cases/qcow2_empty_blueprint.json index 96a52f5c9..fc5b518f5 100644 --- a/test/cases/qcow2_empty_blueprint.json +++ b/test/cases/qcow2_empty_blueprint.json @@ -1,5 +1,6 @@ { "compose": { + "distro": "fedora-30", "output-format": "qcow2", "filename": "image.qcow2", "blueprint": {} diff --git a/test/cases/vhd_empty_blueprint.json b/test/cases/vhd_empty_blueprint.json index 1bcfb436e..f068221b8 100644 --- a/test/cases/vhd_empty_blueprint.json +++ b/test/cases/vhd_empty_blueprint.json @@ -1,5 +1,6 @@ { "compose": { + "distro": "fedora-30", "output-format": "vhd", "filename": "image.vhd", "blueprint": {} diff --git a/test/cases/vmdk_empty_blueprint.json b/test/cases/vmdk_empty_blueprint.json index 894ab1451..84e923924 100644 --- a/test/cases/vmdk_empty_blueprint.json +++ b/test/cases/vmdk_empty_blueprint.json @@ -1,5 +1,6 @@ { "compose": { + "distro": "fedora-30", "output-format": "vmdk", "filename": "disk.vmdk", "blueprint": {}