common/gcp/instance.go (194 lines of code) (raw):
// Copyright 2020 Google Inc. All Rights Reserved.
//
// 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 gcp
import (
"context"
"fmt"
"strings"
"time"
apiBeta "google.golang.org/api/compute/v0.beta"
api "google.golang.org/api/compute/v1"
daisyCompute "github.com/GoogleCloudPlatform/compute-daisy/compute"
)
// Instance is a compute instance.
type Instance struct {
*api.Instance
Client daisyCompute.Client
Project, Zone string
IsWindows bool
}
// InstanceBeta is a compute instance using Beta API.
type InstanceBeta struct {
*apiBeta.Instance
Client daisyCompute.Client
Project, Zone string
IsWindows bool
}
// Cleanup deletes the Instance.
func (i *Instance) Cleanup() error {
return i.Client.DeleteInstance(i.Project, i.Zone, i.Name)
}
// RestartWithScriptCode restarts the instance with given startup script.
func (i *Instance) RestartWithScriptCode(script string) error {
err := i.Client.StopInstance(i.Project, i.Zone, i.Name)
if err != nil {
return err
}
return i.StartWithScriptCode(script, nil)
}
// StartWithScriptCode starts the instance with given startup script and metadata
func (i *Instance) StartWithScriptCode(script string, instanceMetadata map[string]string) error {
if instanceMetadata == nil {
instanceMetadata = make(map[string]string)
}
if i.IsWindows {
instanceMetadata["windows-startup-script-ps1"] = script
} else {
instanceMetadata["startup-script"] = script
}
var metadataItems []*api.MetadataItems
for k, v := range instanceMetadata {
metadataItems = append(metadataItems, BuildInstanceMetadataItem(k, v))
}
err := i.Client.SetInstanceMetadata(i.Project, i.Zone,
i.Name, &api.Metadata{Items: metadataItems, Fingerprint: i.Metadata.Fingerprint})
if err != nil {
return err
}
if err = i.Client.StartInstance(i.Project, i.Zone, i.Name); err != nil {
return err
}
return nil
}
// WaitForSerialOutput waits to a string match on a serial port.
func (i *Instance) WaitForSerialOutput(match string, failureMatches []string, port int64, interval, timeout time.Duration) error {
return WaitForSerialOutput(match, failureMatches, port, interval, timeout, i.Project, i.Zone, i.Name, i.Client)
}
// WaitForSerialOutput waits to a string match on a serial port.
func (i *InstanceBeta) WaitForSerialOutput(match string, failureMatches []string, port int64, interval, timeout time.Duration) error {
return WaitForSerialOutput(match, failureMatches, port, interval, timeout, i.Project, i.Zone, i.Name, i.Client)
}
// WaitForSerialOutput waits to a string match on a serial port.
func WaitForSerialOutput(successMatch string, failureMatches []string, port int64, interval, timeout time.Duration, project, zone, instanceName string, client daisyCompute.Client) error {
var start int64
var errs int
tick := time.Tick(interval)
timedout := time.Tick(timeout)
for {
select {
case <-timedout:
return fmt.Errorf("timed out waiting for %q", successMatch)
case <-tick:
resp, err := client.GetSerialPortOutput(project, zone, instanceName, port, start)
if err != nil {
status, sErr := client.InstanceStatus(project, zone, instanceName)
if sErr != nil {
err = fmt.Errorf("%v, error getting InstanceStatus: %v", err, sErr)
} else {
err = fmt.Errorf("%v, InstanceStatus: %q", err, status)
}
// Wait until machine restarts to evaluate SerialOutput.
if status == "TERMINATED" || status == "STOPPED" || status == "STOPPING" {
continue
}
// Retry up to 3 times in a row on any error if we successfully got InstanceStatus.
if errs < 3 {
errs++
continue
}
return err
}
start = resp.Next
for _, ln := range strings.Split(resp.Contents, "\n") {
if len(failureMatches) > 0 {
for _, failureMatch := range failureMatches {
if i := strings.Index(ln, failureMatch); i != -1 {
errMsg := strings.TrimSpace(ln[i:])
format := "WaitForSerialOutput FailureMatch found for %q: %q"
return fmt.Errorf(format, instanceName, errMsg)
}
}
}
if successMatch != "" {
if i := strings.Index(strings.ToLower(ln), strings.ToLower(successMatch)); i != -1 {
return nil
}
}
}
errs = 0
}
}
}
// SetMetadata sets metadata for the given instance.
func SetMetadata(ctx context.Context, project, zone, name, key, value string, isWindows bool) (*Instance, error) {
i, err := CreateInstanceObject(ctx, project, zone, name, isWindows)
if err != nil {
return nil, err
}
err = i.Client.SetInstanceMetadata(i.Project, i.Zone,
i.Name, &api.Metadata{Items: []*api.MetadataItems{BuildInstanceMetadataItem(
key, value)},
Fingerprint: i.Metadata.Fingerprint})
return i, err
}
// CreateInstanceObject creates an instance object to be operated by GA API client
func CreateInstanceObject(ctx context.Context, project string, zone string, name string, isWindows bool) (*Instance, error) {
client, err := daisyCompute.NewClient(ctx)
if err != nil {
return nil, err
}
var apiInstance *api.Instance
apiInstance, err = client.GetInstance(project, zone, name)
return &Instance{apiInstance, client, project, zone, isWindows}, err
}
// CreateInstanceBetaObject creates an instance object to be operated by Beta API client
func CreateInstanceBetaObject(ctx context.Context, project string, zone string, name string, isWindows bool) (*InstanceBeta, error) {
client, err := daisyCompute.NewClient(ctx)
if err != nil {
return nil, err
}
var apiInstance *apiBeta.Instance
apiInstance, err = client.GetInstanceBeta(project, zone, name)
return &InstanceBeta{apiInstance, client, project, zone, isWindows}, err
}
// CreateMachineImageObject creates a machine image object
func CreateMachineImageObject(ctx context.Context, project string, name string) (*api.MachineImage, error) {
client, err := daisyCompute.NewClient(ctx)
if err != nil {
return nil, err
}
return client.GetMachineImage(project, name)
}
// CreateInstanceBeta creates a VM instance (not just an object representing an existing VM) using Beta API
func CreateInstanceBeta(ctx context.Context, project string, zone string, name string,
isWindows bool, machineImageName string) (*InstanceBeta, error) {
client, err := daisyCompute.NewClient(ctx)
if err != nil {
return nil, err
}
apiBetaInstance := &apiBeta.Instance{
SourceMachineImage: fmt.Sprintf("projects/%s/global/machineImages/%s", project, machineImageName),
Name: name,
Zone: zone,
}
i := &InstanceBeta{apiBetaInstance, client, project, zone, isWindows}
if err := client.CreateInstanceBeta(i.Project, i.Zone, i.Instance); err != nil {
return i, err
}
return i, nil
}
// BuildInstanceMetadataItem create an metadata item
func BuildInstanceMetadataItem(key, value string) *api.MetadataItems {
return &api.MetadataItems{
Key: key,
Value: func() *string { v := value; return &v }(),
}
}
// StartWithScriptCode starts the instance with given startup script and metadata.
func (i *InstanceBeta) StartWithScriptCode(script string, instanceMetadata map[string]string) error {
if instanceMetadata == nil {
instanceMetadata = make(map[string]string)
}
if i.IsWindows {
instanceMetadata["windows-startup-script-ps1"] = script
} else {
instanceMetadata["startup-script"] = script
}
var metadataItems []*api.MetadataItems
for k, v := range instanceMetadata {
metadataItems = append(metadataItems, BuildInstanceMetadataItem(k, v))
}
err := i.Client.SetInstanceMetadata(i.Project, i.Zone,
i.Name, &api.Metadata{Items: metadataItems, Fingerprint: i.Metadata.Fingerprint})
if err != nil {
return err
}
if err = i.Client.StartInstance(i.Project, i.Zone, i.Name); err != nil {
return err
}
return nil
}
// Cleanup deletes the InstanceBeta.
func (i *InstanceBeta) Cleanup() error {
return i.Client.DeleteInstance(i.Project, i.Zone, i.Name)
}