fixtures.go (823 lines of code) (raw):
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package imagetest
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"strings"
daisy "github.com/GoogleCloudPlatform/compute-daisy"
"github.com/google/uuid"
computeBeta "google.golang.org/api/compute/v0.beta"
"google.golang.org/api/compute/v1"
)
const (
waitForVMQuotaStepName = "wait-for-vm-quota"
createVMsStepName = "create-vms"
createDisksStepName = "create-disks"
waitForDisksQuotaStepName = "wait-for-disk-quota"
createNetworkStepName = "create-networks"
createFirewallStepName = "create-firewalls"
createSubnetworkStepName = "create-sub-networks"
successMatch = "FINISHED-TEST"
// ShouldRebootDuringTest is a local map key to indicate that the
// test will reboot and relies on results from the second boot.
ShouldRebootDuringTest = "shouldRebootDuringTest"
// DefaultSourceRange is the RFC-1918 range used in default rules.
DefaultSourceRange = "10.128.0.0/9"
// DefaultMTU is the default MTU set for a network.
DefaultMTU = 1460
// JumboFramesMTU is the maximum MTU settable for a network.
JumboFramesMTU = 8896
// DefaultMachineType is the default machine type when machine type isn't specified.
DefaultMachineType = "n1-standard-1"
)
// TestVM is a test VM.
type TestVM struct {
name string
testWorkflow *TestWorkflow
// The underlying instance running the test. Exactly one of these must be non-nil.
instance *daisy.Instance
instancebeta *daisy.InstanceBeta
}
// AddUser add user public key to metadata ssh-keys.
func (t *TestVM) AddUser(user, publicKey string) {
keyline := fmt.Sprintf("%s:%s", user, publicKey)
if t.instance != nil {
if keys, ok := t.instance.Metadata["ssh-keys"]; ok {
keyline = fmt.Sprintf("%s\n%s", keys, keyline)
}
} else if t.instancebeta != nil {
if keys, ok := t.instancebeta.Metadata["ssh-keys"]; ok {
keyline = fmt.Sprintf("%s\n%s", keys, keyline)
}
}
t.AddMetadata("ssh-keys", keyline)
}
// Skip marks a test workflow to be skipped.
func (t *TestWorkflow) Skip(message string) {
t.skipped = true
t.skippedMessage = message
}
// SkippedMessage returns the skip reason message for the workflow.
func (t *TestWorkflow) SkippedMessage() string {
return t.skippedMessage
}
// LockProject indicates this test modifies project-level data and must have
// exclusive use of the project.
func (t *TestWorkflow) LockProject() {
t.lockProject = true
}
// WaitForVMQuota appends a list of quotas to the wait for vm quota step. Quotas with a blank region will be populated with the region corresponding to the workflow zone.
func (t *TestWorkflow) WaitForVMQuota(qa *daisy.QuotaAvailable) error {
return t.waitForQuotaStep(qa, waitForVMQuotaStepName)
}
// WaitForDisksQuota appends a list of quotas to the wait for disk quota step. Quotas with a blank region will be populated with the region corresponding to the workflow zone.
func (t *TestWorkflow) WaitForDisksQuota(qa *daisy.QuotaAvailable) error {
return t.waitForQuotaStep(qa, waitForDisksQuotaStepName)
}
func (t *TestWorkflow) waitForQuotaStep(qa *daisy.QuotaAvailable, stepname string) error {
step, ok := t.wf.Steps[stepname]
if !ok {
var err error
step, err = t.wf.NewStep(stepname)
if err != nil {
return err
}
step.WaitForAvailableQuotas = &daisy.WaitForAvailableQuotas{Interval: "90s"}
}
// If the step is already waiting for this quota, add the number of units to
// the existing quota.
for _, q := range step.WaitForAvailableQuotas.Quotas {
if q.Metric == qa.Metric && q.Region == qa.Region {
q.Units = q.Units + qa.Units
return nil
}
}
step.WaitForAvailableQuotas.Quotas = append(step.WaitForAvailableQuotas.Quotas, qa)
return nil
}
// CreateTestVM adds the necessary steps to create a VM with the specified
// name to the workflow.
func (t *TestWorkflow) CreateTestVM(name string) (*TestVM, error) {
parts := strings.Split(name, ".")
vmname := strings.ReplaceAll(parts[0], "_", "-")
bootDisk := &compute.Disk{Name: vmname}
createDisksStep, err := t.appendCreateDisksStep(bootDisk)
if err != nil {
return nil, err
}
daisyInst := &daisy.Instance{}
// createDisksStep doesn't depend on any other steps.
createVMStep, i, err := t.appendCreateVMStep([]*compute.Disk{bootDisk}, daisyInst)
if err != nil {
return nil, err
}
if err := t.wf.AddDependency(createVMStep, createDisksStep); err != nil {
return nil, err
}
waitStep, err := t.addWaitStep(vmname, vmname)
if err != nil {
return nil, err
}
if err := t.wf.AddDependency(waitStep, createVMStep); err != nil {
return nil, err
}
if createSubnetworkStep, ok := t.wf.Steps[createSubnetworkStepName]; ok {
if err := t.wf.AddDependency(createVMStep, createSubnetworkStep); err != nil {
return nil, err
}
}
if createNetworkStep, ok := t.wf.Steps[createNetworkStepName]; ok {
if err := t.wf.AddDependency(createVMStep, createNetworkStep); err != nil {
return nil, err
}
}
return &TestVM{name: vmname, testWorkflow: t, instance: i}, nil
}
// CreateTestVMBeta adds the necessary steps to create a VM with the specified
// name from the compute beta API to the workflow.
func (t *TestWorkflow) CreateTestVMBeta(name string) (*TestVM, error) {
parts := strings.Split(name, ".")
vmname := strings.ReplaceAll(parts[0], "_", "-")
bootDisk := &compute.Disk{Name: vmname}
createDisksStep, err := t.appendCreateDisksStep(bootDisk)
if err != nil {
return nil, err
}
daisyInst := &daisy.InstanceBeta{}
// createDisksStep doesn't depend on any other steps.
createVMStep, i, err := t.appendCreateVMStepBeta([]*compute.Disk{bootDisk}, daisyInst)
if err != nil {
return nil, err
}
if err := t.wf.AddDependency(createVMStep, createDisksStep); err != nil {
return nil, err
}
waitStep, err := t.addWaitStep(vmname, vmname)
if err != nil {
return nil, err
}
if err := t.wf.AddDependency(waitStep, createVMStep); err != nil {
return nil, err
}
if createSubnetworkStep, ok := t.wf.Steps[createSubnetworkStepName]; ok {
if err := t.wf.AddDependency(createVMStep, createSubnetworkStep); err != nil {
return nil, err
}
}
if createNetworkStep, ok := t.wf.Steps[createNetworkStepName]; ok {
if err := t.wf.AddDependency(createVMStep, createNetworkStep); err != nil {
return nil, err
}
}
return &TestVM{name: vmname, testWorkflow: t, instancebeta: i}, nil
}
// CreateTestVMMultipleDisks adds the necessary steps to create a VM with the specified
// name to the workflow.
func (t *TestWorkflow) CreateTestVMMultipleDisks(disks []*compute.Disk, instanceParams *daisy.Instance) (*TestVM, error) {
if len(disks) == 0 || disks[0].Name == "" {
return nil, fmt.Errorf("failed to create multiple disk VM with empty boot disk")
}
name := disks[0].Name
parts := strings.Split(name, ".")
vmname := strings.ReplaceAll(parts[0], "_", "-")
createDisksSteps := make([]*daisy.Step, len(disks))
for i, disk := range disks {
// the disk creation steps are slightly different for the boot disk and mount disks
var createDisksStep *daisy.Step
var err error
if i == 0 {
createDisksStep, err = t.appendCreateDisksStep(disk)
} else {
createDisksStep, err = t.appendCreateMountDisksStep(disk)
}
if err != nil {
return nil, err
}
createDisksSteps[i] = createDisksStep
}
var daisyInst *daisy.Instance
if instanceParams == nil {
daisyInst = &daisy.Instance{}
} else {
daisyInst = instanceParams
}
// createDisksStep doesn't depend on any other steps.
createVMStep, i, err := t.appendCreateVMStep(disks, daisyInst)
if err != nil {
return nil, err
}
for _, createDisksStep := range createDisksSteps {
if err := t.wf.AddDependency(createVMStep, createDisksStep); err != nil {
return nil, err
}
}
// In a follow-up, guest attribute support will be added.
// If this is the first boot before a reboot, this should use a
// different guest attribute when waiting for the instance signal.
var waitStep *daisy.Step
if _, foundKey := daisyInst.Metadata[ShouldRebootDuringTest]; foundKey {
waitStep, err = t.addWaitRebootGAStep(vmname, vmname)
} else {
waitStep, err = t.addWaitStep(vmname, vmname)
}
if err != nil {
return nil, err
}
if err := t.wf.AddDependency(waitStep, createVMStep); err != nil {
return nil, err
}
if createSubnetworkStep, ok := t.wf.Steps[createSubnetworkStepName]; ok {
if err := t.wf.AddDependency(createVMStep, createSubnetworkStep); err != nil {
return nil, err
}
}
if createNetworkStep, ok := t.wf.Steps[createNetworkStepName]; ok {
if err := t.wf.AddDependency(createVMStep, createNetworkStep); err != nil {
return nil, err
}
}
return &TestVM{name: vmname, testWorkflow: t, instance: i}, nil
}
// CreateTestVMFromInstanceBeta creates a test vm struct to run CIT suites on from
// the given daisy instancebeta and adds it to the test workflow.
func (t *TestWorkflow) CreateTestVMFromInstanceBeta(i *daisy.InstanceBeta, disks []*compute.Disk) (*TestVM, error) {
if len(disks) == 0 || disks[0].Name == "" {
return nil, fmt.Errorf("failed to create multiple disk VM with empty boot disk")
}
name := disks[0].Name
parts := strings.Split(name, ".")
vmname := strings.ReplaceAll(parts[0], "_", "-")
createDisksSteps := make([]*daisy.Step, len(disks))
for i, disk := range disks {
// the disk creation steps are slightly different for the boot disk and mount disks
var createDisksStep *daisy.Step
var err error
if i == 0 {
createDisksStep, err = t.appendCreateDisksStep(disk)
} else {
createDisksStep, err = t.appendCreateMountDisksStep(disk)
}
if err != nil {
return nil, err
}
createDisksSteps[i] = createDisksStep
}
// createDisksStep doesn't depend on any other steps.
createVMStep, i, err := t.appendCreateVMStepBeta(disks, i)
if err != nil {
return nil, err
}
for _, createDisksStep := range createDisksSteps {
if err := t.wf.AddDependency(createVMStep, createDisksStep); err != nil {
return nil, err
}
}
var waitStep *daisy.Step
if _, foundKey := i.Metadata[ShouldRebootDuringTest]; foundKey {
waitStep, err = t.addWaitRebootGAStep(vmname, vmname)
} else {
waitStep, err = t.addWaitStep(vmname, vmname)
}
if err != nil {
return nil, err
}
if err := t.wf.AddDependency(waitStep, createVMStep); err != nil {
return nil, err
}
if createSubnetworkStep, ok := t.wf.Steps[createSubnetworkStepName]; ok {
if err := t.wf.AddDependency(createVMStep, createSubnetworkStep); err != nil {
return nil, err
}
}
if createNetworkStep, ok := t.wf.Steps[createNetworkStepName]; ok {
if err := t.wf.AddDependency(createVMStep, createNetworkStep); err != nil {
return nil, err
}
}
return &TestVM{name: vmname, testWorkflow: t, instancebeta: i}, nil
}
// CreateDerivativeVM creates a derivative image using the sourceVM's boot disk
// as the source and adds it to the test workflow. The new VM is named using the
// provided name, with a "derivative-" prefix.
func (t *TestVM) CreateDerivativeVM(name string) (*TestVM, error) {
t.testWorkflow.counter++
stepSuffix := fmt.Sprintf("%s-%d", t.name, t.testWorkflow.counter)
vmname := fmt.Sprintf("derivative-%s", name)
// Stop the source VM.
lastStep, err := t.testWorkflow.getLastStepForVM(t.name)
if err != nil {
return nil, fmt.Errorf("failed to resolve last step")
}
stopStep, err := t.testWorkflow.addStopStep(stepSuffix, t.name)
if err != nil {
return nil, err
}
if err := t.testWorkflow.wf.AddDependency(stopStep, lastStep); err != nil {
return nil, err
}
// Wait for the source VM to stop.
waitStopStep, err := t.testWorkflow.addWaitStoppedStep("stopped-"+stepSuffix, t.name)
if err != nil {
return nil, err
}
if err := t.testWorkflow.wf.AddDependency(waitStopStep, stopStep); err != nil {
return nil, err
}
// Now detach the boot disk from the source VM.
detachStepName := fmt.Sprintf("detach-disk-%s", stepSuffix)
detachDiskStep, err := t.testWorkflow.appendDetachDiskStep(detachStepName, t.name, []string{t.name})
if err != nil {
return nil, err
}
if err := t.testWorkflow.wf.AddDependency(detachDiskStep, waitStopStep); err != nil {
return nil, err
}
// Create a new instance using the original boot disk.
daisyInst := &daisy.Instance{}
bootDisk := &compute.Disk{Name: t.name}
createVMStep, i, err := t.testWorkflow.addNewVMStep([]*compute.Disk{bootDisk}, daisyInst)
if err != nil {
return nil, err
}
if err := t.testWorkflow.wf.AddDependency(createVMStep, detachDiskStep); err != nil {
return nil, err
}
// Override the name with the derivative name.
i.Name = vmname
// Add wait step for the new VM.
waitStep, err := t.testWorkflow.addWaitStep(vmname, vmname)
if err != nil {
return nil, err
}
if err := t.testWorkflow.wf.AddDependency(waitStep, createVMStep); err != nil {
return nil, err
}
return &TestVM{name: vmname, testWorkflow: t.testWorkflow, instance: i}, nil
}
// AddDisk adds a given number of disks to the VM.
func (t *TestVM) AddDisk(diskType string, initializeParams *compute.AttachedDiskInitializeParams) error {
// Empty string defaults to PERSISTENT.
if diskType != "SCRATCH" && diskType != "PERSISTENT" && diskType != "" {
return fmt.Errorf("disk type %s not one of SCRATCH or PERSISTENT", diskType)
}
t.instance.Disks = append(t.instance.Disks, &compute.AttachedDisk{
AutoDelete: true,
Boot: false,
DeviceName: fmt.Sprintf("custom-disk-%d", len(t.instance.Disks)),
InitializeParams: initializeParams,
Mode: "READ_WRITE",
Type: diskType,
})
return nil
}
// AddMetadata adds the specified key:value pair to metadata during VM creation.
func (t *TestVM) AddMetadata(key, value string) {
if t.instance != nil {
if t.instance.Metadata == nil {
t.instance.Metadata = make(map[string]string)
}
t.instance.Metadata[key] = value
} else if t.instancebeta != nil {
if t.instancebeta.Metadata == nil {
t.instancebeta.Metadata = make(map[string]string)
}
t.instancebeta.Metadata[key] = value
}
}
// AddScope adds the specified auth scope to the service account on the VM.
func (t *TestVM) AddScope(scope string) {
if t.instance != nil {
t.instance.Scopes = append(t.instance.Scopes, scope)
} else if t.instancebeta != nil {
t.instancebeta.Scopes = append(t.instancebeta.Scopes, scope)
}
}
// RunTests runs only the named tests on the testVM.
//
// From go help test:
//
// -run regexp
// Run only those tests and examples matching the regular expression.
// For tests, the regular expression is split by unbracketed slash (/)
// characters into a sequence of regular expressions, and each part
// of a test's identifier must match the corresponding element in
// the sequence, if any. Note that possible parents of matches are
// run too, so that -run=X/Y matches and runs and reports the result
// of all tests matching X, even those without sub-tests matching Y,
// because it must run them to look for those sub-tests.
func (t *TestVM) RunTests(runtest string) {
t.AddMetadata("_test_run", runtest)
}
// SetShutdownScript sets the `shutdown-script` metadata key for a non-Windows VM.
func (t *TestVM) SetShutdownScript(script string) {
t.AddMetadata("shutdown-script", script)
}
// SetWindowsShutdownScript sets the `windows-shutdown-script-ps1` metadata key for a Windows VM.
func (t *TestVM) SetWindowsShutdownScript(script string) {
t.AddMetadata("windows-shutdown-script-ps1", script)
}
// SetShutdownScriptURL sets the`shutdown-script-url` metadata key for a non-Windows VM.
func (t *TestVM) SetShutdownScriptURL(script string) error {
fileName := fmt.Sprintf("/tmp/shutdown_script-%s", uuid.New())
if err := ioutil.WriteFile(fileName, []byte(script), 0755); err != nil {
return err
}
t.testWorkflow.wf.Sources["shutdown-script"] = fileName
t.AddMetadata("shutdown-script-url", "${SOURCESPATH}/shutdown-script")
return nil
}
// SetWindowsShutdownScriptURL sets the`windows-shutdown-script-url` metadata key for a Windows VM.
func (t *TestVM) SetWindowsShutdownScriptURL(script string) error {
fileName := fmt.Sprintf("/tmp/shutdown_script-%s.ps1", uuid.New())
if err := ioutil.WriteFile(fileName, []byte(script), 0755); err != nil {
return err
}
t.testWorkflow.wf.Sources["shutdown-script.ps1"] = fileName
t.AddMetadata("windows-shutdown-script-url", "${SOURCESPATH}/shutdown-script.ps1")
return nil
}
// SetStartupScript sets the `startup-script` metadata key for a VM. On Windows VMs, this script does not run on startup: different guest attributes must be set.
func (t *TestVM) SetStartupScript(script string) {
t.AddMetadata("startup-script", script)
}
// SetWindowsStartupScript sets the `windows-startup-script-ps1` metadata key for a VM.
func (t *TestVM) SetWindowsStartupScript(script string) {
t.AddMetadata("windows-startup-script-ps1", script)
}
// SetNetworkPerformanceTier sets the performance tier of the VM.
// The tier must be one of "DEFAULT" or "TIER_1"
func (t *TestVM) SetNetworkPerformanceTier(tier string) error {
if tier != "DEFAULT" && tier != "TIER_1" {
return fmt.Errorf("Error: %v not one of DEFAULT or TIER_1", tier)
}
if t.instance != nil {
if t.instance.NetworkPerformanceConfig == nil {
t.instance.NetworkPerformanceConfig = &compute.NetworkPerformanceConfig{TotalEgressBandwidthTier: tier}
} else {
t.instance.NetworkPerformanceConfig.TotalEgressBandwidthTier = tier
}
} else if t.instancebeta != nil {
if t.instancebeta.NetworkPerformanceConfig == nil {
t.instancebeta.NetworkPerformanceConfig = &computeBeta.NetworkPerformanceConfig{TotalEgressBandwidthTier: tier}
} else {
t.instancebeta.NetworkPerformanceConfig.TotalEgressBandwidthTier = tier
}
}
return nil
}
// Reboot stops the VM, waits for it to shutdown, then starts it again. Your
// test package must handle being run twice.
func (t *TestVM) Reboot() error {
// TODO: better solution than a shared counter for name collisions.
t.testWorkflow.counter++
stepSuffix := fmt.Sprintf("%s-%d", t.name, t.testWorkflow.counter)
lastStep, err := t.testWorkflow.getLastStepForVM(t.name)
if err != nil {
return fmt.Errorf("failed resolve last step")
}
stopInstancesStep, err := t.testWorkflow.addStopStep(stepSuffix, t.name)
if err != nil {
return err
}
if err := t.testWorkflow.wf.AddDependency(stopInstancesStep, lastStep); err != nil {
return err
}
waitStopStep, err := t.testWorkflow.addWaitStoppedStep("stopped-"+stepSuffix, t.name)
if err != nil {
return err
}
if err := t.testWorkflow.wf.AddDependency(waitStopStep, stopInstancesStep); err != nil {
return err
}
startInstancesStep, err := t.testWorkflow.addStartStep(stepSuffix, t.name)
if err != nil {
return err
}
if err := t.testWorkflow.wf.AddDependency(startInstancesStep, waitStopStep); err != nil {
return err
}
waitStartedStep, err := t.testWorkflow.addWaitStep("started-"+stepSuffix, t.name)
if err != nil {
return err
}
if err := t.testWorkflow.wf.AddDependency(waitStartedStep, startInstancesStep); err != nil {
return err
}
return nil
}
// Resume waits for the vm to be SUSPENDED, then resumes it. It does not handle suspension.
func (t *TestVM) Resume() error {
// TODO: better solution than a shared counter for name collisions.
t.testWorkflow.counter++
stepSuffix := fmt.Sprintf("%s-%d", t.name, t.testWorkflow.counter)
lastStep, err := t.testWorkflow.getLastStepForVM(t.name)
if err != nil {
return fmt.Errorf("failed resolve last step")
}
waitSuspended, err := t.testWorkflow.wf.NewStep(fmt.Sprintf("wait-suspended-%s", stepSuffix))
if err != nil {
return err
}
waitSuspended.WaitForInstancesSignal = &daisy.WaitForInstancesSignal{
{Name: t.name, Status: []string{"SUSPENDED"}},
}
createStep := t.testWorkflow.wf.Steps[createVMsStepName]
if err := t.testWorkflow.wf.AddDependency(waitSuspended, createStep); err != nil {
return err
}
resume, err := t.testWorkflow.wf.NewStep(fmt.Sprintf("resume-%s", stepSuffix))
if err != nil {
return err
}
resume.Resume = &daisy.Resume{
Instance: t.name,
}
if err := t.testWorkflow.wf.AddDependency(resume, waitSuspended); err != nil {
return err
}
if err := t.testWorkflow.wf.AddDependency(lastStep, resume); err != nil {
return err
}
return nil
}
// ResizeDiskAndReboot resize the disk of the current test VMs and reboot
func (t *TestVM) ResizeDiskAndReboot(diskSize int) error {
t.testWorkflow.counter++
stepSuffix := fmt.Sprintf("%s-%d", t.name, t.testWorkflow.counter)
lastStep, err := t.testWorkflow.getLastStepForVM(t.name)
if err != nil {
return fmt.Errorf("failed resolve last step")
}
diskResizeStep, err := t.testWorkflow.addDiskResizeStep(stepSuffix, t.name, diskSize)
if err != nil {
return err
}
if err := t.testWorkflow.wf.AddDependency(diskResizeStep, lastStep); err != nil {
return err
}
return t.Reboot()
}
// ForceMachineType sets the machine type for the test VM. This will override
// the machine_type flag in the CIT wrapper, and should only be used when a
// test absolutely requires a specific machine shape.
func (t *TestVM) ForceMachineType(machinetype string) {
if t.instance != nil {
t.instance.MachineType = machinetype
} else if t.instancebeta != nil {
t.instancebeta.MachineType = machinetype
}
}
// ForceZone sets the zone for the test vm. This will override the zone option
// from the CIT wrapper and and should only be used when a test requires a specific
// zone.
func (t *TestVM) ForceZone(z string) {
if t.instance != nil {
t.instance.Zone = z
} else if t.instancebeta != nil {
t.instancebeta.Zone = z
}
}
// EnableSecureBoot make the current test VMs in workflow with secure boot.
func (t *TestVM) EnableSecureBoot() {
if t.instance != nil {
if t.instance.ShieldedInstanceConfig == nil {
t.instance.ShieldedInstanceConfig = &compute.ShieldedInstanceConfig{}
}
t.instance.ShieldedInstanceConfig.EnableSecureBoot = true
} else if t.instancebeta != nil {
if t.instancebeta.ShieldedInstanceConfig == nil {
t.instancebeta.ShieldedInstanceConfig = &computeBeta.ShieldedInstanceConfig{}
}
t.instancebeta.ShieldedInstanceConfig.EnableSecureBoot = true
}
}
// EnableConfidentialInstance enabled CVM features for the instance.
func (t *TestVM) EnableConfidentialInstance() {
if t.instance != nil {
if t.instance.ConfidentialInstanceConfig == nil {
t.instance.ConfidentialInstanceConfig = &compute.ConfidentialInstanceConfig{}
}
t.instance.ConfidentialInstanceConfig.EnableConfidentialCompute = true
if t.instance.Scheduling == nil {
t.instance.Scheduling = &compute.Scheduling{}
}
t.instance.Scheduling.OnHostMaintenance = "TERMINATE"
} else if t.instancebeta != nil {
if t.instancebeta.ConfidentialInstanceConfig == nil {
t.instancebeta.ConfidentialInstanceConfig = &computeBeta.ConfidentialInstanceConfig{}
}
t.instancebeta.ConfidentialInstanceConfig.EnableConfidentialCompute = true
if t.instancebeta.Scheduling == nil {
t.instancebeta.Scheduling = &computeBeta.Scheduling{}
}
t.instancebeta.Scheduling.OnHostMaintenance = "TERMINATE"
}
}
// SetMinCPUPlatform sets the minimum CPU platform of the instance.
func (t *TestVM) SetMinCPUPlatform(minCPUPlatform string) {
if t.instance != nil {
t.instance.MinCpuPlatform = minCPUPlatform
} else if t.instancebeta != nil {
t.instancebeta.MinCpuPlatform = minCPUPlatform
}
}
// UseGVNIC sets the type of vNIC to be used to GVNIC
func (t *TestVM) UseGVNIC() {
if t.instance != nil {
if t.instance.NetworkInterfaces == nil {
t.instance.NetworkInterfaces = []*compute.NetworkInterface{
{
NicType: "GVNIC",
},
}
} else {
t.instance.NetworkInterfaces[0].NicType = "GVNIC"
}
} else if t.instancebeta != nil {
if t.instancebeta.NetworkInterfaces == nil {
t.instancebeta.NetworkInterfaces = []*computeBeta.NetworkInterface{
{
NicType: "GVNIC",
},
}
} else {
t.instancebeta.NetworkInterfaces[0].NicType = "GVNIC"
}
}
}
// AddCustomNetwork add current test VMs in workflow using provided network and
// subnetwork. If subnetwork is empty, not using subnetwork, in this case
// network has to be in auto mode VPC.
func (t *TestVM) AddCustomNetwork(network *Network, subnetwork *Subnetwork) error {
var subnetworkName string
if subnetwork == nil {
subnetworkName = ""
if !*network.network.AutoCreateSubnetworks {
return fmt.Errorf("network %s is not auto mode, subnet is required", network.name)
}
} else {
subnetworkName = subnetwork.name
}
if t.instance != nil {
// Add network config.
networkInterface := compute.NetworkInterface{
Network: network.name,
Subnetwork: subnetworkName,
AccessConfigs: []*compute.AccessConfig{
{
Type: "ONE_TO_ONE_NAT",
},
},
}
if t.instance.NetworkInterfaces == nil {
t.instance.NetworkInterfaces = []*compute.NetworkInterface{&networkInterface}
} else {
t.instance.NetworkInterfaces = append(t.instance.NetworkInterfaces, &networkInterface)
}
} else if t.instancebeta != nil {
// Add network config.
networkInterface := computeBeta.NetworkInterface{
Network: network.name,
Subnetwork: subnetworkName,
AccessConfigs: []*computeBeta.AccessConfig{
{
Type: "ONE_TO_ONE_NAT",
},
},
}
if t.instancebeta.NetworkInterfaces == nil {
t.instancebeta.NetworkInterfaces = []*computeBeta.NetworkInterface{&networkInterface}
} else {
t.instancebeta.NetworkInterfaces = append(t.instancebeta.NetworkInterfaces, &networkInterface)
}
}
return nil
}
// AddAliasIPRanges add alias ip range to current test VMs.
func (t *TestVM) AddAliasIPRanges(aliasIPRange, rangeName string) error {
// TODO: If we haven't set any NetworkInterface struct, does it make sense to support adding alias IPs?
if t.instance != nil {
if t.instance.NetworkInterfaces == nil {
return fmt.Errorf("must call AddCustomNetwork prior to AddAliasIPRanges")
}
t.instance.NetworkInterfaces[0].AliasIpRanges = append(t.instance.NetworkInterfaces[0].AliasIpRanges, &compute.AliasIpRange{
IpCidrRange: aliasIPRange,
SubnetworkRangeName: rangeName,
})
} else if t.instancebeta != nil {
if t.instancebeta.NetworkInterfaces == nil {
return fmt.Errorf("must call AddCustomNetwork prior to AddAliasIPRanges")
}
t.instancebeta.NetworkInterfaces[0].AliasIpRanges = append(t.instancebeta.NetworkInterfaces[0].AliasIpRanges, &computeBeta.AliasIpRange{
IpCidrRange: aliasIPRange,
SubnetworkRangeName: rangeName,
})
}
return nil
}
// SetPrivateIP set IPv4 internal IP address for target network to the current test VMs.
func (t *TestVM) SetPrivateIP(network *Network, networkIP string) error {
if t.instance != nil {
if t.instance.NetworkInterfaces == nil {
return fmt.Errorf("must call AddCustomNetwork prior to AddPrivateIP")
}
for _, nic := range t.instance.NetworkInterfaces {
if nic.Network == network.name {
nic.NetworkIP = networkIP
return nil
}
}
} else if t.instancebeta != nil {
if t.instancebeta.NetworkInterfaces == nil {
return fmt.Errorf("must call AddCustomNetwork prior to AddPrivateIP")
}
for _, nic := range t.instancebeta.NetworkInterfaces {
if nic.Network == network.name {
nic.NetworkIP = networkIP
return nil
}
}
}
return fmt.Errorf("not found network interface %s", network.name)
}
// Network represent network used by vm in setup.go.
type Network struct {
name string
testWorkflow *TestWorkflow
network *daisy.Network
}
// Subnetwork represent subnetwork used by vm in setup.go.
type Subnetwork struct {
name string
testWorkflow *TestWorkflow
subnetwork *daisy.Subnetwork
network *Network
}
// CreateNetworkFromDaisyNetwork creates a network exactly as specified by the daisy network.
// Callers who don't need to set custom fields should call CreateNetwork instead.
func (t *TestWorkflow) CreateNetworkFromDaisyNetwork(net *daisy.Network) (*Network, error) {
createNetworkStep, network, err := t.appendCreateNetworkStep(net)
if err != nil {
return nil, err
}
createVMsStep, ok := t.wf.Steps[createVMsStepName]
if ok {
if err := t.wf.AddDependency(createVMsStep, createNetworkStep); err != nil {
return nil, err
}
}
return &Network{net.Name, t, network}, nil
}
// CreateNetwork creates custom network. Using AddCustomNetwork method provided by
// TestVM to config network on vm
func (t *TestWorkflow) CreateNetwork(networkName string, autoCreateSubnetworks bool) (*Network, error) {
net := &daisy.Network{
Network: compute.Network{
Name: networkName,
Mtu: DefaultMTU,
},
AutoCreateSubnetworks: &autoCreateSubnetworks,
}
createNetworkStep, network, err := t.appendCreateNetworkStep(net)
if err != nil {
return nil, err
}
createVMsStep, ok := t.wf.Steps[createVMsStepName]
if ok {
if err := t.wf.AddDependency(createVMsStep, createNetworkStep); err != nil {
return nil, err
}
}
return &Network{networkName, t, network}, nil
}
// SetMTU sets the MTU of the network. The MTU must be between 1460 and 8896, inclusively.
func (n *Network) SetMTU(mtu int) {
if mtu >= DefaultMTU && mtu <= JumboFramesMTU {
n.network.Mtu = int64(mtu)
}
}
// CreateSubnetwork creates custom subnetwork. Using AddCustomNetwork method
// provided by TestVM to config network on vm
func (n *Network) CreateSubnetwork(name string, ipRange string) (*Subnetwork, error) {
subnetwork := &daisy.Subnetwork{
Subnetwork: compute.Subnetwork{
Name: name,
IpCidrRange: ipRange,
Network: n.name,
},
}
createSubnetworksStep, subnetwork, err := n.testWorkflow.appendCreateSubnetworksStep(subnetwork)
if err != nil {
return nil, err
}
createNetworkStep, ok := n.testWorkflow.wf.Steps[createNetworkStepName]
if !ok {
return nil, fmt.Errorf("create-network step missing")
}
if err := n.testWorkflow.wf.AddDependency(createSubnetworksStep, createNetworkStep); err != nil {
return nil, err
}
return &Subnetwork{name, n.testWorkflow, subnetwork, n}, nil
}
// CreateSubnetworkFromDaisySubnetwork creates custom subnetwork from daisy
// subnetwork. Callers that don't need to specify properties beyond ip ranges,
// region, purpose, and role should call CreateSubnetwork instead.
func (n *Network) CreateSubnetworkFromDaisySubnetwork(subnetwork *daisy.Subnetwork) (*Subnetwork, error) {
subnetwork.Network = n.name
createSubnetworksStep, subnetwork, err := n.testWorkflow.appendCreateSubnetworksStep(subnetwork)
if err != nil {
return nil, err
}
createNetworkStep, ok := n.testWorkflow.wf.Steps[createNetworkStepName]
if !ok {
return nil, fmt.Errorf("create-network step missing")
}
if err := n.testWorkflow.wf.AddDependency(createSubnetworksStep, createNetworkStep); err != nil {
return nil, err
}
return &Subnetwork{subnetwork.Name, n.testWorkflow, subnetwork, n}, nil
}
// SetRegion sets the subnetwork region
func (s *Subnetwork) SetRegion(region string) {
s.subnetwork.Region = region
}
// SetPurpose sets the subnetwork purpose
func (s *Subnetwork) SetPurpose(purpose string) {
s.subnetwork.Purpose = purpose
}
// SetRole sets the subnetwork role
func (s *Subnetwork) SetRole(role string) {
s.subnetwork.Role = role
}
// AddSecondaryRange add secondary IP range to Subnetwork
func (s Subnetwork) AddSecondaryRange(rangeName, ipRange string) {
s.subnetwork.SecondaryIpRanges = append(s.subnetwork.SecondaryIpRanges, &compute.SubnetworkSecondaryRange{
IpCidrRange: ipRange,
RangeName: rangeName,
})
}
func (t *TestWorkflow) appendCreateFirewallStep(firewallName, networkName, protocol string, ports, ranges []string) (*daisy.Step, *daisy.FirewallRule, error) {
if ranges == nil {
ranges = []string{DefaultSourceRange}
}
firewall := &daisy.FirewallRule{
Firewall: compute.Firewall{
Name: firewallName,
Network: networkName,
SourceRanges: ranges,
Allowed: []*compute.FirewallAllowed{
{
IPProtocol: protocol,
Ports: ports,
},
},
},
}
createFirewallRules := &daisy.CreateFirewallRules{}
*createFirewallRules = append(*createFirewallRules, firewall)
createFirewallStep, ok := t.wf.Steps[createFirewallStepName]
if ok {
// append to existing step.
*createFirewallStep.CreateFirewallRules = append(*createFirewallStep.CreateFirewallRules, firewall)
} else {
var err error
createFirewallStep, err = t.wf.NewStep(createFirewallStepName)
if err != nil {
return nil, nil, err
}
createFirewallStep.CreateFirewallRules = createFirewallRules
}
return createFirewallStep, firewall, nil
}
// AddSSHKey generate ssh key pair and return public key.
func (t *TestWorkflow) AddSSHKey(user string) (string, error) {
keyFileName := os.TempDir() + "/id_rsa_" + uuid.New().String()
if _, err := os.Stat(keyFileName); os.IsExist(err) {
os.Remove(keyFileName)
}
commandArgs := []string{"-t", "rsa", "-f", keyFileName, "-N", "", "-q"}
cmd := exec.Command("ssh-keygen", commandArgs...)
if out, err := cmd.Output(); err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
return "", fmt.Errorf("ssh-keygen failed: %s %s %v", out, exitErr.Stderr, err)
}
return "", fmt.Errorf("ssh-keygen failed: %v %v", out, err)
}
publicKey, err := ioutil.ReadFile(keyFileName + ".pub")
if err != nil {
return "", fmt.Errorf("failed to read public key: %v", err)
}
sourcePath := fmt.Sprintf("%s-ssh-key", user)
t.wf.Sources[sourcePath] = keyFileName
return string(publicKey), nil
}
// CreateFirewallRule create firewall rule.
func (n *Network) CreateFirewallRule(firewallName, protocol string, ports, ranges []string) error {
createFirewallStep, _, err := n.testWorkflow.appendCreateFirewallStep(firewallName, n.name, protocol, ports, ranges)
if err != nil {
return err
}
createNetworkStep, ok := n.testWorkflow.wf.Steps[createNetworkStepName]
if ok {
if err := n.testWorkflow.wf.AddDependency(createFirewallStep, createNetworkStep); err != nil {
return err
}
}
createVMsStep, ok := n.testWorkflow.wf.Steps[createVMsStepName]
if ok {
if err := n.testWorkflow.wf.AddDependency(createVMsStep, createFirewallStep); err != nil {
return err
}
}
return nil
}