github.com/Equinix-Metal/virtlet@v1.5.2-0.20210807010419-342346535dc5/tests/e2e/framework/vm_interface.go (about)

     1  /*
     2  Copyright 2017 Mirantis
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package framework
    18  
    19  import (
    20  	"encoding/xml"
    21  	"flag"
    22  	"fmt"
    23  	"regexp"
    24  	"strconv"
    25  	"time"
    26  
    27  	libvirtxml "github.com/libvirt/libvirt-go-xml"
    28  	"k8s.io/api/core/v1"
    29  	"k8s.io/apimachinery/pkg/api/resource"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  )
    32  
    33  var useDHCPNetworkConfig = flag.Bool("use-dhcp-network-config", false, "use DHCP network config instead of Cloud-Init-based one")
    34  
    35  // VMInterface provides API to work with virtlet VM pods
    36  type VMInterface struct {
    37  	controller *Controller
    38  	pod        *PodInterface
    39  
    40  	Name string
    41  	PVCs []*PVCInterface
    42  }
    43  
    44  // VMOptions defines VM parameters
    45  type VMOptions struct {
    46  	// VM image to use.
    47  	Image string
    48  	// Number of virtual CPUs.
    49  	VCPUCount int
    50  	// SSH public key to add to the VM.
    51  	SSHKey string
    52  	// SSH key source to use
    53  	SSHKeySource string
    54  	// Cloud-init userdata script
    55  	CloudInitScript string
    56  	// Disk driver to use
    57  	DiskDriver string
    58  	// VM resource limit specs
    59  	Limits map[string]string
    60  	// Cloud-init userdata
    61  	UserData string
    62  	// Enable overridding the userdata
    63  	OverwriteUserData bool
    64  	// Replaces cloud-init userdata with a script
    65  	UserDataScript string
    66  	// Data source for the userdata
    67  	UserDataSource string
    68  	// The name of the node to run the VM on
    69  	NodeName string
    70  	// Root volume size spec
    71  	RootVolumeSize string
    72  	// "cni" annotation value for CNI-Genie
    73  	MultiCNI string
    74  	// PVCs (with corresponding PVs) to use
    75  	PVCs []PVCSpec
    76  	// ConfigMap or Secret to inject into the rootfs
    77  	InjectFilesToRootfsFrom string
    78  	// SystemUUID to set
    79  	SystemUUID string
    80  }
    81  
    82  func newVMInterface(controller *Controller, name string) *VMInterface {
    83  	return &VMInterface{
    84  		controller: controller,
    85  		Name:       name,
    86  	}
    87  }
    88  
    89  // Pod returns ensures that underlying is started and returns it
    90  func (vmi *VMInterface) Pod() (*PodInterface, error) {
    91  	if vmi.pod == nil {
    92  		pod, err := vmi.controller.Pod(vmi.Name, "")
    93  		if err != nil {
    94  			return nil, err
    95  		}
    96  		vmi.pod = pod
    97  	}
    98  	if vmi.pod == nil {
    99  		return nil, fmt.Errorf("pod %s in namespace %s cannot be found", vmi.Name, vmi.controller.namespace.Name)
   100  	}
   101  	if vmi.pod.Pod.Status.Phase != v1.PodRunning {
   102  		err := vmi.pod.Wait()
   103  		if err != nil {
   104  			return nil, err
   105  		}
   106  	}
   107  	return vmi.pod, nil
   108  }
   109  
   110  // PodWithoutChecks returns the underlying pods without performing any
   111  // checks
   112  func (vmi *VMInterface) PodWithoutChecks() *PodInterface {
   113  	return vmi.pod
   114  }
   115  
   116  // Create creates a new VM pod
   117  func (vmi *VMInterface) Create(options VMOptions, beforeCreate func(*PodInterface)) error {
   118  	pod := newPodInterface(vmi.controller, vmi.buildVMPod(options))
   119  	for _, pvcSpec := range options.PVCs {
   120  		pvc := newPersistentVolumeClaim(vmi.controller, pvcSpec)
   121  		if err := pvc.Create(); err != nil {
   122  			return err
   123  		}
   124  		pvc.AddToPod(pod, pvcSpec.Name)
   125  		vmi.PVCs = append(vmi.PVCs, pvc)
   126  	}
   127  	if beforeCreate != nil {
   128  		beforeCreate(pod)
   129  	}
   130  	if err := pod.Create(); err != nil {
   131  		return err
   132  	}
   133  	vmi.pod = pod
   134  	return nil
   135  }
   136  
   137  // CreateAndWait creates a new VM pod in k8s and waits for it to start
   138  func (vmi *VMInterface) CreateAndWait(options VMOptions, waitTimeout time.Duration, beforeCreate func(*PodInterface)) error {
   139  	err := vmi.Create(options, beforeCreate)
   140  	if err == nil {
   141  		err = vmi.pod.Wait(waitTimeout)
   142  	}
   143  	return err
   144  }
   145  
   146  // Delete deletes VM pod and waits for it to disappear from k8s
   147  func (vmi *VMInterface) Delete(waitTimeout time.Duration) error {
   148  	if vmi.pod == nil {
   149  		return nil
   150  	}
   151  	if err := vmi.pod.Delete(); err != nil {
   152  		return err
   153  	}
   154  	if err := vmi.pod.WaitForDestruction(waitTimeout); err != nil {
   155  		return err
   156  	}
   157  	for _, pvc := range vmi.PVCs {
   158  		if err := pvc.Delete(); err != nil {
   159  			return err
   160  		}
   161  		if err := pvc.WaitForDestruction(); err != nil {
   162  			return err
   163  		}
   164  	}
   165  	return nil
   166  }
   167  
   168  // VirtletPod returns pod in which virtlet instance, responsible for this VM is located
   169  // (i.e. kube-system:virtlet-xxx pod on the same node)
   170  func (vmi *VMInterface) VirtletPod() (*PodInterface, error) {
   171  	vmPod, err := vmi.Pod()
   172  	if err != nil {
   173  		return nil, err
   174  	}
   175  
   176  	node := vmPod.Pod.Spec.NodeName
   177  	pod, err := vmi.controller.FindPod("kube-system", map[string]string{"runtime": "virtlet"},
   178  		func(pod *PodInterface) bool {
   179  			return pod.Pod.Spec.NodeName == node
   180  		},
   181  	)
   182  	if err != nil {
   183  		return nil, err
   184  	} else if pod == nil {
   185  		return nil, fmt.Errorf("cannot find virtlet pod on node %s", node)
   186  	}
   187  	return pod, nil
   188  }
   189  
   190  func (vmi *VMInterface) buildVMPod(options VMOptions) *v1.Pod {
   191  	annotations := map[string]string{
   192  		"kubernetes.io/target-runtime":      "virtlet.cloud",
   193  		"VirtletDiskDriver":                 options.DiskDriver,
   194  		"VirtletCloudInitUserDataOverwrite": strconv.FormatBool(options.OverwriteUserData),
   195  	}
   196  	if *useDHCPNetworkConfig {
   197  		annotations["VirtletForceDHCPNetworkConfig"] = "true"
   198  	}
   199  
   200  	if options.SSHKey != "" {
   201  		annotations["VirtletSSHKeys"] = options.SSHKey
   202  	}
   203  	if options.SSHKeySource != "" {
   204  		annotations["VirtletSSHKeySource"] = options.SSHKeySource
   205  	}
   206  	if options.UserData != "" {
   207  		annotations["VirtletCloudInitUserData"] = options.UserData
   208  	}
   209  	if options.UserDataSource != "" {
   210  		annotations["VirtletCloudInitUserDataSource"] = options.UserDataSource
   211  	}
   212  	if options.UserDataScript != "" {
   213  		annotations["VirtletCloudInitUserDataScript"] = options.UserDataScript
   214  	}
   215  	if options.VCPUCount > 0 {
   216  		annotations["VirtletVCPUCount"] = strconv.Itoa(options.VCPUCount)
   217  	}
   218  	if options.RootVolumeSize != "" {
   219  		annotations["VirtletRootVolumeSize"] = options.RootVolumeSize
   220  	}
   221  	if options.MultiCNI != "" {
   222  		annotations["cni"] = options.MultiCNI
   223  	}
   224  	if options.InjectFilesToRootfsFrom != "" {
   225  		annotations["VirtletFilesFromDataSource"] = options.InjectFilesToRootfsFrom
   226  	}
   227  	if options.SystemUUID != "" {
   228  		annotations["VirtletSystemUUID"] = options.SystemUUID
   229  	}
   230  
   231  	limits := v1.ResourceList{}
   232  	for k, v := range options.Limits {
   233  		limits[v1.ResourceName(k)] = resource.MustParse(v)
   234  	}
   235  
   236  	var nodeMatch v1.NodeSelectorRequirement
   237  	if options.NodeName == "" {
   238  		nodeMatch = v1.NodeSelectorRequirement{
   239  			Key:      "extraRuntime",
   240  			Operator: "In",
   241  			Values:   []string{"virtlet"},
   242  		}
   243  	} else {
   244  		nodeMatch = v1.NodeSelectorRequirement{
   245  			Key:      "kubernetes.io/hostname",
   246  			Operator: "In",
   247  			Values:   []string{options.NodeName},
   248  		}
   249  	}
   250  
   251  	return &v1.Pod{
   252  		ObjectMeta: metav1.ObjectMeta{
   253  			Name:        vmi.Name,
   254  			Namespace:   vmi.controller.namespace.Name,
   255  			Annotations: annotations,
   256  		},
   257  		Spec: v1.PodSpec{
   258  			Affinity: &v1.Affinity{
   259  				NodeAffinity: &v1.NodeAffinity{
   260  					RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{
   261  						NodeSelectorTerms: []v1.NodeSelectorTerm{
   262  							{
   263  								MatchExpressions: []v1.NodeSelectorRequirement{
   264  									nodeMatch,
   265  								},
   266  							},
   267  						},
   268  					},
   269  				},
   270  			},
   271  			Containers: []v1.Container{
   272  				{
   273  					Name:  vmi.Name,
   274  					Image: "virtlet.cloud/" + options.Image,
   275  					Resources: v1.ResourceRequirements{
   276  						Limits: limits,
   277  					},
   278  					ImagePullPolicy: v1.PullIfNotPresent,
   279  					Stdin:           true,
   280  					TTY:             true,
   281  				},
   282  			},
   283  		},
   284  	}
   285  }
   286  
   287  // SSH returns SSH executor that can run commands in VM
   288  func (vmi *VMInterface) SSH(user, secret string) (Executor, error) {
   289  	return newSSHInterface(vmi, user, secret)
   290  }
   291  
   292  // DomainName returns libvirt domain name the VM
   293  func (vmi *VMInterface) DomainName() (string, error) {
   294  	pod, err := vmi.Pod()
   295  	if err != nil {
   296  		return "", err
   297  	}
   298  	if len(pod.Pod.Status.ContainerStatuses) != 1 {
   299  		return "", fmt.Errorf("expected single container status, but got %d statuses", len(pod.Pod.Status.ContainerStatuses))
   300  	}
   301  	containerID := pod.Pod.Status.ContainerStatuses[0].ContainerID
   302  	match := regexp.MustCompile("__(.+)$").FindStringSubmatch(containerID)
   303  	if len(match) < 2 {
   304  		return "", fmt.Errorf("invalid container ID %q", containerID)
   305  	}
   306  	return fmt.Sprintf("virtlet-%s-%s", match[1][:13], pod.Pod.Status.ContainerStatuses[0].Name), nil
   307  }
   308  
   309  // VirshCommand runs virsh command in the virtlet pod, responsible for this VM
   310  // Domain name is automatically substituted into commandline in place of `<domain>`
   311  func (vmi *VMInterface) VirshCommand(command ...string) (string, error) {
   312  	virtletPod, err := vmi.VirtletPod()
   313  	if err != nil {
   314  		return "", err
   315  	}
   316  	for i, c := range command {
   317  		switch c {
   318  		case "<domain>":
   319  			domainName, err := vmi.DomainName()
   320  			if err != nil {
   321  				return "", err
   322  			}
   323  			command[i] = domainName
   324  		}
   325  	}
   326  	return RunVirsh(virtletPod, command...)
   327  }
   328  
   329  // Domain returns libvirt domain definition for the VM
   330  func (vmi *VMInterface) Domain() (libvirtxml.Domain, error) {
   331  	domainXML, err := vmi.VirshCommand("dumpxml", "<domain>")
   332  	if err != nil {
   333  		return libvirtxml.Domain{}, err
   334  	}
   335  	var domain libvirtxml.Domain
   336  	err = xml.Unmarshal([]byte(domainXML), &domain)
   337  	return domain, err
   338  }
   339  
   340  // RunVirsh runs virsh command in the given virtlet pod
   341  func RunVirsh(virtletPod *PodInterface, command ...string) (string, error) {
   342  	container, err := virtletPod.Container("virtlet")
   343  	if err != nil {
   344  		return "", err
   345  	}
   346  	cmd := append([]string{"virsh"}, command...)
   347  	return RunSimple(container, cmd...)
   348  }