blueprint: move pipeline generation into its own package

Introduce the `distro` package, which contains an interface for OS
implementations. Its main purpose is to convert a blueprint to a
distro-specific pipeline.

Also introduce the `distro/fedora30` package. It is the first
implementation of the distro interface. Most of its code has been copied
with minimal modifications from the blueprint package.

The `blueprint` package is now back to serving a single purpose:
representing a weldr blueprint. It does not depend on the `pipeline`
package anymore.

Change osbuild-composer and osbuild-pipeline to use the new API,
hard-coding "fedora-30". This looks a bit weird now, but is the same
behavior as before.

All test cases now also take an "distro" key in the "compose" object.
This commit is contained in:
Lars Karlitski 2019-11-06 01:01:39 +00:00 committed by Tom Gundersen
parent 3ae39e54c5
commit b33ed9e5d2
28 changed files with 661 additions and 541 deletions

View file

@ -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
}