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 }(), } }