Skip to content

Commit

Permalink
Merge pull request #263 from lstocchi/i208
Browse files Browse the repository at this point in the history
allow to pass user/meta data files for cloud-init config
  • Loading branch information
openshift-merge-bot[bot] authored Feb 26, 2025
2 parents 694548d + 7ef4a54 commit aa3cf4c
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 2 deletions.
102 changes: 102 additions & 0 deletions cmd/vfkit/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ limitations under the License.
package main

import (
"bytes"
"fmt"
"io"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"runtime"
"syscall"
"time"
Expand All @@ -36,6 +38,7 @@ import (
"github.com/crc-org/vfkit/pkg/rest"
restvf "github.com/crc-org/vfkit/pkg/rest/vf"
"github.com/crc-org/vfkit/pkg/vf"
"github.com/kdomanski/iso9660"
log "github.com/sirupsen/logrus"

"github.com/crc-org/vfkit/pkg/util"
Expand Down Expand Up @@ -87,6 +90,16 @@ func newVMConfiguration(opts *cmdline.Options) (*config.VirtualMachine, error) {
return nil, err
}

cloudInitISO, err := generateCloudInitImage(opts.CloudInitFiles.GetSlice())
if err != nil {
return nil, err
}

// if it generated a valid cloudinit config ISO file we add it to the devices
if cloudInitISO != "" {
opts.Devices = append(opts.Devices, fmt.Sprintf("virtio-blk,path=%s", cloudInitISO))
}

if err := vmConfig.AddDevicesFromCmdLine(opts.Devices); err != nil {
return nil, err
}
Expand Down Expand Up @@ -264,3 +277,92 @@ func startIgnitionProvisionerServer(ignitionReader io.Reader, ignitionSocketPath
log.Debugf("ignition socket: %s", ignitionSocketPath)
return srv.Serve(listener)
}

// it generates a cloud init image by taking the files passed by the user
// as cloud-init expects files with a specific name (e.g user-data, meta-data) we check the filenames to retrieve the correct info
// if some file is not passed by the user, an empty file will be copied to the cloud-init ISO to
// guarantee it to work (user-data and meta-data files are mandatory and both must exists, even if they are empty)
// if both files are missing it returns an error
func generateCloudInitImage(files []string) (string, error) {
if len(files) == 0 {
return "", nil
}

configFiles := map[string]io.Reader{
"user-data": nil,
"meta-data": nil,
}

hasConfigFile := false
for _, path := range files {
if path == "" {
continue
}
file, err := os.Open(path)
if err != nil {
return "", err
}
defer file.Close()

filename := filepath.Base(path)
if _, ok := configFiles[filename]; ok {
hasConfigFile = true
configFiles[filename] = file
}
}

if !hasConfigFile {
return "", fmt.Errorf("cloud-init needs user-data and meta-data files to work")
}

return createCloudInitISO(configFiles)
}

// It generates a temp ISO file containing the files passed by the user
// It also register an exit handler to delete the file when vfkit exits
func createCloudInitISO(files map[string]io.Reader) (string, error) {
writer, err := iso9660.NewWriter()
if err != nil {
return "", fmt.Errorf("failed to create writer: %w", err)
}

defer func() {
if err := writer.Cleanup(); err != nil {
log.Error(err)
}
}()

for name, reader := range files {
// if reader is nil, we set it to an empty file
if reader == nil {
reader = bytes.NewReader([]byte{})
}
err = writer.AddFile(reader, name)
if err != nil {
return "", fmt.Errorf("failed to add %s file: %w", name, err)
}
}

isoFile, err := os.CreateTemp("", "vfkit-cloudinit-")
if err != nil {
return "", fmt.Errorf("unable to create temporary cloud-init ISO file: %w", err)
}

defer func() {
if err := isoFile.Close(); err != nil {
log.Error(fmt.Errorf("failed to close cloud-init ISO file: %w", err))
}
}()

// register handler to remove isoFile when exiting
util.RegisterExitHandler(func() {
os.Remove(isoFile.Name())
})

err = writer.WriteTo(isoFile, "cidata")
if err != nil {
return "", fmt.Errorf("failed to write cloud-init ISO image: %w", err)
}

return isoFile.Name(), nil
}
65 changes: 65 additions & 0 deletions cmd/vfkit/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net"
"net/http"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -42,3 +43,67 @@ func TestStartIgnitionProvisionerServer(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, ignitionData, body)
}

func getTestAssetsDir() (string, error) {
currentDir, err := os.Getwd()
if err != nil {
return "", err
}

projectRoot := filepath.Join(currentDir, "../../")
return filepath.Join(projectRoot, "test", "assets"), nil
}

func TestGenerateCloudInitImage(t *testing.T) {
assetsDir, err := getTestAssetsDir()
require.NoError(t, err)

iso, err := generateCloudInitImage([]string{
filepath.Join(assetsDir, "user-data"),
filepath.Join(assetsDir, "meta-data"),
})
require.NoError(t, err)

assert.Contains(t, iso, "vfkit-cloudinit")

_, err = os.Stat(iso)
require.NoError(t, err)

err = os.Remove(iso)
require.NoError(t, err)
}

func TestGenerateCloudInitImageWithMissingFile(t *testing.T) {
assetsDir, err := getTestAssetsDir()
require.NoError(t, err)

iso, err := generateCloudInitImage([]string{
filepath.Join(assetsDir, "user-data"),
})
require.NoError(t, err)

assert.Contains(t, iso, "vfkit-cloudinit")

_, err = os.Stat(iso)
require.NoError(t, err)

err = os.Remove(iso)
require.NoError(t, err)
}

func TestGenerateCloudInitImageWithWrongFile(t *testing.T) {
assetsDir, err := getTestAssetsDir()
require.NoError(t, err)

iso, err := generateCloudInitImage([]string{
filepath.Join(assetsDir, "seed.img"),
})
assert.Empty(t, iso)
require.Error(t, err, "cloud-init needs user-data and meta-data files to work")
}

func TestGenerateCloudInitImageWithNoFile(t *testing.T) {
iso, err := generateCloudInitImage([]string{})
assert.Empty(t, iso)
require.NoError(t, err)
}
23 changes: 22 additions & 1 deletion doc/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,24 @@ A copy-on-write image can be created using `cp -c` or [clonefile(2)](http://www.
#### Cloud-init

The `--device virtio-blk` option can also be used to supply an initial configuration to cloud-init through a disk image.
Vfkit can create this ISO image automatically, or you can provide a pre-made ISO.

The ISO image file must be labelled cidata or CIDATA and it must contain the user-data and meta-data files.
##### Automatic ISO Creation

Vfkit allows you to pass the file paths of your `user-data` and `meta-data` files directly as arguments.
It will then handle the creation of the ISO image and the virtio-blk device internally.

Example
```
--cloud-init /Users/virtuser/user-data,/Users/virtuser/meta-data
```

N.B: Vfkit detects the files by using their names so make sure to save them as `user-data` and `meta-data`.

##### Manual ISO Creation

Alternatively, you can create the ISO image yourself.
The ISO image file must be labelled cidata or CIDATA and it must contain the user-data and meta-data files.
It is also possible to add further configurations by using the network-config and vendor-data files.
See https://cloudinit.readthedocs.io/en/latest/reference/datasources/nocloud.html#runtime-configurations for more details.

Expand All @@ -188,6 +204,11 @@ To also provide the cloud-init configuration you can add an additional virtio-bl
--device virtio-blk,path=/Users/virtuser/cloudinit.img
```

If you prefer to use the automatic ISO creation
```
--cloud-init /Users/virtuser/user-data,/Users/virtuser/meta-data
```


### NVM Express

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/containers/common v0.62.0
github.com/crc-org/crc/v2 v2.47.0
github.com/gin-gonic/gin v1.10.0
github.com/kdomanski/iso9660 v0.4.0
github.com/prashantgupta24/mac-sleep-notifier v1.0.1
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.9.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kdomanski/iso9660 v0.4.0 h1:BPKKdcINz3m0MdjIMwS0wx1nofsOjxOq8TOr45WGHFg=
github.com/kdomanski/iso9660 v0.4.0/go.mod h1:OxUSupHsO9ceI8lBLPJKWBTphLemjrCQY8LPXM7qSzU=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
Expand Down
4 changes: 3 additions & 1 deletion pkg/cmdline/cmdline.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ type Options struct {
UseGUI bool

IgnitionPath string

CloudInitFiles stringSliceValue
}

const DefaultRestfulURI = "none://"
Expand Down Expand Up @@ -53,5 +55,5 @@ func AddFlags(cmd *cobra.Command, opts *Options) {
cmd.Flags().StringVar(&opts.RestfulURI, "restful-uri", DefaultRestfulURI, "URI address for RESTful services")

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

cmd.Flags().VarP(&opts.CloudInitFiles, "cloud-init", "", "path to user-data and meta-data cloud-init configuration files")
}
Empty file added test/assets/meta-data
Empty file.
17 changes: 17 additions & 0 deletions test/assets/user-data
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#cloud-config
users:
- name: core
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash
groups: users
lock_passwd: false
ssh_authorized_keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDAcKzAecDf0R0mrLhO2eswdq6YRpLUFN4JNonl71Dud
- name: user2
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash
groups: users
plain_text_passwd: test
lock_passwd: false
ssh_pwauth: true
chpasswd: { expire: false }

0 comments on commit aa3cf4c

Please sign in to comment.