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
228 changes: 226 additions & 2 deletions tests/e2e/lib/virt_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"

corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
Expand All @@ -23,6 +24,11 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
emulationAnnotation = "kubevirt.kubevirt.io/jsonpatch"
useEmulation = `[{"op": "add", "path": "/spec/configuration/developerConfiguration", "value": {"useEmulation": true}}]`
)

var packageManifestsGvr = schema.GroupVersionResource{
Group: "packages.operators.coreos.com",
Resource: "packagemanifests",
Expand All @@ -35,6 +41,12 @@ var hyperConvergedGvr = schema.GroupVersionResource{
Version: "v1beta1",
}

var virtualMachineGvr = schema.GroupVersionResource{
Group: "kubevirt.io",
Resource: "virtualmachines",
Version: "v1",
}

var csvGvr = schema.GroupVersionResource{
Group: "operators.coreos.com",
Resource: "clusterserviceversion",
Expand Down Expand Up @@ -210,6 +222,32 @@ func (v *VirtOperator) checkHco() bool {
return health == "healthy"
}

// Check if KVM emulation is enabled.

Choose a reason for hiding this comment

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

what is KVM?

Copy link
Member

Choose a reason for hiding this comment

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

I think it's referring to this qemu KVM . Read more at wiki

Copy link
Contributor Author

Choose a reason for hiding this comment

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

KVM here means kernel virtual machine, which is a kernel feature that accelerates virtual machines using the virtualization extensions provided by the CPU. Most cloud clusters do not have direct access to the CPU, and can't use KVM. So this pull request turns on an emulation mode that is much slower, but will work on our CI instances.

func (v *VirtOperator) checkEmulation() bool {
hco, err := v.Dynamic.Resource(hyperConvergedGvr).Namespace("openshift-cnv").Get(context.Background(), "kubevirt-hyperconverged", v1.GetOptions{})
if err != nil {
return false
}
if hco == nil {
return false
}

// Look for JSON patcher annotation that enables emulation.
patcher, ok, err := unstructured.NestedString(hco.UnstructuredContent(), "metadata", "annotations", emulationAnnotation)
if err != nil {
log.Printf("Failed to get KVM emulation annotation from HCO: %v", err)
return false
}
if !ok {
log.Printf("No KVM emulation annotation (%s) listed on HCO!", emulationAnnotation)

Choose a reason for hiding this comment

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

should return false here?

Copy link
Contributor

Choose a reason for hiding this comment

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

It will as it will hit line no 248.

}
if strings.Compare(patcher, useEmulation) == 0 {
return true
}

return false
}

// Creates the target virtualization namespace, likely openshift-cnv or kubevirt-hyperconverged
func (v *VirtOperator) installNamespace() error {
err := v.Client.Create(context.Background(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: v.Namespace}})
Expand Down Expand Up @@ -281,6 +319,36 @@ func (v *VirtOperator) installHco() error {
return nil
}

func (v *VirtOperator) configureEmulation() error {
hco, err := v.Dynamic.Resource(hyperConvergedGvr).Namespace("openshift-cnv").Get(context.Background(), "kubevirt-hyperconverged", v1.GetOptions{})
if err != nil {
return err
}
if hco == nil {
return fmt.Errorf("could not find hyperconverged operator to set emulation annotation")
}

annotations, ok, err := unstructured.NestedMap(hco.UnstructuredContent(), "metadata", "annotations")
if err != nil {
return err
}
if !ok {
annotations = make(map[string]interface{})
}
annotations[emulationAnnotation] = useEmulation

if err := unstructured.SetNestedMap(hco.UnstructuredContent(), annotations, "metadata", "annotations"); err != nil {
return err
}

_, err = v.Dynamic.Resource(hyperConvergedGvr).Namespace("openshift-cnv").Update(context.Background(), hco, v1.UpdateOptions{})
if err != nil {
return err
}

return nil
}

// Creates target namespace if needed, and waits for it to exist
func (v *VirtOperator) ensureNamespace(timeout time.Duration) error {
if !v.checkNamespace() {
Expand Down Expand Up @@ -513,6 +581,150 @@ func (v *VirtOperator) ensureHcoRemoved(timeout time.Duration) error {
return err
}

func (v *VirtOperator) getVmStatus(namespace, name string) (string, error) {
vm, err := v.Dynamic.Resource(virtualMachineGvr).Namespace(namespace).Get(context.Background(), name, v1.GetOptions{})
if err != nil {
return "", err
}

status, ok, err := unstructured.NestedString(vm.UnstructuredContent(), "status", "printableStatus")
if err != nil {
return "", err
}
if !ok {
return "", fmt.Errorf("status field not populated yet on VM %s/%s", namespace, name)
}
log.Printf("VM %s/%s status is: %s", namespace, name, status)

return status, nil
}

func (v *VirtOperator) checkVmExists(namespace, name string) bool {
_, err := v.getVmStatus(namespace, name)
if err == nil {
return true
}
return false
}

func (v *VirtOperator) checkVmStatus(namespace, name, expectedStatus string) bool {
status, _ := v.getVmStatus(namespace, name)
return status == expectedStatus
}

func (v *VirtOperator) createVm(namespace, name, source string) error {
unstructuredVm := unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "kubevirt.io/v1",
"kind": "VirtualMachine",
"metadata": map[string]interface{}{
"name": name,
"namespace": namespace,
},
"spec": map[string]interface{}{
"running": true,
"template": map[string]interface{}{
"spec": map[string]interface{}{
"domain": map[string]interface{}{
"devices": map[string]interface{}{
"disks": []map[string]interface{}{
{
"disk": map[string]interface{}{
"bus": "virtio",
},
"name": "rootdisk",
},
},
},
"resources": map[string]interface{}{
"requests": map[string]interface{}{
"cpu": "1",
"memory": "256Mi",
},
},
},
"volumes": []map[string]interface{}{
{
"dataVolume": map[string]interface{}{
"name": name,
},
"name": "rootdisk",
},
},
},
},
},
},
}

if _, err := v.Dynamic.Resource(virtualMachineGvr).Namespace(namespace).Create(context.TODO(), &unstructuredVm, v1.CreateOptions{}); err != nil {
return fmt.Errorf("error creating VM %s/%s: %w", namespace, name, err)
}

return nil
}

func (v *VirtOperator) removeVm(namespace, name string) error {
if err := v.Dynamic.Resource(virtualMachineGvr).Namespace(namespace).Delete(context.TODO(), name, v1.DeleteOptions{}); err != nil {
if !apierrors.IsNotFound(err) {
return fmt.Errorf("error deleting VM %s/%s: %w", namespace, name, err)
}
log.Printf("VM %s/%s not found, delete not necessary.", namespace, name)
}

return nil
}

func (v *VirtOperator) ensureVm(namespace, name, source string, timeout time.Duration) error {

Choose a reason for hiding this comment

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

here the logic is right? It will check if VM exists (without checking status), but if it fails, then it will check status

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe not, it doesn't look like I thought this through. The existence check is really there to speed up development of the CI test itself, since I don't have it tear down and recreate everything from scratch on every run. The status check polling is for the newly created VM. I can probably take out the existence check, since Kubernetes will complain if the VM is already there when the test tries to create it.

if err := v.createVm(namespace, name, source); err != nil {
return fmt.Errorf("failed to create VM %s/%s: %w", namespace, name, err)
}

err := wait.PollImmediate(5*time.Second, timeout, func() (bool, error) {
return v.checkVmStatus(namespace, name, "Running"), nil
})

return err
}

func (v *VirtOperator) ensureVmRemoval(namespace, name string, timeout time.Duration) error {
if !v.checkVmExists(namespace, name) {
log.Printf("VM %s/%s already removed, no action required", namespace, name)
return nil
}

if err := v.removeVm(namespace, name); err != nil {
return err
}

err := wait.PollImmediate(5*time.Second, timeout, func() (bool, error) {
return !v.checkVmExists(namespace, name), nil
})

return err
}

// Enable KVM emulation for use on cloud clusters that do not have direct
// access to the host server's virtualization capabilities.
func (v *VirtOperator) EnsureEmulation(timeout time.Duration) error {
if v.checkEmulation() {
log.Printf("KVM emulation already enabled, no work needed to turn it on.")
return nil
}

log.Printf("Enabling KVM emulation...")

if err := v.configureEmulation(); err != nil {
return err
}

err := wait.PollImmediate(5*time.Second, timeout, func() (bool, error) {
return v.checkEmulation(), nil
})

return err
}

// IsVirtInstalled returns whether or not the OpenShift Virtualization operator
// is installed and ready, by checking for a HyperConverged operator resource.
func (v *VirtOperator) IsVirtInstalled() bool {
Expand All @@ -525,7 +737,7 @@ func (v *VirtOperator) IsVirtInstalled() bool {

// EnsureVirtInstallation makes sure the OpenShift Virtualization operator is
// installed. This will install the operator if it is not already present.
func (v *VirtOperator) EnsureVirtInstallation(timeout time.Duration) error {
func (v *VirtOperator) EnsureVirtInstallation() error {
if v.IsVirtInstalled() {
log.Printf("Virtualization operator already installed, no action needed")
return nil
Expand Down Expand Up @@ -565,7 +777,7 @@ func (v *VirtOperator) EnsureVirtInstallation(timeout time.Duration) error {
}

// EnsureVirtRemoval makes sure the virtualization operator is removed.
func (v *VirtOperator) EnsureVirtRemoval(timeout time.Duration) error {
func (v *VirtOperator) EnsureVirtRemoval() error {
log.Printf("Removing hyperconverged operator")
if err := v.ensureHcoRemoved(3 * time.Minute); err != nil {
return err
Expand Down Expand Up @@ -598,3 +810,15 @@ func (v *VirtOperator) EnsureVirtRemoval(timeout time.Duration) error {

return nil
}

// Create a virtual machine from an existing PVC.
func (v *VirtOperator) CreateVm(namespace, name, source string, timeout time.Duration) error {
log.Printf("Creating virtual machine %s/%s", namespace, name)
return v.ensureVm(namespace, name, source, timeout)
}

// Remove a virtual machine, but leave its data volume.
func (v *VirtOperator) RemoveVm(namespace, name string, timeout time.Duration) error {
log.Printf("Removing virtual machine %s/%s", namespace, name)
return v.ensureVmRemoval(namespace, name, timeout)
}
Loading