+
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
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")
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm a bit confused by the error message and the test. If I'm not mistaken, the test implies that we need user-data or meta-data to be provided, but the way I understand the error message, we expect to get both?

Copy link
Contributor Author

@lstocchi lstocchi Feb 25, 2025

Choose a reason for hiding this comment

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

Cloud-init requires both files to work. They can be empty but they must exist otherwise it won't work.

However I think it's reasonable to allow the user to only pass one file. Maybe you just need to push some customization using the user-data (or the meta-data) and leave the other file empty. This way if we find out that you only have the user-data, we create an empty meta-data file (or vice versa).

On the other hand, if we do not find any of those files, there is most probably an error and the user needs to be informed

Copy link
Collaborator

Choose a reason for hiding this comment

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

should the input be a dir that contains files and we check for meta and user data as a minimum and proceed? or is it common to have multiples of those? (when I was a cloud-init expert in the day, it was just mostly those two)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i am using this stuff in another tool and I thought the same thing.
Would it be acceptable to allow the user to pass a folder or files? Or would it be confusing?
--cloud-init <folder-path>
and also
--cloud-init <folder-path>/userdata,<other-folder>/meta-data

}

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 @@ -8,6 +8,7 @@ require (
github.com/containers/common v0.60.4
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.8.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 }
Loading
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载