Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for arbitrary kickstart file injection into ISOs (HMS-3879) #631

Merged
merged 19 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
92a6bab
customizations: new subpackage: kickstart
achilleas-k Apr 24, 2024
253e80c
image: drop osname from the live installer
achilleas-k May 6, 2024
4b6b255
manifest: use kickstart.Options on AnacondaInstallerISOTree
achilleas-k Apr 23, 2024
2690871
manifest: use kickstart.Options on AnacondaInstaller
achilleas-k Apr 24, 2024
2926433
blueprint: support user kickstart injection
achilleas-k Apr 24, 2024
e4b362f
distro: wire up the user kickstart customization
achilleas-k Apr 24, 2024
c4a4df0
manifest: calculate raw kickstart test file content hash in test
achilleas-k Apr 24, 2024
2063ca6
manifest: check len(SudoNopasswd) in test instead of true/false
achilleas-k Apr 24, 2024
9c4407d
test: add a config that uses a user kickstart file
achilleas-k Apr 24, 2024
0f9092d
blueprint: custom kickstart incompatible with installer customizations
achilleas-k May 6, 2024
755d379
distro: ostree users and groups incompatible with kickstart contents
achilleas-k May 6, 2024
1df77c8
osbuild: add note about future include plans in kickstart stage
achilleas-k May 6, 2024
4efb219
manifest: restrict kickstart options for bootc container installer
achilleas-k May 6, 2024
23211db
manifest: restrict kickstart options for anaconda installers
achilleas-k May 6, 2024
35d623e
manifest: update kickstart tests
achilleas-k May 6, 2024
c10a539
test: update config to match new rules
achilleas-k May 6, 2024
4d64e67
customizations: function for kickstart customization initialisation
achilleas-k May 13, 2024
3beb9c9
distro: flip the installer customization check order
achilleas-k May 13, 2024
df3bdb4
customizations/kickstart: unify option validation
achilleas-k May 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions pkg/blueprint/customizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,9 +387,21 @@ func (c *Customizations) GetContainerStorage() *ContainerStorageCustomization {
return c.ContainersStorage
}

func (c *Customizations) GetInstaller() *InstallerCustomization {
func (c *Customizations) GetInstaller() (*InstallerCustomization, error) {
if c == nil || c.Installer == nil {
return nil
return nil, nil
}
return c.Installer

// Validate conflicting customizations: Installer options aren't supported
// when the user adds their own kickstart content
if c.Installer.Kickstart != nil && len(c.Installer.Kickstart.Contents) > 0 {
if c.Installer.Unattended {
return nil, fmt.Errorf("installer.unattended is not allowed when adding custom kickstart contents")
}
if len(c.Installer.SudoNopasswd) > 0 {
return nil, fmt.Errorf("installer.sudo-nopasswd is not allowed when adding custom kickstart contents")
}
}

return c.Installer, nil
}
9 changes: 7 additions & 2 deletions pkg/blueprint/installer_customizations.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package blueprint

type InstallerCustomization struct {
Unattended bool `json:"unattended,omitempty" toml:"unattended,omitempty"`
SudoNopasswd []string `json:"sudo-nopasswd,omitempty" toml:"sudo-nopasswd,omitempty"`
Unattended bool `json:"unattended,omitempty" toml:"unattended,omitempty"`
SudoNopasswd []string `json:"sudo-nopasswd,omitempty" toml:"sudo-nopasswd,omitempty"`
Kickstart *Kickstart `json:"kickstart,omitempty" toml:"kickstart,omitempty"`
}

type Kickstart struct {
Contents string `json:"contents" toml:"contents"`
}
93 changes: 93 additions & 0 deletions pkg/customizations/kickstart/kickstart.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package kickstart

import (
"fmt"

"github.com/osbuild/images/pkg/blueprint"
"github.com/osbuild/images/pkg/customizations/users"
)

type File struct {
Contents string
}

type OSTree struct {
OSName string
Remote string
}

type Options struct {
// Path where the kickstart file will be created
Path string

// Add kickstart options to make the installation fully unattended
Unattended bool

// Create a sudoers drop-in file for each user or group to enable the
// NOPASSWD option
SudoNopasswd []string

// Kernel options that will be appended to the installed system
// (not the iso)
KernelOptionsAppend []string

// Enable networking on on boot in the installed system
NetworkOnBoot bool

Language *string
Keyboard *string
Timezone *string

// Users to create during installation
Users []users.User

// Groups to create during installation
Groups []users.Group

// ostree-related kickstart options
OSTree *OSTree

// User-defined kickstart files that will be added to the ISO
UserFile *File
}

func New(customizations *blueprint.Customizations) (*Options, error) {
achilleas-k marked this conversation as resolved.
Show resolved Hide resolved
options := &Options{
Users: users.UsersFromBP(customizations.GetUsers()),
Groups: users.GroupsFromBP(customizations.GetGroups()),
}

instCust, err := customizations.GetInstaller()
if err != nil {
return nil, err
}
if instCust != nil {
options.SudoNopasswd = instCust.SudoNopasswd
options.Unattended = instCust.Unattended
if instCust.Kickstart != nil {
options.UserFile = &File{Contents: instCust.Kickstart.Contents}
}
}

if err := options.Validate(); err != nil {
return nil, err
}
return options, nil
}

func (options Options) Validate() error {
achilleas-k marked this conversation as resolved.
Show resolved Hide resolved
if options.UserFile != nil {
// users, groups, and other kickstart options are not allowed when
// users add their own kickstarts
if options.Unattended {
return fmt.Errorf("kickstart unattended options are not compatible with user-supplied kickstart content")
}
if len(options.SudoNopasswd) > 0 {
return fmt.Errorf("kickstart sudo nopasswd drop-in file creation is not compatible with user-supplied kickstart content")
}
if len(options.Users)+len(options.Groups) > 0 {
return fmt.Errorf("kickstart users and/or groups are not compatible with user-supplied kickstart content")
}
}
return nil
}
41 changes: 20 additions & 21 deletions pkg/distro/fedora/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/osbuild/images/pkg/customizations/fdo"
"github.com/osbuild/images/pkg/customizations/fsnode"
"github.com/osbuild/images/pkg/customizations/ignition"
"github.com/osbuild/images/pkg/customizations/kickstart"
"github.com/osbuild/images/pkg/customizations/oscap"
"github.com/osbuild/images/pkg/customizations/users"
"github.com/osbuild/images/pkg/distro"
Expand Down Expand Up @@ -415,7 +416,6 @@ func liveInstallerImage(workload workload.Workload,
d := t.arch.distro

img.Product = d.product
img.OSName = "fedora"
img.Variant = "Workstation"
img.OSVersion = d.osVersion
img.Release = fmt.Sprintf("%s %s", d.product, d.osVersion)
Expand Down Expand Up @@ -444,12 +444,16 @@ func imageInstallerImage(workload workload.Workload,

img := image.NewAnacondaTarInstaller()

if instCust := customizations.GetInstaller(); instCust != nil {
img.NoPasswd = instCust.SudoNopasswd
img.UnattendedKickstart = instCust.Unattended
var err error
img.Kickstart, err = kickstart.New(customizations)
if err != nil {
return nil, err
}
img.Kickstart.Language = &img.OSCustomizations.Language
img.Kickstart.Keyboard = img.OSCustomizations.Keyboard
img.Kickstart.Timezone = &img.OSCustomizations.Timezone

if img.UnattendedKickstart {
if img.Kickstart.Unattended {
// NOTE: this is not supported right now because the
// image-installer on Fedora isn't working when unattended.
// These options are probably necessary but could change.
Expand All @@ -462,15 +466,12 @@ func imageInstallerImage(workload workload.Workload,
img.Platform = t.platform
img.Workload = workload

var err error
img.OSCustomizations, err = osCustomizations(t, packageSets[osPkgsKey], containers, bp.Customizations)
if err != nil {
return nil, err
}

img.ExtraBasePackages = packageSets[installerPkgsKey]
img.Users = users.UsersFromBP(customizations.GetUsers())
img.Groups = users.GroupsFromBP(customizations.GetGroups())

img.SquashfsCompression = "lz4"

Expand All @@ -481,8 +482,6 @@ func imageInstallerImage(workload workload.Workload,
// We don't know the variant that goes into the OS pipeline that gets installed
img.Variant = "Unknown"

img.OSName = "fedora"

img.OSVersion = d.osVersion
img.Release = fmt.Sprintf("%s %s", d.product, d.osVersion)

Expand Down Expand Up @@ -648,31 +647,31 @@ func iotInstallerImage(workload workload.Workload,
img.FIPS = customizations.GetFIPS()
img.Platform = t.platform
img.ExtraBasePackages = packageSets[installerPkgsKey]
img.Users = users.UsersFromBP(customizations.GetUsers())
img.Groups = users.GroupsFromBP(customizations.GetGroups())

img.Language, img.Keyboard = customizations.GetPrimaryLocale()
img.Kickstart, err = kickstart.New(customizations)
if err != nil {
return nil, err
}
img.Kickstart.OSTree = &kickstart.OSTree{
OSName: "fedora-iot",
Remote: "fedora-iot",
}
img.Kickstart.Path = osbuild.KickstartPathOSBuild
img.Kickstart.Language, img.Kickstart.Keyboard = customizations.GetPrimaryLocale()
// ignore ntp servers - we don't currently support setting these in the
// kickstart though kickstart does support setting them
img.Timezone, _ = customizations.GetTimezoneSettings()
img.Kickstart.Timezone, _ = customizations.GetTimezoneSettings()

img.AdditionalAnacondaModules = []string{
"org.fedoraproject.Anaconda.Modules.Timezone",
"org.fedoraproject.Anaconda.Modules.Localization",
"org.fedoraproject.Anaconda.Modules.Users",
}

if instCust := customizations.GetInstaller(); instCust != nil {
img.NoPasswd = instCust.SudoNopasswd
img.UnattendedKickstart = instCust.Unattended
}

img.SquashfsCompression = "lz4"

img.Product = d.product
img.Variant = "IoT"
img.OSName = "fedora-iot"
img.Remote = "fedora-iot"
img.OSVersion = d.osVersion
img.Release = fmt.Sprintf("%s %s", d.product, d.osVersion)
img.Preview = common.VersionGreaterThanOrEqual(img.OSVersion, VERSION_BRANCHED)
Expand Down
19 changes: 17 additions & 2 deletions pkg/distro/fedora/imagetype.go
Original file line number Diff line number Diff line change
Expand Up @@ -418,10 +418,25 @@ func (t *imageType) checkOptions(bp *blueprint.Blueprint, options distro.ImageOp
return []string{w}, nil
}

if customizations.GetInstaller() != nil {
instCust, err := customizations.GetInstaller()
if err != nil {
return nil, err
}
if instCust != nil {
// only supported by the Anaconda installer
if slices.Index([]string{"iot-installer"}, t.name) == -1 {
return nil, fmt.Errorf("installer customizations are not supported for %q", t.name)
return nil, fmt.Errorf("installer customizations are not supported for %q", t.Name())
}

// NOTE: the image type check is redundant with the check above, but
achilleas-k marked this conversation as resolved.
Show resolved Hide resolved
// let's keep it explicit in case one of the two changes.
// The kickstart contents is incompatible with the users and groups
// customization only for the iot-installer.
if t.Name() == "iot-installer" &&
instCust.Kickstart != nil &&
achilleas-k marked this conversation as resolved.
Show resolved Hide resolved
len(instCust.Kickstart.Contents) > 0 &&
(customizations.GetUsers() != nil || customizations.GetGroups() != nil) {
return nil, fmt.Errorf("iot-installer installer.kickstart.contents are not supported in combination with users or groups")
}
}

Expand Down
38 changes: 20 additions & 18 deletions pkg/distro/rhel/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/osbuild/images/pkg/customizations/fdo"
"github.com/osbuild/images/pkg/customizations/fsnode"
"github.com/osbuild/images/pkg/customizations/ignition"
"github.com/osbuild/images/pkg/customizations/kickstart"
"github.com/osbuild/images/pkg/customizations/oscap"
"github.com/osbuild/images/pkg/customizations/users"
"github.com/osbuild/images/pkg/distro"
Expand Down Expand Up @@ -487,18 +488,19 @@ func EdgeInstallerImage(workload workload.Workload,

img.Platform = t.platform
img.ExtraBasePackages = packageSets[InstallerPkgsKey]
img.Users = users.UsersFromBP(customizations.GetUsers())
img.Groups = users.GroupsFromBP(customizations.GetGroups())

img.Language, img.Keyboard = customizations.GetPrimaryLocale()
img.Kickstart, err = kickstart.New(customizations)
if err != nil {
return nil, err
}
img.Kickstart.OSTree = &kickstart.OSTree{
OSName: "rhel-edge",
}
img.Kickstart.Path = osbuild.KickstartPathOSBuild
img.Kickstart.Language, img.Kickstart.Keyboard = customizations.GetPrimaryLocale()
// ignore ntp servers - we don't currently support setting these in the
// kickstart though kickstart does support setting them
img.Timezone, _ = customizations.GetTimezoneSettings()

if instCust := customizations.GetInstaller(); instCust != nil {
img.NoPasswd = instCust.SudoNopasswd
img.UnattendedKickstart = instCust.Unattended
}
img.Kickstart.Timezone, _ = customizations.GetTimezoneSettings()

img.SquashfsCompression = "xz"

Expand All @@ -512,7 +514,7 @@ func EdgeInstallerImage(workload workload.Workload,
img.AdditionalDrivers = installerConfig.AdditionalDrivers
}

if len(img.Users)+len(img.Groups) > 0 {
if len(img.Kickstart.Users)+len(img.Kickstart.Groups) > 0 {
// only enable the users module if needed
img.AdditionalAnacondaModules = []string{"org.fedoraproject.Anaconda.Modules.Users"}
}
Expand All @@ -524,7 +526,6 @@ func EdgeInstallerImage(workload workload.Workload,

img.Product = t.Arch().Distro().Product()
img.Variant = "edge"
img.OSName = "rhel-edge"
img.OSVersion = t.Arch().Distro().OsVersion()
img.Release = fmt.Sprintf("%s %s", t.Arch().Distro().Product(), t.Arch().Distro().OsVersion())
img.FIPS = customizations.GetFIPS()
Expand Down Expand Up @@ -676,8 +677,14 @@ func ImageInstallerImage(workload workload.Workload,
}

img.ExtraBasePackages = packageSets[InstallerPkgsKey]
img.Users = users.UsersFromBP(customizations.GetUsers())
img.Groups = users.GroupsFromBP(customizations.GetGroups())

img.Kickstart, err = kickstart.New(customizations)
if err != nil {
return nil, err
}
img.Kickstart.Language = &img.OSCustomizations.Language
img.Kickstart.Keyboard = img.OSCustomizations.Keyboard
img.Kickstart.Timezone = &img.OSCustomizations.Timezone

installerConfig, err := t.getDefaultInstallerConfig()
if err != nil {
Expand All @@ -691,11 +698,6 @@ func ImageInstallerImage(workload workload.Workload,

img.AdditionalAnacondaModules = []string{"org.fedoraproject.Anaconda.Modules.Users"}

if instCust := customizations.GetInstaller(); instCust != nil {
img.NoPasswd = instCust.SudoNopasswd
img.UnattendedKickstart = instCust.Unattended
}

img.SquashfsCompression = "xz"

// put the kickstart file in the root of the iso
Expand Down
6 changes: 5 additions & 1 deletion pkg/distro/rhel/rhel10/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@ func checkOptions(t *rhel.ImageType, bp *blueprint.Blueprint, options distro.Ima
warnings = append(warnings, w)
}

if customizations.GetInstaller() != nil {
instCust, err := customizations.GetInstaller()
if err != nil {
return warnings, err
}
if instCust != nil {
// only supported by the Anaconda installer
if slices.Index([]string{"image-installer", "edge-installer", "live-installer"}, t.Name()) == -1 {
return warnings, fmt.Errorf("installer customizations are not supported for %q", t.Name())
Expand Down
13 changes: 12 additions & 1 deletion pkg/distro/rhel/rhel8/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,11 +175,22 @@ func checkOptions(t *rhel.ImageType, bp *blueprint.Blueprint, options distro.Ima
warnings = append(warnings, w)
}

if customizations.GetInstaller() != nil {
instCust, err := customizations.GetInstaller()
if err != nil {
return warnings, err
}
if instCust != nil {
// only supported by the Anaconda installer
if slices.Index([]string{"image-installer", "edge-installer", "live-installer"}, t.Name()) == -1 {
return warnings, fmt.Errorf("installer customizations are not supported for %q", t.Name())
}

if t.Name() == "edge-installer" &&
instCust.Kickstart != nil &&
len(instCust.Kickstart.Contents) > 0 &&
(customizations.GetUsers() != nil || customizations.GetGroups() != nil) {
return warnings, fmt.Errorf("edge-installer installer.kickstart.contents are not supported in combination with users or groups")
}
}

return warnings, nil
Expand Down
Loading
Loading