diff --git a/Dockerfile b/Dockerfile index a902ba515..6c3b99e5e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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/ diff --git a/go.mod b/go.mod index 2e32da565..98c709e71 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 3b67a4a9d..41c5707ff 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/adaptor/cloud/libvirt/cloudinit.go b/pkg/adaptor/cloud/libvirt/cloudinit.go new file mode 100644 index 000000000..be82f2ffe --- /dev/null +++ b/pkg/adaptor/cloud/libvirt/cloudinit.go @@ -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 +} diff --git a/pkg/adaptor/cloud/libvirt/cloudinit_test.go b/pkg/adaptor/cloud/libvirt/cloudinit_test.go new file mode 100644 index 000000000..7153e90cf --- /dev/null +++ b/pkg/adaptor/cloud/libvirt/cloudinit_test.go @@ -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) +} diff --git a/pkg/adaptor/cloud/libvirt/image.go b/pkg/adaptor/cloud/libvirt/image.go index ee45786fe..b12d25ddd 100644 --- a/pkg/adaptor/cloud/libvirt/image.go +++ b/pkg/adaptor/cloud/libvirt/image.go @@ -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" ) @@ -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)) } diff --git a/pkg/adaptor/cloud/libvirt/libvirt.go b/pkg/adaptor/cloud/libvirt/libvirt.go index 3812afe82..a68fae40d 100644 --- a/pkg/adaptor/cloud/libvirt/libvirt.go +++ b/pkg/adaptor/cloud/libvirt/libvirt.go @@ -9,10 +9,7 @@ import ( "context" "encoding/xml" "fmt" - "log" "net/netip" - "os" - "os/exec" "strconv" "time" @@ -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) { @@ -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 } @@ -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) diff --git a/pkg/adaptor/cloud/libvirt/types.go b/pkg/adaptor/cloud/libvirt/types.go index 3b5dedb43..b54a59650 100644 --- a/pkg/adaptor/cloud/libvirt/types.go +++ b/pkg/adaptor/cloud/libvirt/types.go @@ -29,7 +29,6 @@ type vmConfig struct { mem uint rootDiskSize uint64 userData string - metaData string ips []netip.Addr instanceId string //keeping it consistent with sandbox.vsi launchSecurityType LaunchSecurityType diff --git a/pkg/adaptor/cloud/libvirt/volume.go b/pkg/adaptor/cloud/libvirt/volume.go index e837099ae..8324a2542 100644 --- a/pkg/adaptor/cloud/libvirt/volume.go +++ b/pkg/adaptor/cloud/libvirt/volume.go @@ -12,8 +12,6 @@ import ( "errors" "fmt" "io" - "strconv" - "strings" "time" libvirt "libvirt.org/go/libvirt" @@ -107,18 +105,6 @@ func newDefVolumeFromXML(s string) (libvirtxml.StorageVolume, error) { return volumeDef, nil } -func timeFromEpoch(str string) time.Time { - var s, ns int - - ts := strings.Split(str, ".") - if len(ts) == 2 { - ns, _ = strconv.Atoi(ts[1]) - } - s, _ = strconv.Atoi(ts[0]) - - return time.Unix(int64(s), int64(ns)) -} - func uploadVolume(libvirtClient *libvirtClient, volumeDef libvirtxml.StorageVolume, img image) (volumeKey string, err error) { // Refresh the pool of the volume so that libvirt knows it is