e2e_tests/compute/instance.go (170 lines of code) (raw):
// Copyright 2018 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 compute contains wrappers around the GCE compute API.
package compute
import (
"context"
"fmt"
"net/http"
"os"
"path"
"regexp"
"strings"
"time"
daisyCompute "github.com/GoogleCloudPlatform/compute-image-tools/daisy/compute"
computeApiBeta "google.golang.org/api/compute/v0.beta"
computeApi "google.golang.org/api/compute/v1"
"google.golang.org/api/googleapi"
)
// Instance is a compute instance.
type Instance struct {
*computeApi.Instance
client daisyCompute.Client
Project, Zone string
}
// Cleanup deletes the Instance.
func (i *Instance) Cleanup() {
if err := i.client.DeleteInstance(i.Project, i.Zone, i.Name); err != nil {
fmt.Printf("Error deleting instance: %v\n", err)
}
}
// WaitForGuestAttributes waits for guest attribute (queryPath, variableKey) to appear.
func (i *Instance) WaitForGuestAttributes(queryPath string, interval, timeout time.Duration) ([]*computeApiBeta.GuestAttributesEntry, error) {
tick := time.Tick(interval)
timedout := time.After(timeout)
for {
select {
case <-timedout:
return nil, fmt.Errorf("timed out waiting for guest attribute %q", queryPath)
case <-tick:
attr, err := i.GetGuestAttributes(queryPath)
if err != nil {
apiErr, ok := err.(*googleapi.Error)
if ok && apiErr.Code == http.StatusNotFound {
continue
}
return nil, err
}
return attr, nil
}
}
}
// GetGuestAttributes gets guest attributes for an instance.
func (i *Instance) GetGuestAttributes(queryPath string) ([]*computeApiBeta.GuestAttributesEntry, error) {
resp, err := i.client.GetGuestAttributes(i.Project, i.Zone, i.Name, queryPath, "")
if err != nil {
return nil, err
}
if resp.QueryValue == nil {
return nil, nil
}
return resp.QueryValue.Items, nil
}
// AddMetadata adds metadata to the instance.
func (i *Instance) AddMetadata(mdi ...*computeApi.MetadataItems) error {
resp, err := i.client.GetInstance(i.Project, i.Zone, i.Name)
if err != nil {
return err
}
for _, old := range resp.Metadata.Items {
found := false
for _, new := range mdi {
if old.Key == new.Key {
found = true
break
}
}
if found {
continue
}
mdi = append(mdi, old)
}
resp.Metadata.Items = mdi
return i.client.SetInstanceMetadata(i.Project, i.Zone, i.Name, resp.Metadata)
}
// WaitForSerialOutput waits for all positive regex matches and reports error for any negative regex match on a serial port.
func (i *Instance) WaitForSerialOutput(positiveRegexes []*regexp.Regexp, negativeRegexes []*regexp.Regexp, port int64, interval, timeout time.Duration) error {
var start int64
var errs int
matches := make([]bool, len(positiveRegexes))
tick := time.Tick(interval)
timedout := time.After(timeout)
for {
select {
case <-timedout:
var patterns []string
for _, regex := range positiveRegexes {
patterns = append(patterns, regex.String())
}
return fmt.Errorf("timed out waiting for regexes [%s]", strings.Join(patterns, ","))
case <-tick:
resp, err := i.client.GetSerialPortOutput(i.Project, i.Zone, i.Name, port, start)
if err != nil {
status, sErr := i.client.InstanceStatus(i.Project, i.Zone, i.Name)
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 isTerminal(status) {
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") {
for _, regex := range negativeRegexes {
if regex.MatchString(ln) {
return fmt.Errorf("matched negative regexes [%s]", regex)
}
}
for i, regex := range positiveRegexes {
if regex.MatchString(ln) {
matches[i] = true
}
}
matched := 0
for _, match := range matches {
if match {
matched++
}
}
if matched == len(positiveRegexes) {
return nil
}
}
errs = 0
}
}
}
// RecordSerialOutput stores the serial output of an instance to GCS bucket
func (i *Instance) RecordSerialOutput(ctx context.Context, logsPath string, port int64) {
os.MkdirAll(logsPath, 0770)
f, err := os.Create(path.Join(logsPath, fmt.Sprintf("%s-serial-port%d.log", i.Name, port)))
if err != nil {
fmt.Printf("Instance %q: error creating serial log file: %s", i.Name, err)
}
resp, err := i.client.GetSerialPortOutput(path.Base(i.Project), path.Base(i.Zone), i.Name, port, 0)
if err != nil {
// Instance is stopped or stopping.
status, _ := i.client.InstanceStatus(path.Base(i.Project), path.Base(i.Zone), i.Name)
if !isTerminal(status) {
fmt.Printf("Instance %q: error getting serial port: %s", i.Name, err)
}
return
}
if _, err := f.Write([]byte(resp.Contents)); err != nil {
fmt.Printf("Instance %q: error writing serial log file: %s", i.Name, err)
}
if err := f.Close(); err != nil {
fmt.Printf("Instance %q: error closing serial log file: %s", i.Name, err)
}
}
func isTerminal(status string) bool {
return status == "TERMINATED" || status == "STOPPED" || status == "STOPPING"
}
// CreateInstance creates a compute instance.
func CreateInstance(client daisyCompute.Client, project, zone string, i *computeApi.Instance) (*Instance, error) {
if err := client.CreateInstance(project, zone, i); err != nil {
return nil, err
}
return &Instance{Instance: i, client: client, Project: project, Zone: zone}, nil
}
// BuildInstanceMetadataItem create an metadata item
func BuildInstanceMetadataItem(key, value string) *computeApi.MetadataItems {
return &computeApi.MetadataItems{
Key: key,
Value: func() *string { v := value; return &v }(),
}
}