Skip to content

Commit

Permalink
Merge pull request #209 from lstocchi/i124
Browse files Browse the repository at this point in the history
ignition: add support for ignition config file
  • Loading branch information
openshift-merge-bot[bot] authored Oct 23, 2024
2 parents 0ca2af7 + 21c947b commit 1351d5a
Show file tree
Hide file tree
Showing 10 changed files with 266 additions and 1 deletion.
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)
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)
}
44 changes: 44 additions & 0 deletions cmd/vfkit/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package main

import (
"bytes"
"io"
"net"
"net/http"
"os"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestStartIgnitionProvisionerServer(t *testing.T) {
socketPath := "virtiovsock"
defer os.Remove(socketPath)

ignitionData := []byte("ignition configuration")
ignitionReader := bytes.NewReader(ignitionData)

// Start the server using the socket so that it can returns the ignition data
go func() {
err := startIgnitionProvisionerServer(ignitionReader, socketPath)
require.NoError(t, err)
}()

// Make a request to the server
client := http.Client{
Transport: &http.Transport{
Dial: func(_, _ string) (net.Conn, error) {
return net.Dial("unix", socketPath)
},
},
}
resp, err := client.Get("http://unix://" + socketPath)
require.NoError(t, err)
defer resp.Body.Close()

// Verify the response from the server is actually the ignition data
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, ignitionData, body)
}
9 changes: 9 additions & 0 deletions contrib/ignition/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## Ignition

Ignition uses a JSON configuration file to define the desired changes. The format of this config is specified in detail [here](https://coreos.github.io/ignition/specs/), and its [MIME type](http://www.iana.org/assignments/media-types/application/vnd.coreos.ignition+json) is registered with IANA.

`myconfig.json` file provides an example of configuration that adds a new `testuser`, creates a new file to `/etc/myapp` with the content listed in the same `files` section and add a systemd unit drop-in to modify the existing service `systemd-journald` and sets its environment variable SYSTEMD_LOG_LEVEL to debug.

### Examples

More examples can be found at https://coreos.github.io/ignition/examples/
31 changes: 31 additions & 0 deletions contrib/ignition/myconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"ignition": {
"version": "3.0.0"
},
"passwd": {
"users": [
{
"name": "testuser",
"sshAuthorizedKeys": [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO43Z9aRPvvj1zMib/Hh4oWX+MuOPXdFChFMaWfishLj"
]
}
]
},
"storage": {
"files": [{
"path": "/etc/someconfig",
"mode": 420,
"contents": { "source": "data:,example%20file%0A" }
}]
},
"systemd": {
"units": [{
"name": "systemd-journald.service",
"dropins": [{
"name": "debug.conf",
"contents": "[Service]\nEnvironment=SYSTEMD_LOG_LEVEL=debug"
}]
}]
}
}
15 changes: 15 additions & 0 deletions doc/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -461,3 +461,18 @@ Proper use of this flag may look similar to the following section of a command:
```bash
--device virtio-input,keyboard --device virtio-input,pointing --device virtio-gpu,width=1920,height=1080 --gui
```

### Ignition

#### Description

The `--ignition` option allows you to specify a configuration file for Ignition. Vfkit will open a vsock connection between the host and the guest and start a lightweight HTTP server to push the configuration file to Ignition.

You can find example configurations and more details about Ignition at https://coreos.github.io/ignition/

#### Example

This command provisions the configuration file to Ignition on the guest
```
--ignition configuration-path
```
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
23 changes: 23 additions & 0 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package config

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestAddIgnitionFile_MultipleOptions(t *testing.T) {
vm := &VirtualMachine{}
err := vm.AddIgnitionFileFromCmdLine("file1,file2")
assert.EqualError(t, err, "ignition only accept one option in command line argument")
}

func TestAddIgnitionFile_OneOption(t *testing.T) {
vm := &VirtualMachine{}
err := vm.AddIgnitionFileFromCmdLine("file1")
require.NoError(t, err)
assert.Len(t, vm.Devices, 1)
assert.Equal(t, uint32(ignitionVsockPort), vm.Devices[0].(*VirtioVsock).Port)
assert.Equal(t, "file1", vm.Ignition.ConfigPath)
}
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": {
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

0 comments on commit 1351d5a

Please sign in to comment.