Blob Blame History Raw
// +build integration

package vmwaretest

import (
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"

	// importing the packages registers these cli commands
	"github.com/vmware/govmomi/govc/cli"
	_ "github.com/vmware/govmomi/govc/datastore"
	_ "github.com/vmware/govmomi/govc/importx"
	_ "github.com/vmware/govmomi/govc/vm"
	_ "github.com/vmware/govmomi/govc/vm/guest"
)

const WaitTimeout = 6000 // in seconds

type AuthOptions struct {
	Host       string
	Username   string
	Password   string
	Datacenter string
	Cluster    string
	Network    string
	Datastore  string
	Folder     string
}

func AuthOptionsFromEnv() (*AuthOptions, error) {
	host, hostExists := os.LookupEnv("GOVMOMI_URL")
	username, userExists := os.LookupEnv("GOVMOMI_USERNAME")
	password, pwdExists := os.LookupEnv("GOVMOMI_PASSWORD")
	datacenter, dcExists := os.LookupEnv("GOVMOMI_DATACENTER")
	cluster, clusterExists := os.LookupEnv("GOVMOMI_CLUSTER")
	network, netExists := os.LookupEnv("GOVMOMI_NETWORK")
	datastore, dsExists := os.LookupEnv("GOVMOMI_DATASTORE")
	folder, folderExists := os.LookupEnv("GOVMOMI_FOLDER")

	// If only one/two of them are not set, then fail
	if !hostExists {
		return nil, errors.New("GOVMOMI_URL not set")
	}

	if !userExists {
		return nil, errors.New("GOVMOMI_USERNAME not set")
	}

	if !pwdExists {
		return nil, errors.New("GOVMOMI_PASSWORD not set")
	}

	if !dcExists {
		return nil, errors.New("GOVMOMI_DATACENTER not set")
	}

	if !clusterExists {
		return nil, errors.New("GOVMOMI_CLUSTER not set")
	}

	if !netExists {
		return nil, errors.New("GOVMOMI_NETWORK not set")
	}

	if !dsExists {
		return nil, errors.New("GOVMOMI_DATASTORE not set")
	}

	if !folderExists {
		return nil, errors.New("GOVMOMI_FOLDER not set")
	}

	return &AuthOptions{
		Host:       host,
		Username:   username,
		Password:   password,
		Datacenter: datacenter,
		Cluster:    cluster,
		Network:    network,
		Datastore:  datastore,
		Folder:     folder,
	}, nil
}

func ImportImage(creds *AuthOptions, imagePath, imageName string) error {
	args := []string{
		"import.vmdk",
		fmt.Sprintf("-u=%s:%s@%s", creds.Username, creds.Password, creds.Host),
		"-k=true",
		fmt.Sprintf("-pool=%s/Resources", creds.Cluster),
		fmt.Sprintf("-dc=%s", creds.Datacenter),
		fmt.Sprintf("-ds=%s", creds.Datastore),
		imagePath,
		imageName,
	}
	retcode := cli.Run(args)

	if retcode != 0 {
		return errors.New("importing vmdk failed")
	}
	return nil
}

func DeleteImage(creds *AuthOptions, directoryName string) error {
	retcode := cli.Run([]string{
		"datastore.rm",
		"-f=true",
		fmt.Sprintf("-u=%s:%s@%s", creds.Username, creds.Password, creds.Host),
		"-k=true",
		fmt.Sprintf("-dc=%s", creds.Datacenter),
		fmt.Sprintf("-ds=%s", creds.Datastore),
		directoryName + "*", // because vm.create creates another directory with _1 prefix
	})

	if retcode != 0 {
		return errors.New("deleting directory failed")
	}
	return nil
}

func runWithStdout(args []string) (string, int) {
	oldStdout := os.Stdout
	r, w, _ := os.Pipe()
	os.Stdout = w

	retcode := cli.Run(args)

	w.Close()
	out, _ := ioutil.ReadAll(r)
	os.Stdout = oldStdout

	return strings.TrimSpace(string(out)), retcode
}

func WithBootedImage(creds *AuthOptions, imagePath, imageName, publicKey string, f func(address string) error) (retErr error) {
	vmdkBaseName := filepath.Base(imagePath)

	args := []string{
		"vm.create",
		fmt.Sprintf("-u=%s:%s@%s", creds.Username, creds.Password, creds.Host),
		"-k=true",
		fmt.Sprintf("-pool=%s/Resources", creds.Cluster),
		fmt.Sprintf("-dc=%s", creds.Datacenter),
		fmt.Sprintf("-ds=%s", creds.Datastore),
		fmt.Sprintf("-folder=%s", creds.Folder),
		fmt.Sprintf("-net=%s", creds.Network),
		"-m=2048", "-g=rhel8_64Guest", "-on=true", "-firmware=bios",
		fmt.Sprintf("-disk=%s/%s", imageName, vmdkBaseName),
		"--disk.controller=ide",
		imageName,
	}
	retcode := cli.Run(args)
	if retcode != 0 {
		return errors.New("Creating VM from vmdk failed")
	}

	defer func() {
		args = []string{
			"vm.destroy",
			fmt.Sprintf("-u=%s:%s@%s", creds.Username, creds.Password, creds.Host),
			"-k=true",
			imageName,
		}
		retcode := cli.Run(args)

		if retcode != 0 {
			fmt.Printf("Deleting VM %s failed", imageName)
			return
		}
	}()

	// note: by default this will wait/block until an IP address is returned
	// note: using exec() instead of running the command b/c .Run() returns an int
	args = []string{
		"vm.ip",
		fmt.Sprintf("-u=%s:%s@%s", creds.Username, creds.Password, creds.Host),
		"-k=true",
		imageName,
	}
	ipAddress, retcode := runWithStdout(args)

	if retcode != 0 {
		return errors.New("Getting IP address for VM failed")
	}

	// Disabled b/c of https://github.com/vmware/govmomi/issues/2054
	// upload public key on the VM
	//args = []string{
	//	"guest.mkdir",
	//	fmt.Sprintf("-u=%s:%s@%s", creds.Username, creds.Password, creds.Host),
	//	"-k=true",
	//	fmt.Sprintf("-vm=%s", imageName),
	//	"-p", "/root/.ssh",
	//}
	//retcode = cli.Run(args)
	//if retcode != 0 {
	//	return errors.New("mkdir /root/.ssh on VM failed")
	//}

	//args = []string{
	//	"guest.upload",
	//	fmt.Sprintf("-u=%s:%s@%s", creds.Username, creds.Password, creds.Host),
	//	"-k=true",
	//	fmt.Sprintf("-vm=%s", imageName),
	//	"-f=true",
	//	publicKey, // this is a file path
	//	"/root/.ssh/authorized_keys",
	//}
	//retcode = cli.Run(args)
	//if retcode != 0 {
	//	return errors.New("Uploading public key to VM failed")
	//}

	return f(ipAddress)
}

// hard-coded SSH keys b/c we're having troubles uploading publicKey
// to the VM, see https://github.com/vmware/govmomi/issues/2054
func WithSSHKeyPair(f func(privateKey, publicKey string) error) error {
	public := "/usr/share/tests/osbuild-composer/keyring/id_rsa.pub"
	private := "/usr/share/tests/osbuild-composer/keyring/id_rsa"

	return f(private, public)
}