Skip to content

Commit

Permalink
pkg/adaptor/libvirt: cloud init ISO file using native go
Browse files Browse the repository at this point in the history
Fixes confidential-containers#1251
Fixes confidential-containers#1250

This PR uses a native go implementation of an tool to create
the cloud init ISO file, removing the dependency on
an external tool. It also creates the ISO in-memory. This
makes sense because the ISO file is very small and the content
is backed in memory anyway. Creating the ISO file in-memory
reduces the potential of errors, since nothing has to be written
to the file system of the hosting container.
ISO file generation as well as memory based image uploader
are backed by unit tests.

Signed-off-by: Dr. Carsten Leue <[email protected]>
  • Loading branch information
CarstenLeue authored and wainersm committed Sep 7, 2023
1 parent 4422f14 commit 8c25163
Show file tree
Hide file tree
Showing 9 changed files with 170 additions and 162 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ RUN make ARCH=$TARGETARCH COMMIT=$COMMIT VERSION=$VERSION RELEASE_BUILD=$RELEASE
FROM --platform=$TARGETPLATFORM $BASE as base-release

FROM base-release as base-dev
RUN dnf install -y libvirt-libs genisoimage /usr/bin/ssh && dnf clean all
RUN dnf install -y libvirt-libs /usr/bin/ssh && dnf clean all

FROM base-${BUILD_TYPE}
COPY --from=builder /work/cloud-api-adaptor /work/entrypoint.sh /usr/local/bin/
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ require (
github.com/confidential-containers/cloud-api-adaptor/peerpod-ctrl v0.0.0-20230329054732-0d6eda047e81
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f
github.com/kata-containers/kata-containers/src/runtime v0.0.0-20230721195217-16d6e37196cb
github.com/kdomanski/iso9660 v0.3.5
github.com/moby/sys/mountinfo v0.6.2
github.com/sirupsen/logrus v1.9.0
golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1195,6 +1195,8 @@ github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1q
github.com/kata-containers/kata-containers/src/runtime v0.0.0-20230721195217-16d6e37196cb h1:XpcSeQWRQeGqV38RvxB6ulEHpBkH9DlkCTMnA0ALm2c=
github.com/kata-containers/kata-containers/src/runtime v0.0.0-20230721195217-16d6e37196cb/go.mod h1:4i+EBdCeAg34WOxQMjiJ9e7ZtwtI7C5ZSK4tg70hoeE=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kdomanski/iso9660 v0.3.5 h1:LO1n75zPjLeDQkz0Pyk1eZ7JGinjKjk2C174GSABVwY=
github.com/kdomanski/iso9660 v0.3.5/go.mod h1:K+UlIGxKgtrdAWyoigPnFbeQLVs/Xudz4iztWFThBwo=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
Expand Down
51 changes: 51 additions & 0 deletions pkg/adaptor/cloud/libvirt/cloudinit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// (C) Copyright Confidential Containers Contributors
// SPDX-License-Identifier: Apache-2.0

package libvirt

import (
"bytes"

"github.com/kdomanski/iso9660"
)

const (
userDataFilename = "user-data"
metaDataFilename = "meta-data"
vendorDataFilename = "vendor-data"
ciDataVolumeName = "cidata"
)

// createCloudInit produces a cloud init ISO file as a data blob with a userdata and a metadata section
func createCloudInit(userData, metaData []byte) ([]byte, error) {
writer, err := iso9660.NewWriter()
if err != nil {
return nil, err
}
defer writer.Cleanup() //nolint:errcheck // no need to check error in deferal

err = writer.AddFile(bytes.NewReader(userData), userDataFilename)
if err != nil {
return nil, err
}

err = writer.AddFile(bytes.NewReader(metaData), metaDataFilename)
if err != nil {
return nil, err
}

err = writer.AddFile(bytes.NewReader([]byte{}), vendorDataFilename)
if err != nil {
return nil, err
}

var buf bytes.Buffer

err = writer.WriteTo(&buf, ciDataVolumeName)
if err != nil {
return nil, err
}

// done
return buf.Bytes(), nil
}
89 changes: 89 additions & 0 deletions pkg/adaptor/cloud/libvirt/cloudinit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// (C) Copyright Confidential Containers Contributors
// SPDX-License-Identifier: Apache-2.0

package libvirt

import (
CR "crypto/rand"
"fmt"
"io"
"math/rand"
"os"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"libvirt.org/go/libvirtxml"

"github.com/kdomanski/iso9660"
)

func TestCloudInit(t *testing.T) {

file, err := os.CreateTemp("", "CloudInit-*.iso")
require.NoError(t, err)
defer os.Remove(file.Name())

fmt.Printf("temp file: %s", file.Name())

userDataContent := []byte("userdata")
metaDataContent := []byte("metadata")

isoData, err := createCloudInit(userDataContent, metaDataContent)
require.NoError(t, err)

err = os.WriteFile(file.Name(), isoData, os.ModePerm)
require.NoError(t, err)

isoFile, err := os.Open(file.Name())
require.NoError(t, err)

isoImg, err := iso9660.OpenImage(isoFile)
require.NoError(t, err)

rootFile, err := isoImg.RootDir()
require.NoError(t, err)

children, err := rootFile.GetChildren()
require.NoError(t, err)

files := make(map[string][]byte)
for _, child := range children {
key := child.Name()
data, err := io.ReadAll(child.Reader())
require.NoError(t, err)

files[key] = data
}

assert.Equal(t, userDataContent, files[userDataFilename])
assert.Equal(t, metaDataContent, files[metaDataFilename])

err = isoFile.Close()
require.NoError(t, err)
}

func TestInMemoryCopier(t *testing.T) {
// generate some test data
size := rand.Intn(1000) + 1000
buf := make([]byte, size)
_, err := CR.Read(buf)
require.NoError(t, err)
// build the image abstraction
img, err := newImageFromBytes(buf)
require.NoError(t, err)

sizeFromImg, err := img.size()
require.NoError(t, err)
assert.Equal(t, uint64(size), sizeFromImg)

var otherBuf []byte
err = img.importImage(func(rdr io.Reader) error {
bufRead, err := io.ReadAll(rdr)
otherBuf = bufRead
return err
}, libvirtxml.StorageVolume{})
require.NoError(t, err)

assert.Equal(t, buf, otherBuf)
}
115 changes: 13 additions & 102 deletions pkg/adaptor/cloud/libvirt/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,9 @@ package libvirt
// Code copied from https://github.com/openshift/cluster-api-provider-libvirt

import (
"bytes"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"

libvirtxml "libvirt.org/go/libvirtxml"
)
Expand All @@ -25,109 +21,24 @@ type image interface {
string() string
}

type httpImage struct {
url *url.URL
// inMemoryImage represents an image backed by a byte array in memory
type inMemoryImage struct {
data []byte
}

func (i *httpImage) string() string {
return i.url.String()
// newImageFromBytes creates a new image implementation backed by an in-memory byte array
func newImageFromBytes(source []byte) (image, error) {
return &inMemoryImage{data: source}, nil
}

func (i *httpImage) size() (uint64, error) {
response, err := http.Head(i.url.String())
if err != nil {
return 0, err
}
if response.StatusCode != 200 {
return 0,
fmt.Errorf(
"Error accessing remote resource: %s - %s",
i.url.String(),
response.Status)
}

length, err := strconv.Atoi(response.Header.Get("Content-Length"))
if err != nil {
err = fmt.Errorf(
"Error while getting Content-Length of %q: %v - got %s",
i.url.String(),
err,
response.Header.Get("Content-Length"))
return 0, err
}
return uint64(length), nil
}

func (i *httpImage) importImage(copier func(io.Reader) error, vol libvirtxml.StorageVolume) error {
client := &http.Client{}
req, _ := http.NewRequest("GET", i.url.String(), nil)

if vol.Target.Timestamps != nil && vol.Target.Timestamps.Mtime != "" {
req.Header.Set("If-Modified-Since", timeFromEpoch(vol.Target.Timestamps.Mtime).UTC().Format(http.TimeFormat))
}
response, err := client.Do(req)

if err != nil {
return fmt.Errorf("Error while downloading %s: %s", i.url.String(), err)
}

defer response.Body.Close()
if response.StatusCode == http.StatusNotModified {
return nil
}

return copier(response.Body)
}

type localImage struct {
path string
}

func newImage(source string) (image, error) {
url, err := url.Parse(source)
if err != nil {
return nil, fmt.Errorf("can't parse source %q as url: %v", source, err)
}

if strings.HasPrefix(url.Scheme, "http") {
return &httpImage{url: url}, nil
} else if url.Scheme == "file" || url.Scheme == "" {
return &localImage{path: url.Path}, nil
} else {
return nil, fmt.Errorf("don't know how to read from %q: %s", url.String(), err)
}
func (i *inMemoryImage) string() string {
return fmt.Sprintf("plain bytes of size [%d]", len(i.data))
}

func (i *localImage) string() string {
return i.path
func (i *inMemoryImage) size() (uint64, error) {
return uint64(len(i.data)), nil
}

func (i *localImage) size() (uint64, error) {
fi, err := os.Stat(i.path)
if err != nil {
return 0, err
}
return uint64(fi.Size()), nil
}

func (i *localImage) importImage(copier func(io.Reader) error, vol libvirtxml.StorageVolume) error {
file, err := os.Open(i.path)
if err != nil {
return fmt.Errorf("Error while opening %s: %s", i.path, err)
}
defer file.Close()

fi, err := file.Stat()
if err != nil {
return err
}
// we can skip the upload if the modification times are the same
if vol.Target.Timestamps != nil && vol.Target.Timestamps.Mtime != "" {
if fi.ModTime() == timeFromEpoch(vol.Target.Timestamps.Mtime) {
logger.Printf("Modification time is the same: skipping image copy")
return nil
}
}

return copier(file)
func (i *inMemoryImage) importImage(copier func(io.Reader) error, vol libvirtxml.StorageVolume) error {
return copier(bytes.NewReader(i.data))
}
57 changes: 13 additions & 44 deletions pkg/adaptor/cloud/libvirt/libvirt.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@ import (
"context"
"encoding/xml"
"fmt"
"log"
"net/netip"
"os"
"os/exec"
"strconv"
"time"

Expand Down Expand Up @@ -65,45 +62,14 @@ func (s *sevGuestPolicy) getGuestPolicy() uint {
return res
}

func createCloudInitISO(v *vmConfig, libvirtClient *libvirtClient) string {
logger.Printf("Create cloudInit iso\n")
cloudInitIso := libvirtClient.dataDir + "/" + v.name + "-cloudinit.iso"
// createCloudInitISO creates an ISO file with a userdata and a metadata file. The ISO image will be created in-memory since it is small
func createCloudInitISO(v *vmConfig) ([]byte, error) {
logger.Println("Create cloudInit iso")

if _, err := os.Stat("/usr/bin/genisoimage"); os.IsNotExist(err) {
log.Fatal("'genisoimage' command doesn't exist.Please install the command before.")
}
userData := v.userData
metaData := fmt.Sprintf("local-hostname: %s", v.name)

// Set VM Hostname
if err := os.MkdirAll(libvirtClient.dataDir, os.ModePerm); err != nil {
log.Fatalf("Failed to create data-dir path %s: %s", v.metaData, err)
}
v.metaData = libvirtClient.dataDir + "/" + "meta-data"
metaFile, _ := os.Create(v.metaData)
if _, err := metaFile.WriteString("local-hostname: " + v.name); err != nil {
metaFile.Close()
log.Fatalf("Failed to write to %s: %s", v.metaData, err)
}
metaFile.Close()

// Write the userData to a file
userDataFile := libvirtClient.dataDir + "/" + "user-data"
udf, _ := os.Create(userDataFile)
if _, err := udf.WriteString(v.userData); err != nil {
udf.Close()
log.Fatalf("Failed to write to %s: %s", userDataFile, err)
}
udf.Close()

logger.Println("Executing genisoimage")
// genisoimage -output cloudInitIso.iso -volid cidata -joliet -rock user-data meta-data
cmd := exec.Command("genisoimage", "-output", cloudInitIso, "-volid", "cidata", "-joliet", "-rock", userDataFile, v.metaData)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
logger.Printf("Created cloudInit iso\n")
return cloudInitIso
return createCloudInit([]byte(userData), []byte(metaData))
}

func checkDomainExistsByName(name string, libvirtClient *libvirtClient) (exist bool, err error) {
Expand Down Expand Up @@ -138,12 +104,12 @@ func checkDomainExistsById(id uint32, libvirtClient *libvirtClient) (exist bool,

}

func uploadIso(isoFile string, isoVolName string, libvirtClient *libvirtClient) (string, error) {
func uploadIso(isoData []byte, isoVolName string, libvirtClient *libvirtClient) (string, error) {

logger.Printf("Uploading iso file: %s\n", isoFile)
logger.Printf("Uploading iso file: %s\n", isoVolName)
volumeDef := newDefVolume(isoVolName)

img, err := newImage(isoFile)
img, err := newImageFromBytes(isoData)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -584,7 +550,10 @@ func CreateDomain(ctx context.Context, libvirtClient *libvirtClient, v *vmConfig
return nil, fmt.Errorf("Error in creating volume: %s", err)
}

cloudInitIso := createCloudInitISO(v, libvirtClient)
cloudInitIso, err := createCloudInitISO(v)
if err != nil {
return nil, fmt.Errorf("error in creating cloud init ISO file, cause: %w", err)
}

isoVolName := v.name + "-cloudinit.iso"
isoVolFile, err := uploadIso(cloudInitIso, isoVolName, libvirtClient)
Expand Down
Loading

0 comments on commit 8c25163

Please sign in to comment.