github.com/mirantis/virtlet@v1.5.2-0.20191204181327-1659b8a48e9b/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 }