diff --git a/builder/googlecompute/artifact.go b/builder/googlecompute/artifact.go index cb2ad88bb..128db103b 100644 --- a/builder/googlecompute/artifact.go +++ b/builder/googlecompute/artifact.go @@ -7,7 +7,7 @@ import ( // Artifact represents a GCE image as the result of a Packer build. type Artifact struct { - imageName string + image Image driver Driver } @@ -18,8 +18,8 @@ func (*Artifact) BuilderId() string { // Destroy destroys the GCE image represented by the artifact. func (a *Artifact) Destroy() error { - log.Printf("Destroying image: %s", a.imageName) - errCh := a.driver.DeleteImage(a.imageName) + log.Printf("Destroying image: %s", a.image.Name) + errCh := a.driver.DeleteImage(a.image.Name) return <-errCh } @@ -30,12 +30,12 @@ func (*Artifact) Files() []string { // Id returns the GCE image name. func (a *Artifact) Id() string { - return a.imageName + return a.image.Name } // String returns the string representation of the artifact. func (a *Artifact) String() string { - return fmt.Sprintf("A disk image was created: %v", a.imageName) + return fmt.Sprintf("A disk image was created: %v", a.image.Name) } func (a *Artifact) State(name string) interface{} { diff --git a/builder/googlecompute/builder.go b/builder/googlecompute/builder.go index 0f6e4bd2f..987672a90 100644 --- a/builder/googlecompute/builder.go +++ b/builder/googlecompute/builder.go @@ -67,6 +67,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe SSHConfig: sshConfig, }, new(common.StepProvision), + new(StepWaitInstanceStartup), new(StepTeardownInstance), new(StepCreateImage), } @@ -86,14 +87,14 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe if rawErr, ok := state.GetOk("error"); ok { return nil, rawErr.(error) } - if _, ok := state.GetOk("image_name"); !ok { - log.Println("Failed to find image_name in state. Bug?") + if _, ok := state.GetOk("image"); !ok { + log.Println("Failed to find image in state. Bug?") return nil, nil } artifact := &Artifact{ - imageName: state.Get("image_name").(string), - driver: driver, + image: state.Get("image").(Image), + driver: driver, } return artifact, nil } diff --git a/builder/googlecompute/common.go b/builder/googlecompute/common.go new file mode 100644 index 000000000..8f5520c16 --- /dev/null +++ b/builder/googlecompute/common.go @@ -0,0 +1,44 @@ +package googlecompute + +import ( + "fmt" + "math" + "time" +) + +var RetryExhaustedError error = fmt.Errorf("Function never succeeded in Retry") + +// Retry retries a function up to numTries times with exponential backoff. +// If numTries == 0, retry indefinitely. If interval == 0, Retry will not delay retrying and there will be +// no exponential backoff. If maxInterval == 0, maxInterval is set to +Infinity. +// Intervals are in seconds. +// Returns an error if initial > max intervals, if retries are exhausted, or if the passed function returns +// an error. +func Retry(initialInterval float64, maxInterval float64, numTries uint, function func() (bool, error)) error { + if maxInterval == 0 { + maxInterval = math.Inf(1) + } else if initialInterval < 0 || initialInterval > maxInterval { + return fmt.Errorf("Invalid retry intervals (negative or initial < max). Initial: %f, Max: %f.", initialInterval, maxInterval) + } + + var err error + done := false + interval := initialInterval + for i := uint(0); !done && (numTries == 0 || i < numTries); i++ { + done, err = function() + if err != nil { + return err + } + + if !done { + // Retry after delay. Calculate next delay. + time.Sleep(time.Duration(interval) * time.Second) + interval = math.Min(interval * 2, maxInterval) + } + } + + if !done { + return RetryExhaustedError + } + return nil +} diff --git a/builder/googlecompute/common_test.go b/builder/googlecompute/common_test.go new file mode 100644 index 000000000..caf9c2436 --- /dev/null +++ b/builder/googlecompute/common_test.go @@ -0,0 +1,64 @@ +package googlecompute + +import ( + "fmt" + "testing" +) + +func TestRetry(t *testing.T) { + numTries := uint(0) + // Test that a passing function only gets called once. + err := Retry(0, 0, 0, func() (bool, error) { + numTries++ + return true, nil + }) + if numTries != 1 { + t.Fatal("Passing function should not have been retried.") + } + if err != nil { + t.Fatalf("Passing function should not have returned a retry error. Error: %s", err) + } + + // Test that a failing function gets retried (once in this example). + numTries = 0 + results := []bool{false, true} + err = Retry(0, 0, 0, func() (bool, error) { + result := results[numTries] + numTries++ + return result, nil + }) + if numTries != 2 { + t.Fatalf("Retried function should have been tried twice. Tried %d times.", numTries) + } + if err != nil { + t.Fatalf("Successful retried function should not have returned a retry error. Error: %s", err) + } + + // Test that a function error gets returned, and the function does not get called again. + numTries = 0 + funcErr := fmt.Errorf("This function had an error!") + err = Retry(0, 0, 0, func() (bool, error) { + numTries++ + return false, funcErr + }) + if numTries != 1 { + t.Fatal("Errant function should not have been retried.") + } + if err != funcErr { + t.Fatalf("Errant function did not return the right error %s. Error: %s", funcErr, err) + } + + // Test when a function exhausts its retries. + numTries = 0 + expectedTries := uint(3) + err = Retry(0, 0, expectedTries, func() (bool, error) { + numTries++ + return false, nil + }) + if numTries != expectedTries { + t.Fatalf("Unsuccessul retry function should have been called %d times. Only called %d times.", expectedTries, numTries) + } + if err != RetryExhaustedError { + t.Fatalf("Unsuccessful retry function should have returned a retry exhausted error. Actual error: %s", err) + } +} \ No newline at end of file diff --git a/builder/googlecompute/config.go b/builder/googlecompute/config.go index e93347087..2072bd112 100644 --- a/builder/googlecompute/config.go +++ b/builder/googlecompute/config.go @@ -26,6 +26,7 @@ type Config struct { AccountFile string `mapstructure:"account_file"` ProjectId string `mapstructure:"project_id"` + Address string `mapstructure:"address"` DiskName string `mapstructure:"disk_name"` DiskSizeGb int64 `mapstructure:"disk_size"` DiskType string `mapstructure:"disk_type"` @@ -36,15 +37,15 @@ type Config struct { MachineType string `mapstructure:"machine_type"` Metadata map[string]string `mapstructure:"metadata"` Network string `mapstructure:"network"` - Subnetwork string `mapstructure:"subnetwork"` - Address string `mapstructure:"address"` Preemptible bool `mapstructure:"preemptible"` + RawStateTimeout string `mapstructure:"state_timeout"` + Region string `mapstructure:"region"` SourceImage string `mapstructure:"source_image"` SourceImageProjectId string `mapstructure:"source_image_project_id"` - RawStateTimeout string `mapstructure:"state_timeout"` + StartupScriptFile string `mapstructure:"startup_script_file"` + Subnetwork string `mapstructure:"subnetwork"` Tags []string `mapstructure:"tags"` UseInternalIP bool `mapstructure:"use_internal_ip"` - Region string `mapstructure:"region"` Zone string `mapstructure:"zone"` account accountFile diff --git a/builder/googlecompute/driver.go b/builder/googlecompute/driver.go index 13cd2809e..f1c6c931e 100644 --- a/builder/googlecompute/driver.go +++ b/builder/googlecompute/driver.go @@ -10,7 +10,7 @@ type Driver interface { // CreateImage creates an image from the given disk in Google Compute // Engine. - CreateImage(name, description, family, zone, disk string) <-chan error + CreateImage(name, description, family, zone, disk string) (<-chan Image, <-chan error) // DeleteImage deletes the image with the given name. DeleteImage(name string) <-chan error @@ -21,12 +21,15 @@ type Driver interface { // DeleteDisk deletes the disk with the given name. DeleteDisk(zone, name string) (<-chan error, error) - // GetNatIP gets the NAT IP address for the instance. - GetNatIP(zone, name string) (string, error) - // GetInternalIP gets the GCE-internal IP address for the instance. GetInternalIP(zone, name string) (string, error) + // GetNatIP gets the NAT IP address for the instance. + GetNatIP(zone, name string) (string, error) + + // GetSerialPortOutput gets the Serial Port contents for the instance. + GetSerialPortOutput(zone, name string) (string, error) + // RunInstance takes the given config and launches an instance. RunInstance(*InstanceConfig) (<-chan error, error) @@ -37,21 +40,23 @@ type Driver interface { type Image struct { Name string ProjectId string + SizeGb int64 } type InstanceConfig struct { - Description string - DiskSizeGb int64 - DiskType string - Image Image - MachineType string - Metadata map[string]string - Name string - Network string - Subnetwork string - Address string - Preemptible bool - Tags []string - Region string - Zone string + Address string + Description string + DiskSizeGb int64 + DiskType string + Image Image + MachineType string + Metadata map[string]string + Name string + Network string + Preemptible bool + Region string + ServiceAccountEmail string + Subnetwork string + Tags []string + Zone string } diff --git a/builder/googlecompute/driver_gce.go b/builder/googlecompute/driver_gce.go index 306ab1756..3b262d59e 100644 --- a/builder/googlecompute/driver_gce.go +++ b/builder/googlecompute/driver_gce.go @@ -5,7 +5,6 @@ import ( "log" "net/http" "runtime" - "time" "github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/version" @@ -91,7 +90,7 @@ func (d *driverGCE) ImageExists(name string) bool { return err == nil } -func (d *driverGCE) CreateImage(name, description, family, zone, disk string) <-chan error { +func (d *driverGCE) CreateImage(name, description, family, zone, disk string) (<-chan Image, <-chan error) { image := &compute.Image{ Description: description, Name: name, @@ -100,15 +99,32 @@ func (d *driverGCE) CreateImage(name, description, family, zone, disk string) <- SourceType: "RAW", } + imageCh := make(chan Image, 1) errCh := make(chan error, 1) op, err := d.service.Images.Insert(d.projectId, image).Do() if err != nil { errCh <- err } else { - go waitForState(errCh, "DONE", d.refreshGlobalOp(op)) + go func() { + err = waitForState(errCh, "DONE", d.refreshGlobalOp(op)) + if err != nil { + close(imageCh) + } + image, err = d.getImage(name, d.projectId) + if err != nil { + close(imageCh) + errCh <- err + } + imageCh <- Image{ + Name: name, + ProjectId: d.projectId, + SizeGb: image.DiskSizeGb, + } + close(imageCh) + }() } - return errCh + return imageCh, errCh } func (d *driverGCE) DeleteImage(name string) <-chan error { @@ -181,6 +197,15 @@ func (d *driverGCE) GetInternalIP(zone, name string) (string, error) { return "", nil } +func (d *driverGCE) GetSerialPortOutput(zone, name string) (string, error) { + output, err := d.service.Instances.GetSerialPortOutput(d.projectId, zone, name).Do() + if err != nil { + return "", err + } + + return output.Contents, nil +} + func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) { // Get the zone d.ui.Message(fmt.Sprintf("Loading zone: %s", c.Zone)) @@ -191,7 +216,7 @@ func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) { // Get the image d.ui.Message(fmt.Sprintf("Loading image: %s in project %s", c.Image.Name, c.Image.ProjectId)) - image, err := d.getImage(c.Image) + image, err := d.getImage(c.Image.Name, c.Image.ProjectId) if err != nil { return nil, err } @@ -294,7 +319,7 @@ func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) { }, ServiceAccounts: []*compute.ServiceAccount{ &compute.ServiceAccount{ - Email: "default", + Email: c.ServiceAccountEmail, Scopes: []string{ "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/compute", @@ -324,17 +349,17 @@ func (d *driverGCE) WaitForInstance(state, zone, name string) <-chan error { return errCh } -func (d *driverGCE) getImage(img Image) (image *compute.Image, err error) { - projects := []string{img.ProjectId, "centos-cloud", "coreos-cloud", "debian-cloud", "google-containers", "opensuse-cloud", "rhel-cloud", "suse-cloud", "ubuntu-os-cloud", "windows-cloud"} +func (d *driverGCE) getImage(name, projectId string) (image *compute.Image, err error) { + projects := []string{projectId, "centos-cloud", "coreos-cloud", "debian-cloud", "google-containers", "opensuse-cloud", "rhel-cloud", "suse-cloud", "ubuntu-os-cloud", "windows-cloud"} for _, project := range projects { - image, err = d.service.Images.Get(project, img.Name).Do() + image, err = d.service.Images.Get(project, name).Do() if err == nil && image != nil && image.SelfLink != "" { return } image = nil } - err = fmt.Errorf("Image %s could not be found in any of these projects: %s", img.Name, projects) + err = fmt.Errorf("Image %s could not be found in any of these projects: %s", name, projects) return } @@ -396,18 +421,16 @@ type stateRefreshFunc func() (string, error) // waitForState will spin in a loop forever waiting for state to // reach a certain target. -func waitForState(errCh chan<- error, target string, refresh stateRefreshFunc) { - for { +func waitForState(errCh chan<- error, target string, refresh stateRefreshFunc) error { + err := Retry(2, 2, 0, func() (bool, error) { state, err := refresh() if err != nil { - errCh <- err - return + return false, err + } else if state == target { + return true, nil } - if state == target { - errCh <- nil - return - } - - time.Sleep(2 * time.Second) - } + return false, nil + }) + errCh <- err + return err } diff --git a/builder/googlecompute/driver_mock.go b/builder/googlecompute/driver_mock.go index 551671a91..90ab726c5 100644 --- a/builder/googlecompute/driver_mock.go +++ b/builder/googlecompute/driver_mock.go @@ -6,12 +6,15 @@ type DriverMock struct { ImageExistsName string ImageExistsResult bool - CreateImageName string - CreateImageDesc string - CreateImageFamily string - CreateImageZone string - CreateImageDisk string - CreateImageErrCh <-chan error + CreateImageName string + CreateImageDesc string + CreateImageFamily string + CreateImageZone string + CreateImageDisk string + CreateImageProjectId string + CreateImageSizeGb int64 + CreateImageErrCh <-chan error + CreateImageResultCh <-chan Image DeleteImageName string DeleteImageErrCh <-chan error @@ -35,6 +38,11 @@ type DriverMock struct { GetInternalIPName string GetInternalIPResult string GetInternalIPErr error + + GetSerialPortOutputZone string + GetSerialPortOutputName string + GetSerialPortOutputResult string + GetSerialPortOutputErr error RunInstanceConfig *InstanceConfig RunInstanceErrCh <-chan error @@ -51,21 +59,39 @@ func (d *DriverMock) ImageExists(name string) bool { return d.ImageExistsResult } -func (d *DriverMock) CreateImage(name, description, family, zone, disk string) <-chan error { +func (d *DriverMock) CreateImage(name, description, family, zone, disk string) (<-chan Image, <-chan error) { d.CreateImageName = name d.CreateImageDesc = description d.CreateImageFamily = family d.CreateImageZone = zone d.CreateImageDisk = disk + if d.CreateImageSizeGb == 0 { + d.CreateImageSizeGb = 10 + } + if d.CreateImageProjectId == "" { + d.CreateImageProjectId = "test" + } - resultCh := d.CreateImageErrCh + resultCh := d.CreateImageResultCh if resultCh == nil { - ch := make(chan error) + ch := make(chan Image, 1) + ch <- Image{ + Name: name, + ProjectId: d.CreateImageProjectId, + SizeGb: d.CreateImageSizeGb, + } close(ch) resultCh = ch } - return resultCh + errCh := d.CreateImageErrCh + if errCh == nil { + ch := make(chan error) + close(ch) + errCh = ch + } + + return resultCh, errCh } func (d *DriverMock) DeleteImage(name string) <-chan error { @@ -121,6 +147,12 @@ func (d *DriverMock) GetInternalIP(zone, name string) (string, error) { return d.GetInternalIPResult, d.GetInternalIPErr } +func (d *DriverMock) GetSerialPortOutput(zone, name string) (string, error) { + d.GetSerialPortOutputZone = zone + d.GetSerialPortOutputName = name + return d.GetSerialPortOutputResult, d.GetSerialPortOutputErr +} + func (d *DriverMock) RunInstance(c *InstanceConfig) (<-chan error, error) { d.RunInstanceConfig = c diff --git a/builder/googlecompute/startup.go b/builder/googlecompute/startup.go new file mode 100644 index 000000000..308e33e18 --- /dev/null +++ b/builder/googlecompute/startup.go @@ -0,0 +1,53 @@ +package googlecompute + +import ( + "encoding/base64" + "fmt" +) + +const StartupScriptStartLog string = "Packer startup script starting." +const StartupScriptDoneLog string = "Packer startup script done." +const StartupScriptKey string = "startup-script" +const StartupWrappedScriptKey string = "packer-wrapped-startup-script" + +// We have to encode StartupScriptDoneLog because we use it as a sentinel value to indicate +// that the user-provided startup script is done. If we pass StartupScriptDoneLog as-is, it +// will be printed early in the instance console log (before the startup script even runs; +// we print out instance creation metadata which contains this wrapper script). +var StartupScriptDoneLogBase64 string = base64.StdEncoding.EncodeToString([]byte(StartupScriptDoneLog)) + +var StartupScript string = fmt.Sprintf(`#!/bin/bash +echo %s +RETVAL=0 + +GetMetadata () { + echo "$(curl -f -H "Metadata-Flavor: Google" http://metadata/computeMetadata/v1/instance/attributes/$1 2> /dev/null)" +} + +STARTUPSCRIPT=$(GetMetadata %s) +STARTUPSCRIPTPATH=/packer-wrapped-startup-script +if [ -f "/var/log/startupscript.log" ]; then + STARTUPSCRIPTLOGPATH=/var/log/startupscript.log +else + STARTUPSCRIPTLOGPATH=/var/log/daemon.log +fi +STARTUPSCRIPTLOGDEST=$(GetMetadata startup-script-log-dest) + +if [[ ! -z $STARTUPSCRIPT ]]; then + echo "Executing user-provided startup script..." + echo "${STARTUPSCRIPT}" > ${STARTUPSCRIPTPATH} + chmod +x ${STARTUPSCRIPTPATH} + ${STARTUPSCRIPTPATH} + RETVAL=$? + + if [[ ! -z $STARTUPSCRIPTLOGDEST ]]; then + echo "Uploading user-provided startup script log to ${STARTUPSCRIPTLOGDEST}..." + gsutil -h "Content-Type:text/plain" cp ${STARTUPSCRIPTLOGPATH} ${STARTUPSCRIPTLOGDEST} + fi + + rm ${STARTUPSCRIPTPATH} +fi + +echo $(echo %s | base64 --decode) +exit $RETVAL +`, StartupScriptStartLog, StartupWrappedScriptKey, StartupScriptDoneLogBase64) diff --git a/builder/googlecompute/step_create_image.go b/builder/googlecompute/step_create_image.go index dbb690dd3..f7e531d43 100644 --- a/builder/googlecompute/step_create_image.go +++ b/builder/googlecompute/step_create_image.go @@ -23,7 +23,8 @@ func (s *StepCreateImage) Run(state multistep.StateBag) multistep.StepAction { ui := state.Get("ui").(packer.Ui) ui.Say("Creating image...") - errCh := driver.CreateImage(config.ImageName, config.ImageDescription, config.ImageFamily, config.Zone, config.DiskName) + + imageCh, errCh := driver.CreateImage(config.ImageName, config.ImageDescription, config.ImageFamily, config.Zone, config.DiskName) var err error select { case err = <-errCh: @@ -38,7 +39,7 @@ func (s *StepCreateImage) Run(state multistep.StateBag) multistep.StepAction { return multistep.ActionHalt } - state.Put("image_name", config.ImageName) + state.Put("image", <-imageCh) return multistep.ActionContinue } diff --git a/builder/googlecompute/step_create_image_test.go b/builder/googlecompute/step_create_image_test.go index 4358f10de..c956ec5e3 100644 --- a/builder/googlecompute/step_create_image_test.go +++ b/builder/googlecompute/step_create_image_test.go @@ -18,13 +18,35 @@ func TestStepCreateImage(t *testing.T) { config := state.Get("config").(*Config) driver := state.Get("driver").(*DriverMock) + driver.CreateImageProjectId = "createimage-project" + driver.CreateImageSizeGb = 100 // run the step if action := step.Run(state); action != multistep.ActionContinue { t.Fatalf("bad action: %#v", action) } + + uncastImage, ok := state.GetOk("image") + if !ok { + t.Fatal("should have image") + } + image, ok := uncastImage.(Image) + if !ok { + t.Fatal("image is not an Image") + } + + // Verify created Image results. + if image.Name != config.ImageName { + t.Fatalf("Created image name, %s, does not match config name, %s.", image.Name, config.ImageName) + } + if driver.CreateImageProjectId != image.ProjectId { + t.Fatalf("Created image project ID, %s, does not match driver project ID, %s.", image.ProjectId, driver.CreateImageProjectId) + } + if driver.CreateImageSizeGb != image.SizeGb { + t.Fatalf("Created image size, %d, does not match the expected test value, %d.", image.SizeGb, driver.CreateImageSizeGb) + } - // Verify state + // Verify proper args passed to driver.CreateImage. if driver.CreateImageName != config.ImageName { t.Fatalf("bad: %#v", driver.CreateImageName) } @@ -40,16 +62,6 @@ func TestStepCreateImage(t *testing.T) { if driver.CreateImageDisk != config.DiskName { t.Fatalf("bad: %#v", driver.CreateImageDisk) } - - nameRaw, ok := state.GetOk("image_name") - if !ok { - t.Fatal("should have name") - } - if name, ok := nameRaw.(string); !ok { - t.Fatal("name is not a string") - } else if name != config.ImageName { - t.Fatalf("bad name: %s", name) - } } func TestStepCreateImage_errorOnChannel(t *testing.T) { diff --git a/builder/googlecompute/step_create_instance.go b/builder/googlecompute/step_create_instance.go index 45320d158..cf997a920 100644 --- a/builder/googlecompute/step_create_instance.go +++ b/builder/googlecompute/step_create_instance.go @@ -3,6 +3,7 @@ package googlecompute import ( "errors" "fmt" + "io/ioutil" "time" "github.com/mitchellh/multistep" @@ -22,23 +23,34 @@ func (config *Config) getImage() Image { return Image{Name: config.SourceImage, ProjectId: project} } -func (config *Config) getInstanceMetadata(sshPublicKey string) map[string]string { +func (config *Config) getInstanceMetadata(sshPublicKey string) (map[string]string, error) { instanceMetadata := make(map[string]string) + var err error - // Copy metadata from config + // Copy metadata from config. for k, v := range config.Metadata { instanceMetadata[k] = v } - // Merge any existing ssh keys with our public key + // Merge any existing ssh keys with our public key. sshMetaKey := "sshKeys" sshKeys := fmt.Sprintf("%s:%s", config.Comm.SSHUsername, sshPublicKey) if confSshKeys, exists := instanceMetadata[sshMetaKey]; exists { sshKeys = fmt.Sprintf("%s\n%s", sshKeys, confSshKeys) } instanceMetadata[sshMetaKey] = sshKeys + + // Wrap any startup script with our own startup script. + if config.StartupScriptFile != "" { + var content []byte + content, err = ioutil.ReadFile(config.StartupScriptFile) + instanceMetadata[StartupWrappedScriptKey] = string(content) + } else if wrappedStartupScript, exists := instanceMetadata[StartupScriptKey]; exists { + instanceMetadata[StartupWrappedScriptKey] = wrappedStartupScript + } + instanceMetadata[StartupScriptKey] = StartupScript - return instanceMetadata + return instanceMetadata, err } // Run executes the Packer build step that creates a GCE instance. @@ -51,21 +63,26 @@ func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction ui.Say("Creating instance...") name := config.InstanceName - errCh, err := driver.RunInstance(&InstanceConfig{ - Description: "New instance created by Packer", - DiskSizeGb: config.DiskSizeGb, - DiskType: config.DiskType, - Image: config.getImage(), - MachineType: config.MachineType, - Metadata: config.getInstanceMetadata(sshPublicKey), - Name: name, - Network: config.Network, - Subnetwork: config.Subnetwork, - Address: config.Address, - Preemptible: config.Preemptible, - Tags: config.Tags, - Region: config.Region, - Zone: config.Zone, + var errCh <-chan error + var err error + var metadata map[string]string + metadata, err = config.getInstanceMetadata(sshPublicKey) + errCh, err = driver.RunInstance(&InstanceConfig{ + Address: config.Address, + Description: "New instance created by Packer", + DiskSizeGb: config.DiskSizeGb, + DiskType: config.DiskType, + Image: config.getImage(), + MachineType: config.MachineType, + Metadata: metadata, + Name: name, + Network: config.Network, + Preemptible: config.Preemptible, + Region: config.Region, + ServiceAccountEmail: config.account.ClientEmail, + Subnetwork: config.Subnetwork, + Tags: config.Tags, + Zone: config.Zone, }) if err == nil { diff --git a/builder/googlecompute/step_create_ssh_key.go b/builder/googlecompute/step_create_ssh_key.go index 521e6c3d6..4c4894fba 100644 --- a/builder/googlecompute/step_create_ssh_key.go +++ b/builder/googlecompute/step_create_ssh_key.go @@ -20,6 +20,8 @@ type StepCreateSSHKey struct { } // Run executes the Packer build step that generates SSH key pairs. +// The key pairs are added to the multistep state as "ssh_private_key" and +// "ssh_public_key". func (s *StepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction { ui := state.Get("ui").(packer.Ui) diff --git a/builder/googlecompute/step_instance_info.go b/builder/googlecompute/step_instance_info.go index 92f382f06..511906206 100644 --- a/builder/googlecompute/step_instance_info.go +++ b/builder/googlecompute/step_instance_info.go @@ -17,6 +17,7 @@ type StepInstanceInfo struct { } // Run executes the Packer build step that gathers GCE instance info. +// This adds "instance_ip" to the multistep state. func (s *StepInstanceInfo) Run(state multistep.StateBag) multistep.StepAction { config := state.Get("config").(*Config) driver := state.Get("driver").(Driver) diff --git a/builder/googlecompute/step_teardown_instance.go b/builder/googlecompute/step_teardown_instance.go index b623d24fd..42ad83a5d 100644 --- a/builder/googlecompute/step_teardown_instance.go +++ b/builder/googlecompute/step_teardown_instance.go @@ -27,6 +27,8 @@ func (s *StepTeardownInstance) Run(state multistep.StateBag) multistep.StepActio } ui.Say("Deleting instance...") + instanceLog, _ := driver.GetSerialPortOutput(config.Zone, name) + state.Put("instance_log", instanceLog) errCh, err := driver.DeleteInstance(config.Zone, name) if err == nil { select { @@ -43,7 +45,6 @@ func (s *StepTeardownInstance) Run(state multistep.StateBag) multistep.StepActio "Error: %s", name, err)) return multistep.ActionHalt } - ui.Message("Instance has been deleted!") state.Put("instance_name", "") diff --git a/builder/googlecompute/step_wait_instance_startup.go b/builder/googlecompute/step_wait_instance_startup.go new file mode 100644 index 000000000..3fa162732 --- /dev/null +++ b/builder/googlecompute/step_wait_instance_startup.go @@ -0,0 +1,50 @@ +package googlecompute + +import( + "fmt" + "strings" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type StepWaitInstanceStartup int + +// Run reads the instance serial port output and looks for the log entry indicating the startup script finished. +func (s *StepWaitInstanceStartup) Run(state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*Config) + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + instanceName := state.Get("instance_name").(string) + + ui.Say("Waiting for any running startup script to finish...") + + // Keep checking the serial port output to see if the startup script is done. + err := Retry(10, 60, 0, func() (bool, error) { + output, err := driver.GetSerialPortOutput(config.Zone, instanceName) + + if err != nil { + err := fmt.Errorf("Error getting serial port output: %s", err) + return false, err + } + + done := strings.Contains(output, StartupScriptDoneLog) + if !done { + ui.Say("Startup script not finished yet. Waiting...") + } + + return done, nil + }) + + if err != nil { + err := fmt.Errorf("Error waiting for startup script to finish: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + ui.Say("Startup script, if any, has finished running.") + return multistep.ActionContinue +} + +// Cleanup. +func (s *StepWaitInstanceStartup) Cleanup(state multistep.StateBag) {} diff --git a/builder/googlecompute/step_wait_instance_startup_test.go b/builder/googlecompute/step_wait_instance_startup_test.go new file mode 100644 index 000000000..da6d896d3 --- /dev/null +++ b/builder/googlecompute/step_wait_instance_startup_test.go @@ -0,0 +1,38 @@ +package googlecompute + +import ( + "github.com/mitchellh/multistep" + "testing" +) + +func TestStepWaitInstanceStartup(t *testing.T) { + state := testState(t) + step := new(StepWaitInstanceStartup) + config := state.Get("config").(*Config) + driver := state.Get("driver").(*DriverMock) + + testZone := "test-zone" + testInstanceName := "test-instance-name" + + config.Zone = testZone + state.Put("instance_name", testInstanceName) + // The done log triggers step completion. + driver.GetSerialPortOutputResult = StartupScriptDoneLog + + // Run the step. + if action := step.Run(state); action != multistep.ActionContinue { + t.Fatalf("StepWaitInstanceStartup did not return a Continue action: %#v", action) + } + + // Check that GetSerialPortOutput was called properly. + if driver.GetSerialPortOutputZone != testZone { + t.Fatalf( + "GetSerialPortOutput wrong zone. Expected: %s, Actual: %s", driver.GetSerialPortOutputZone, + testZone) + } + if driver.GetSerialPortOutputName != testInstanceName { + t.Fatalf( + "GetSerialPortOutput wrong instance name. Expected: %s, Actual: %s", driver.GetSerialPortOutputName, + testInstanceName) + } +} \ No newline at end of file diff --git a/website/source/docs/builders/googlecompute.html.md b/website/source/docs/builders/googlecompute.html.md index c4842b976..c944e9d4c 100644 --- a/website/source/docs/builders/googlecompute.html.md +++ b/website/source/docs/builders/googlecompute.html.md @@ -1,8 +1,9 @@ --- description: | The `googlecompute` Packer builder is able to create images for use with Google - Compute Engine (GCE) based on existing images. Google Compute Engine doesn't - allow the creation of images from scratch. + Compute Engine (GCE) based on existing images. Building GCE images from scratch + is not possible from Packer at this time. For building images from scratch, please see + [Building GCE Images from Scratch](https://cloud.google.com/compute/docs/tutorials/building-images). layout: docs page_title: Google Compute Builder ... @@ -14,9 +15,9 @@ Type: `googlecompute` The `googlecompute` Packer builder is able to create [images](https://developers.google.com/compute/docs/images) for use with [Google Compute Engine](https://cloud.google.com/products/compute-engine)(GCE) based on -existing images. Google Compute Engine doesn't allow the creation of images from -scratch. - +existing images. Building GCE images from scratch is not possible from Packer at +this time. For building images from scratch, please see +[Building GCE Images from Scratch](https://cloud.google.com/compute/docs/tutorials/building-images). ## Authentication Authenticating with Google Cloud services requires at most one JSON file, called @@ -76,10 +77,10 @@ straightforwarded, it is documented here. ## Basic Example Below is a fully functioning example. It doesn't do anything useful, since no -provisioners are defined, but it will effectively repackage an existing GCE -image. The account_file is obtained in the previous section. If it parses as -JSON it is assumed to be the file itself, otherwise it is assumed to be -the path to the file containing the JSON. +provisioners or startup-script metadata are defined, but it will effectively +repackage an existing GCE image. The account_file is obtained in the previous +section. If it parses as JSON it is assumed to be the file itself, otherwise it +is assumed to be the path to the file containing the JSON. ``` {.javascript} { @@ -150,6 +151,9 @@ builder. - `region` (string) - The region in which to launch the instance. Defaults to to the region hosting the specified `zone`. +- `startup_script_file` (string) - The filepath to a startup script to run on + the VM from which the image will be made. + - `state_timeout` (string) - The time to wait for instance state changes. Defaults to `"5m"`. @@ -163,6 +167,27 @@ builder. - `use_internal_ip` (boolean) - If true, use the instance's internal IP instead of its external IP during building. + +## Startup Scripts + +Startup scripts can be a powerful tool for configuring the instance from which the image is made. +The builder will wait for a startup script to terminate. A startup script can be provided via the +`startup_script_file` or 'startup-script' instance creation `metadata` field. Therefore, the build +time will vary depending on the duration of the startup script. If `startup_script_file` is set, +the 'startup-script' `metadata` field will be overwritten. In other words,`startup_script_file` +takes precedence. + +The builder does not check for a pass/fail/error signal from the startup script, at this time. Until +such support is implemented, startup scripts should be robust, as an image will still be built even +when a startup script fails. + +### Windows +Startup scripts do not work on Windows builds, at this time. + +### Logging +Startup script logs can be copied to a Google Cloud Storage (GCS) location specified via the +'startup-script-log-dest' instance creation `metadata` field. The GCS location must be writeable by +the credentials provided in the builder config's `account_file`. ## Gotchas