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

ignition: add support for ignition config file #209

Merged
merged 3 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
52 changes: 52 additions & 0 deletions cmd/vfkit/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ package main

import (
"fmt"
"io"
"net"
"net/http"
"os"
"os/signal"
"runtime"
Expand Down Expand Up @@ -86,6 +89,10 @@ func newVMConfiguration(opts *cmdline.Options) (*config.VirtualMachine, error) {
return nil, err
}

if err := vmConfig.AddIgnitionFileFromCmdLine(opts.IgnitionPath); err != nil {
return nil, fmt.Errorf("failed to add ignition file: %w", err)
}

return vmConfig, nil
}

Expand Down Expand Up @@ -137,6 +144,21 @@ func runVFKit(vmConfig *config.VirtualMachine, opts *cmdline.Options) error {
}

func runVirtualMachine(vmConfig *config.VirtualMachine, vm *vf.VirtualMachine) error {
if vm.Config().Ignition != nil {
go func() {
file, err := os.Open(vmConfig.Ignition.ConfigPath)
if err != nil {
log.Error(err)
}
defer file.Close()
reader := file
if err := startIgnitionProvisionerServer(reader, vmConfig.Ignition.SocketPath); err != nil {
log.Error(err)
}
log.Debug("ignition vsock server exited")
}()
}

if err := vm.Start(); err != nil {
return err
}
Expand Down Expand Up @@ -198,3 +220,33 @@ func runVirtualMachine(vmConfig *config.VirtualMachine, vm *vf.VirtualMachine) e

return <-errCh
}

func startIgnitionProvisionerServer(ignitionReader io.Reader, ignitionSocketPath string) error {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
_, err := io.Copy(w, ignitionReader)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could use https://pkg.go.dev/net/http#ServeContent (no need to make this change right, this can be a follow-up)

if err != nil {
log.Errorf("failed to serve ignition file: %v", err)
}
})

listener, err := net.Listen("unix", ignitionSocketPath)
if err != nil {
return err
}
defer func() {
if err := listener.Close(); err != nil {
log.Error(err)
}
}()

srv := &http.Server{
Handler: mux,
Addr: ignitionSocketPath,
ReadHeaderTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}

log.Debugf("ignition socket: %s", ignitionSocketPath)
return srv.Serve(listener)
}
4 changes: 4 additions & 0 deletions pkg/cmdline/cmdline.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ type Options struct {
LogLevel string

UseGUI bool

IgnitionPath string
}

const DefaultRestfulURI = "none://"
Expand Down Expand Up @@ -50,4 +52,6 @@ func AddFlags(cmd *cobra.Command, opts *Options) {
cmd.Flags().StringVar(&opts.LogLevel, "log-level", "", "set log level")
cmd.Flags().StringVar(&opts.RestfulURI, "restful-uri", DefaultRestfulURI, "URI address for RESTful services")

cmd.Flags().StringVar(&opts.IgnitionPath, "ignition", "", "path to the ignition file")

}
49 changes: 49 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"math"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"

Expand All @@ -30,6 +31,7 @@ type VirtualMachine struct {
Bootloader Bootloader `json:"bootloader"`
Devices []VirtioDevice `json:"devices,omitempty"`
Timesync *TimeSync `json:"timesync,omitempty"`
Ignition *Ignition `json:"ignition,omitempty"`
}

// TimeSync enables synchronization of the host time to the linux guest after the host was suspended.
Expand All @@ -38,13 +40,23 @@ type TimeSync struct {
VsockPort uint32 `json:"vsockPort"`
}

type Ignition struct {
ConfigPath string `json:"configPath"`
SocketPath string `json:"socketPath"`
}

// The VMComponent interface represents a VM element (device, bootloader, ...)
// which can be converted from/to commandline parameters
type VMComponent interface {
FromOptions([]option) error
ToCmdLine() ([]string, error)
}

const (
ignitionVsockPort uint = 1024
ignitionSocketName string = "ignition.sock"
)

// NewVirtualMachine creates a new VirtualMachine instance. The virtual machine
// will use vcpus virtual CPUs and it will be allocated memoryMiB mibibytes
// (1024*1024 bytes) of RAM. bootloader specifies how the virtual machine will
Expand Down Expand Up @@ -96,6 +108,10 @@ func (vm *VirtualMachine) ToCmdLine() ([]string, error) {
args = append(args, devArgs...)
}

if vm.Ignition != nil {
args = append(args, "--ignition", vm.Ignition.ConfigPath)
}

return args, nil
}

Expand Down Expand Up @@ -191,6 +207,39 @@ func (vm *VirtualMachine) TimeSync() *TimeSync {
return vm.Timesync
}

func IgnitionNew(configPath string, socketPath string) (*Ignition, error) {
if configPath == "" || socketPath == "" {
return nil, fmt.Errorf("config path and socket path cannot be empty")
}
return &Ignition{
ConfigPath: configPath,
SocketPath: socketPath,
}, nil
}

func (vm *VirtualMachine) AddIgnitionFileFromCmdLine(cmdlineOpts string) error {
if cmdlineOpts == "" {
return nil
}
opts := strings.Split(cmdlineOpts, ",")
if len(opts) != 1 {
return fmt.Errorf("ignition only accept one option in command line argument")
}

socketPath := filepath.Join(os.TempDir(), ignitionSocketName)
dev, err := VirtioVsockNew(ignitionVsockPort, socketPath, true)
if err != nil {
return err
}
vm.Devices = append(vm.Devices, dev)
ignition, err := IgnitionNew(opts[0], socketPath)
if err != nil {
return err
}
vm.Ignition = ignition
return nil
}

func TimeSyncNew(vsockPort uint) (VMComponent, error) {

if vsockPort > math.MaxUint32 {
Expand Down
28 changes: 28 additions & 0 deletions pkg/config/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const (
usbMassStorage vmComponentKind = "usbmassstorage"
nvme vmComponentKind = "nvme"
rosetta vmComponentKind = "rosetta"
ignition vmComponentKind = "ignition"
)

type jsonKind struct {
Expand Down Expand Up @@ -89,6 +90,17 @@ func unmarshalDevices(rawMsg json.RawMessage) ([]VirtioDevice, error) {
return devices, nil
}

func unmarshalIgnition(rawMsg json.RawMessage) (Ignition, error) {
var ignition Ignition

err := json.Unmarshal(rawMsg, &ignition)
if err != nil {
return Ignition{}, err
}

return ignition, nil
}

// VirtioNet needs a custom unmarshaller as net.HardwareAddress is not
// serialized/unserialized in its expected format, instead of
// '00:11:22:33:44:55', it's serialized as base64-encoded raw bytes such as
Expand Down Expand Up @@ -208,6 +220,11 @@ func (vm *VirtualMachine) UnmarshalJSON(b []byte) error {
if err == nil {
vm.Devices = devices
}
case "ignition":
ignition, err := unmarshalIgnition(*rawMsg)
if err == nil {
vm.Ignition = &ignition
}
}

if err != nil {
Expand Down Expand Up @@ -367,3 +384,14 @@ func (dev *USBMassStorage) MarshalJSON() ([]byte, error) {
USBMassStorage: *dev,
})
}

func (ign *Ignition) MarshalJSON() ([]byte, error) {
type devWithKind struct {
jsonKind
Ignition
}
return json.Marshal(devWithKind{
jsonKind: kind(ignition),
Ignition: *ign,
})
}
12 changes: 11 additions & 1 deletion pkg/config/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ var jsonTests = map[string]jsonTest{
},
expectedJSON: `{"vcpus":3,"memoryBytes":4194304000,"bootloader":{"kind":"linuxBootloader","vmlinuzPath":"/vmlinuz","initrdPath":"/initrd","kernelCmdLine":"console=hvc0"},"timesync":{"vsockPort":1234}}`,
},
"TestIgnition": {
cfergeau marked this conversation as resolved.
Show resolved Hide resolved
newVM: func(t *testing.T) *VirtualMachine {
vm := newLinuxVM(t)
ignition, err := IgnitionNew("config", "socket")
require.NoError(t, err)
vm.Ignition = ignition
return vm
},
expectedJSON: `{"vcpus":3,"memoryBytes":4194304000,"bootloader":{"kind":"linuxBootloader","vmlinuzPath":"/vmlinuz","initrdPath":"/initrd","kernelCmdLine":"console=hvc0"}, "ignition":{"kind":"ignition","configPath":"config","socketPath":"socket"}}`,
},
"TestVirtioRNG": {
newVM: func(t *testing.T) *VirtualMachine {
vm := newLinuxVM(t)
Expand Down Expand Up @@ -207,7 +217,7 @@ var jsonStabilityTests = map[string]jsonStabilityTest{

return vm
},
skipFields: []string{"Bootloader", "Devices", "Timesync"},
skipFields: []string{"Bootloader", "Devices", "Timesync", "Ignition"},
expectedJSON: `{"vcpus":3,"memoryBytes":3,"bootloader":{"kind":"linuxBootloader","vmlinuzPath":"/vmlinuz","kernelCmdLine":"console=hvc0","initrdPath":"/initrd"},"devices":[{"kind":"virtiorng"}],"timesync":{"vsockPort":1234}}`,
},
"RosettaShare": {
Expand Down