diff --git a/builder/googlecompute/api.go b/builder/googlecompute/api.go new file mode 100644 index 000000000..5a3d741b4 --- /dev/null +++ b/builder/googlecompute/api.go @@ -0,0 +1,318 @@ +// Copyright (c) 2013 Kelsey Hightower. All rights reserved. +// Use of this source code is governed by the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package googlecompute + +import ( + "errors" + "net/http" + "strings" + + "code.google.com/p/goauth2/oauth" + "code.google.com/p/goauth2/oauth/jwt" + "code.google.com/p/google-api-go-client/compute/v1beta16" +) + +// GoogleComputeClient represents a GCE client. +type GoogleComputeClient struct { + ProjectId string + Service *compute.Service + Zone string + clientSecrets *clientSecrets +} + +// InstanceConfig represents a GCE instance configuration. +// Used for creating machine instances. +type InstanceConfig struct { + Description string + Image string + MachineType string + Metadata *compute.Metadata + Name string + NetworkInterfaces []*compute.NetworkInterface + ServiceAccounts []*compute.ServiceAccount + Tags *compute.Tags +} + +// New initializes and returns a *GoogleComputeClient. +// +// The projectId must be the project name, i.e. myproject, not the project +// number. +func New(projectId string, zone string, c *clientSecrets, pemKey []byte) (*GoogleComputeClient, error) { + googleComputeClient := &GoogleComputeClient{ + ProjectId: projectId, + Zone: zone, + } + // Get the access token. + t := jwt.NewToken(c.Web.ClientEmail, scopes(), pemKey) + t.ClaimSet.Aud = c.Web.TokenURI + httpClient := &http.Client{} + token, err := t.Assert(httpClient) + if err != nil { + return nil, err + } + config := &oauth.Config{ + ClientId: c.Web.ClientId, + Scope: scopes(), + TokenURL: c.Web.TokenURI, + AuthURL: c.Web.AuthURI, + } + transport := &oauth.Transport{Config: config} + transport.Token = token + s, err := compute.New(transport.Client()) + if err != nil { + return nil, err + } + googleComputeClient.Service = s + return googleComputeClient, nil +} + +// GetZone returns a *compute.Zone representing the named zone. +func (g *GoogleComputeClient) GetZone(name string) (*compute.Zone, error) { + zoneGetCall := g.Service.Zones.Get(g.ProjectId, name) + zone, err := zoneGetCall.Do() + if err != nil { + return nil, err + } + return zone, nil +} + +// GetMachineType returns a *compute.MachineType representing the named machine type. +func (g *GoogleComputeClient) GetMachineType(name, zone string) (*compute.MachineType, error) { + machineTypesGetCall := g.Service.MachineTypes.Get(g.ProjectId, zone, name) + machineType, err := machineTypesGetCall.Do() + if err != nil { + return nil, err + } + if machineType.Deprecated == nil { + return machineType, nil + } + return nil, errors.New("Machine Type does not exist: " + name) +} + +// GetImage returns a *compute.Image representing the named image. +func (g *GoogleComputeClient) GetImage(name string) (*compute.Image, error) { + var err error + var image *compute.Image + projects := []string{g.ProjectId, "debian-cloud", "centos-cloud"} + for _, project := range projects { + imagesGetCall := g.Service.Images.Get(project, name) + image, err = imagesGetCall.Do() + if image != nil { + break + } + } + if err != nil { + return nil, err + } + if image != nil { + if image.SelfLink != "" { + return image, nil + } + } + return nil, errors.New("Image does not exist: " + name) +} + +// GetNetwork returns a *compute.Network representing the named network. +func (g *GoogleComputeClient) GetNetwork(name string) (*compute.Network, error) { + networkGetCall := g.Service.Networks.Get(g.ProjectId, name) + network, err := networkGetCall.Do() + if err != nil { + return nil, err + } + return network, nil +} + +// CreateInstance creates an instance in Google Compute Engine based on the +// supplied instanceConfig. +func (g *GoogleComputeClient) CreateInstance(zone string, instanceConfig *InstanceConfig) (*compute.Operation, error) { + instance := &compute.Instance{ + Description: instanceConfig.Description, + Image: instanceConfig.Image, + MachineType: instanceConfig.MachineType, + Metadata: instanceConfig.Metadata, + Name: instanceConfig.Name, + NetworkInterfaces: instanceConfig.NetworkInterfaces, + ServiceAccounts: instanceConfig.ServiceAccounts, + Tags: instanceConfig.Tags, + } + instanceInsertCall := g.Service.Instances.Insert(g.ProjectId, zone, instance) + operation, err := instanceInsertCall.Do() + if err != nil { + return nil, err + } + return operation, nil +} + +// InstanceStatus returns a string representing the status of the named instance. +// Status will be one of: "PROVISIONING", "STAGING", "RUNNING", "STOPPING", +// "STOPPED", "TERMINATED". +func (g *GoogleComputeClient) InstanceStatus(zone, name string) (string, error) { + instanceGetCall := g.Service.Instances.Get(g.ProjectId, zone, name) + instance, err := instanceGetCall.Do() + if err != nil { + return "", err + } + return instance.Status, nil +} + +// CreateImage registers a GCE Image with a project. +func (g *GoogleComputeClient) CreateImage(name, description, sourceURL string) (*compute.Operation, error) { + imageRawDisk := &compute.ImageRawDisk{ + ContainerType: "TAR", + Source: sourceURL, + } + image := &compute.Image{ + Description: description, + Name: name, + RawDisk: imageRawDisk, + SourceType: "RAW", + } + imageInsertCall := g.Service.Images.Insert(g.ProjectId, image) + operation, err := imageInsertCall.Do() + if err != nil { + return nil, err + } + return operation, nil +} + +// GetNatIp returns the public IPv4 address for named GCE instance. +func (g *GoogleComputeClient) GetNatIP(zone, name string) (string, error) { + instanceGetCall := g.Service.Instances.Get(g.ProjectId, zone, name) + instance, err := instanceGetCall.Do() + if err != nil { + return "", err + } + for _, ni := range instance.NetworkInterfaces { + if ni.AccessConfigs == nil { + continue + } + for _, ac := range ni.AccessConfigs { + if ac.NatIP != "" { + return ac.NatIP, nil + } + } + } + return "", nil +} + +// ZoneOperationStatus returns the status for the named zone operation. +func (g *GoogleComputeClient) ZoneOperationStatus(zone, name string) (string, error) { + zoneOperationsGetCall := g.Service.ZoneOperations.Get(g.ProjectId, zone, name) + operation, err := zoneOperationsGetCall.Do() + if err != nil { + return "", err + } + if operation.Status == "DONE" { + err = processOperationStatus(operation) + if err != nil { + return operation.Status, err + } + } + return operation.Status, nil +} + +// GlobalOperationStatus returns the status for the named global operation. +func (g *GoogleComputeClient) GlobalOperationStatus(name string) (string, error) { + globalOperationsGetCall := g.Service.GlobalOperations.Get(g.ProjectId, name) + operation, err := globalOperationsGetCall.Do() + if err != nil { + return "", err + } + if operation.Status == "DONE" { + err = processOperationStatus(operation) + if err != nil { + return operation.Status, err + } + } + return operation.Status, nil +} + +// processOperationStatus extracts errors from the specified operation. +func processOperationStatus(o *compute.Operation) error { + if o.Error != nil { + messages := make([]string, len(o.Error.Errors)) + for _, e := range o.Error.Errors { + messages = append(messages, e.Message) + } + return errors.New(strings.Join(messages, "\n")) + } + return nil +} + +// DeleteImage deletes the named image. Returns a Global Operation. +func (g *GoogleComputeClient) DeleteImage(name string) (*compute.Operation, error) { + imagesDeleteCall := g.Service.Images.Delete(g.ProjectId, name) + operation, err := imagesDeleteCall.Do() + if err != nil { + return nil, err + } + return operation, nil +} + +// DeleteInstance deletes the named instance. Returns a Zone Operation. +func (g *GoogleComputeClient) DeleteInstance(zone, name string) (*compute.Operation, error) { + instanceDeleteCall := g.Service.Instances.Delete(g.ProjectId, zone, name) + operation, err := instanceDeleteCall.Do() + if err != nil { + return nil, err + } + return operation, nil +} + +// NewNetworkInterface returns a *compute.NetworkInterface based on the data provided. +func NewNetworkInterface(network *compute.Network, public bool) *compute.NetworkInterface { + accessConfigs := make([]*compute.AccessConfig, 0) + if public { + c := &compute.AccessConfig{ + Name: "AccessConfig created by Packer", + Type: "ONE_TO_ONE_NAT", + } + accessConfigs = append(accessConfigs, c) + } + return &compute.NetworkInterface{ + AccessConfigs: accessConfigs, + Network: network.SelfLink, + } +} + +// NewServiceAccount returns a *compute.ServiceAccount with permissions required +// for creating GCE machine images. +func NewServiceAccount(email string) *compute.ServiceAccount { + return &compute.ServiceAccount{ + Email: email, + Scopes: []string{ + "https://www.googleapis.com/auth/userinfo.email", + "https://www.googleapis.com/auth/compute", + "https://www.googleapis.com/auth/devstorage.full_control", + }, + } +} + +// MapToMetadata converts a map[string]string to a *compute.Metadata. +func MapToMetadata(metadata map[string]string) *compute.Metadata { + items := make([]*compute.MetadataItems, len(metadata)) + for k, v := range metadata { + items = append(items, &compute.MetadataItems{k, v}) + } + return &compute.Metadata{ + Items: items, + } +} + +// SliceToTags converts a []string to a *compute.Tags. +func SliceToTags(tags []string) *compute.Tags { + return &compute.Tags{ + Items: tags, + } +} + +// scopes return a space separated list of scopes. +func scopes() string { + s := []string{ + "https://www.googleapis.com/auth/compute", + "https://www.googleapis.com/auth/devstorage.full_control", + } + return strings.Join(s, " ") +} diff --git a/builder/googlecompute/artifact.go b/builder/googlecompute/artifact.go new file mode 100644 index 000000000..45bf360fd --- /dev/null +++ b/builder/googlecompute/artifact.go @@ -0,0 +1,47 @@ +// Copyright (c) 2013 Kelsey Hightower. All rights reserved. +// Use of this source code is governed by the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package googlecompute + +import ( + "fmt" + "log" +) + +// Artifact represents a GCE image as the result of a Packer build. +type Artifact struct { + imageName string + client *GoogleComputeClient +} + +// BuilderId returns the builder Id. +func (*Artifact) BuilderId() string { + return BuilderId +} + +// Destroy destroys the GCE image represented by the artifact. +func (a *Artifact) Destroy() error { + log.Printf("Destroying image: %s", a.imageName) + // Ignore the operation result as we are not waiting until it completes. + _, err := a.client.DeleteImage(a.imageName) + if err != nil { + return err + } + return nil +} + +// Files returns the files represented by the artifact. +func (*Artifact) Files() []string { + return nil +} + +// Id returns the GCE image name. +func (a *Artifact) Id() string { + return a.imageName +} + +// String returns the string representation of the artifact. +func (a *Artifact) String() string { + return fmt.Sprintf("A disk image was created: %v", a.imageName) +} diff --git a/builder/googlecompute/builder.go b/builder/googlecompute/builder.go new file mode 100644 index 000000000..0c720c0ce --- /dev/null +++ b/builder/googlecompute/builder.go @@ -0,0 +1,241 @@ +// Copyright (c) 2013 Kelsey Hightower. All rights reserved. +// Use of this source code is governed by the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +// The googlecompute package contains a packer.Builder implementation that +// builds images for Google Compute Engine. +package googlecompute + +import ( + "errors" + "fmt" + "log" + "time" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/packer" +) + +// The unique ID for this builder. +const BuilderId = "kelseyhightower.googlecompute" + +// Builder represents a Packer Builder. +type Builder struct { + config config + runner multistep.Runner +} + +// config holds the googlecompute builder configuration settings. +type config struct { + BucketName string `mapstructure:"bucket_name"` + ClientSecretsFile string `mapstructure:"client_secrets_file"` + ImageName string `mapstructure:"image_name"` + ImageDescription string `mapstructure:"image_description"` + MachineType string `mapstructure:"machine_type"` + Metadata map[string]string `mapstructure:"metadata"` + Network string `mapstructure:"network"` + Passphrase string `mapstructure:"passphrase"` + PrivateKeyFile string `mapstructure:"private_key_file"` + ProjectId string `mapstructure:"project_id"` + SourceImage string `mapstructure:"source_image"` + SSHUsername string `mapstructure:"ssh_username"` + SSHPort uint `mapstructure:"ssh_port"` + RawSSHTimeout string `mapstructure:"ssh_timeout"` + RawStateTimeout string `mapstructure:"state_timeout"` + Tags []string `mapstructure:"tags"` + Zone string `mapstructure:"zone"` + clientSecrets *clientSecrets + common.PackerConfig `mapstructure:",squash"` + instanceName string + privateKeyBytes []byte + sshTimeout time.Duration + stateTimeout time.Duration + tpl *packer.ConfigTemplate +} + +// Prepare processes the build configuration parameters. +func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { + // Load the packer config. + md, err := common.DecodeConfig(&b.config, raws...) + if err != nil { + return nil, err + } + b.config.tpl, err = packer.NewConfigTemplate() + if err != nil { + return nil, err + } + b.config.tpl.UserVars = b.config.PackerUserVars + + errs := common.CheckUnusedConfig(md) + // Collect errors if any. + if err := common.CheckUnusedConfig(md); err != nil { + return nil, err + } + // Set defaults. + if b.config.Network == "" { + b.config.Network = "default" + } + if b.config.ImageDescription == "" { + b.config.ImageDescription = "Created by Packer" + } + if b.config.ImageName == "" { + // Default to packer-{{ unix timestamp (utc) }} + b.config.ImageName = "packer-{{timestamp}}" + } + if b.config.MachineType == "" { + b.config.MachineType = "n1-standard-1" + } + if b.config.RawSSHTimeout == "" { + b.config.RawSSHTimeout = "5m" + } + if b.config.RawStateTimeout == "" { + b.config.RawStateTimeout = "5m" + } + if b.config.SSHUsername == "" { + b.config.SSHUsername = "root" + } + if b.config.SSHPort == 0 { + b.config.SSHPort = 22 + } + // Process Templates + templates := map[string]*string{ + "bucket_name": &b.config.BucketName, + "client_secrets_file": &b.config.ClientSecretsFile, + "image_name": &b.config.ImageName, + "image_description": &b.config.ImageDescription, + "machine_type": &b.config.MachineType, + "network": &b.config.Network, + "passphrase": &b.config.Passphrase, + "private_key_file": &b.config.PrivateKeyFile, + "project_id": &b.config.ProjectId, + "source_image": &b.config.SourceImage, + "ssh_username": &b.config.SSHUsername, + "ssh_timeout": &b.config.RawSSHTimeout, + "state_timeout": &b.config.RawStateTimeout, + "zone": &b.config.Zone, + } + for n, ptr := range templates { + var err error + *ptr, err = b.config.tpl.Process(*ptr, nil) + if err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Error processing %s: %s", n, err)) + } + } + // Process required parameters. + if b.config.BucketName == "" { + errs = packer.MultiErrorAppend( + errs, errors.New("a bucket_name must be specified")) + } + if b.config.ClientSecretsFile == "" { + errs = packer.MultiErrorAppend( + errs, errors.New("a client_secrets_file must be specified")) + } + if b.config.PrivateKeyFile == "" { + errs = packer.MultiErrorAppend( + errs, errors.New("a private_key_file must be specified")) + } + if b.config.ProjectId == "" { + errs = packer.MultiErrorAppend( + errs, errors.New("a project_id must be specified")) + } + if b.config.SourceImage == "" { + errs = packer.MultiErrorAppend( + errs, errors.New("a source_image must be specified")) + } + if b.config.Zone == "" { + errs = packer.MultiErrorAppend( + errs, errors.New("a zone must be specified")) + } + // Process timeout settings. + sshTimeout, err := time.ParseDuration(b.config.RawSSHTimeout) + if err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err)) + } + b.config.sshTimeout = sshTimeout + stateTimeout, err := time.ParseDuration(b.config.RawStateTimeout) + if err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Failed parsing state_timeout: %s", err)) + } + b.config.stateTimeout = stateTimeout + // Load the client secrets file. + cs, err := loadClientSecrets(b.config.ClientSecretsFile) + if err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Failed parsing client secrets file: %s", err)) + } + b.config.clientSecrets = cs + // Load the private key. + b.config.privateKeyBytes, err = processPrivateKeyFile(b.config.PrivateKeyFile, b.config.Passphrase) + if err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Failed loading private key file: %s", err)) + } + // Check for any errors. + if errs != nil && len(errs.Errors) > 0 { + return nil, errs + } + return nil, nil +} + +// Run executes a googlecompute Packer build and returns a packer.Artifact +// representing a GCE machine image. +func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { + // Initialize the Google Compute Engine API. + client, err := New(b.config.ProjectId, b.config.Zone, b.config.clientSecrets, b.config.privateKeyBytes) + if err != nil { + log.Println("Failed to create the Google Compute Engine client.") + return nil, err + } + // Set up the state. + state := new(multistep.BasicStateBag) + state.Put("config", b.config) + state.Put("client", client) + state.Put("hook", hook) + state.Put("ui", ui) + // Build the steps. + steps := []multistep.Step{ + new(stepCreateSSHKey), + new(stepCreateInstance), + new(stepInstanceInfo), + &common.StepConnectSSH{ + SSHAddress: sshAddress, + SSHConfig: sshConfig, + SSHWaitTimeout: 5 * time.Minute, + }, + new(common.StepProvision), + new(stepUpdateGsutil), + new(stepCreateImage), + new(stepUploadImage), + new(stepRegisterImage), + } + // Run the steps. + if b.config.PackerDebug { + b.runner = &multistep.DebugRunner{ + Steps: steps, + PauseFn: common.MultistepDebugFn(ui), + } + } else { + b.runner = &multistep.BasicRunner{Steps: steps} + } + b.runner.Run(state) + // Report any errors. + 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?") + return nil, nil + } + artifact := &Artifact{ + imageName: state.Get("image_name").(string), + client: client, + } + return artifact, nil +} + +// Cancel. +func (b *Builder) Cancel() {} diff --git a/builder/googlecompute/builder_test.go b/builder/googlecompute/builder_test.go new file mode 100644 index 000000000..e84c36d19 --- /dev/null +++ b/builder/googlecompute/builder_test.go @@ -0,0 +1,5 @@ +// Copyright (c) 2013 Kelsey Hightower. All rights reserved. +// Use of this source code is governed by the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package googlecompute diff --git a/builder/googlecompute/client_secrets.go b/builder/googlecompute/client_secrets.go new file mode 100644 index 000000000..deb361d3e --- /dev/null +++ b/builder/googlecompute/client_secrets.go @@ -0,0 +1,34 @@ +// Copyright (c) 2013 Kelsey Hightower. All rights reserved. +// Use of this source code is governed by the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package googlecompute + +import ( + "encoding/json" + "io/ioutil" +) + +// clientSecrets represents the client secrets of a GCE service account. +type clientSecrets struct { + Web struct { + AuthURI string `json:"auth_uri"` + ClientEmail string `json:"client_email"` + ClientId string `json:"client_id"` + TokenURI string `json:"token_uri"` + } +} + +// loadClientSecrets loads the GCE client secrets file identified by path. +func loadClientSecrets(path string) (*clientSecrets, error) { + var cs *clientSecrets + secretBytes, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + err = json.Unmarshal(secretBytes, &cs) + if err != nil { + return nil, err + } + return cs, nil +} diff --git a/builder/googlecompute/private_key.go b/builder/googlecompute/private_key.go new file mode 100644 index 000000000..4200d6dce --- /dev/null +++ b/builder/googlecompute/private_key.go @@ -0,0 +1,40 @@ +// Copyright (c) 2013 Kelsey Hightower. All rights reserved. +// Use of this source code is governed by the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package googlecompute + +import ( + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "io/ioutil" +) + +// processPrivateKeyFile. +func processPrivateKeyFile(privateKeyFile, passphrase string) ([]byte, error) { + rawPrivateKeyBytes, err := ioutil.ReadFile(privateKeyFile) + if err != nil { + return nil, fmt.Errorf("Failed loading private key file: %s", err) + } + PEMBlock, _ := pem.Decode(rawPrivateKeyBytes) + if PEMBlock == nil { + return nil, fmt.Errorf("%s does not contain a vaild private key", privateKeyFile) + } + if x509.IsEncryptedPEMBlock(PEMBlock) { + if passphrase == "" { + return nil, errors.New("a passphrase must be specified when using an encrypted private key") + } + decryptedPrivateKeyBytes, err := x509.DecryptPEMBlock(PEMBlock, []byte(passphrase)) + if err != nil { + return nil, fmt.Errorf("Failed decrypting private key: %s", err) + } + b := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: decryptedPrivateKeyBytes, + } + return pem.EncodeToMemory(b), nil + } + return rawPrivateKeyBytes, nil +} diff --git a/builder/googlecompute/ssh.go b/builder/googlecompute/ssh.go new file mode 100644 index 000000000..ddfcab2de --- /dev/null +++ b/builder/googlecompute/ssh.go @@ -0,0 +1,32 @@ +package googlecompute + +import ( + "fmt" + + gossh "code.google.com/p/go.crypto/ssh" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/communicator/ssh" +) + +// sshAddress returns the ssh address. +func sshAddress(state multistep.StateBag) (string, error) { + config := state.Get("config").(config) + ipAddress := state.Get("instance_ip").(string) + return fmt.Sprintf("%s:%d", ipAddress, config.SSHPort), nil +} + +// sshConfig returns the ssh configuration. +func sshConfig(state multistep.StateBag) (*gossh.ClientConfig, error) { + config := state.Get("config").(config) + privateKey := state.Get("ssh_private_key").(string) + + keyring := new(ssh.SimpleKeychain) + if err := keyring.AddPEMKey(privateKey); err != nil { + return nil, fmt.Errorf("Error setting up SSH config: %s", err) + } + sshConfig := &gossh.ClientConfig{ + User: config.SSHUsername, + Auth: []gossh.ClientAuth{gossh.ClientAuthKeyring(keyring)}, + } + return sshConfig, nil +} diff --git a/builder/googlecompute/step_create_image.go b/builder/googlecompute/step_create_image.go new file mode 100644 index 000000000..51803de27 --- /dev/null +++ b/builder/googlecompute/step_create_image.go @@ -0,0 +1,50 @@ +// Copyright (c) 2013 Kelsey Hightower. All rights reserved. +// Use of this source code is governed by the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package googlecompute + +import ( + "fmt" + "path/filepath" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +// stepCreateImage represents a Packer build step that creates GCE machine +// images. +type stepCreateImage int + +// Run executes the Packer build step that creates a GCE machine image. +// +// Currently the only way to create a GCE image is to run the gcimagebundle +// command on the running GCE instance. +func (s *stepCreateImage) Run(state multistep.StateBag) multistep.StepAction { + var ( + config = state.Get("config").(config) + comm = state.Get("communicator").(packer.Communicator) + sudoPrefix = "" + ui = state.Get("ui").(packer.Ui) + ) + ui.Say("Creating image...") + if config.SSHUsername != "root" { + sudoPrefix = "sudo " + } + imageFilename := fmt.Sprintf("%s.tar.gz", config.ImageName) + imageBundleCmd := "/usr/bin/gcimagebundle -d /dev/sda -o /tmp/" + cmd := new(packer.RemoteCmd) + cmd.Command = fmt.Sprintf("%s%s --output_file_name %s", + sudoPrefix, imageBundleCmd, imageFilename) + err := cmd.StartWithUi(comm, ui) + if err != nil { + err := fmt.Errorf("Error creating image: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + state.Put("image_file_name", filepath.Join("/tmp", imageFilename)) + return multistep.ActionContinue +} + +func (s *stepCreateImage) Cleanup(state multistep.StateBag) {} diff --git a/builder/googlecompute/step_create_instance.go b/builder/googlecompute/step_create_instance.go new file mode 100644 index 000000000..ddad56a41 --- /dev/null +++ b/builder/googlecompute/step_create_instance.go @@ -0,0 +1,135 @@ +// Copyright (c) 2013 Kelsey Hightower. All rights reserved. +// Use of this source code is governed by the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package googlecompute + +import ( + "fmt" + + "code.google.com/p/google-api-go-client/compute/v1beta16" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/common/uuid" + "github.com/mitchellh/packer/packer" +) + +// stepCreateInstance represents a Packer build step that creates GCE instances. +type stepCreateInstance struct { + instanceName string +} + +// Run executes the Packer build step that creates a GCE instance. +func (s *stepCreateInstance) Run(state multistep.StateBag) multistep.StepAction { + var ( + client = state.Get("client").(*GoogleComputeClient) + config = state.Get("config").(config) + ui = state.Get("ui").(packer.Ui) + ) + ui.Say("Creating instance...") + name := fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID()) + // Build up the instance config. + instanceConfig := &InstanceConfig{ + Description: "New instance created by Packer", + Name: name, + } + // Validate the zone. + zone, err := client.GetZone(config.Zone) + if err != nil { + err := fmt.Errorf("Error creating instance: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + // Set the source image. Must be a fully-qualified URL. + image, err := client.GetImage(config.SourceImage) + if err != nil { + err := fmt.Errorf("Error creating instance: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + instanceConfig.Image = image.SelfLink + // Set the machineType. Must be a fully-qualified URL. + machineType, err := client.GetMachineType(config.MachineType, zone.Name) + if err != nil { + err := fmt.Errorf("Error creating instance: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + + } + instanceConfig.MachineType = machineType.SelfLink + // Set up the Network Interface. + network, err := client.GetNetwork(config.Network) + if err != nil { + err := fmt.Errorf("Error creating instance: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + networkInterface := NewNetworkInterface(network, true) + networkInterfaces := []*compute.NetworkInterface{ + networkInterface, + } + instanceConfig.NetworkInterfaces = networkInterfaces + // Add the metadata, which also setups up the ssh key. + metadata := make(map[string]string) + sshPublicKey := state.Get("ssh_public_key").(string) + metadata["sshKeys"] = fmt.Sprintf("%s:%s", config.SSHUsername, sshPublicKey) + instanceConfig.Metadata = MapToMetadata(metadata) + // Add the default service so we can create an image of the machine and + // upload it to cloud storage. + defaultServiceAccount := NewServiceAccount("default") + serviceAccounts := []*compute.ServiceAccount{ + defaultServiceAccount, + } + instanceConfig.ServiceAccounts = serviceAccounts + // Create the instance based on configuration + operation, err := client.CreateInstance(zone.Name, instanceConfig) + if err != nil { + err := fmt.Errorf("Error creating instance: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + ui.Say("Waiting for the instance to be created...") + err = waitForZoneOperationState("DONE", config.Zone, operation.Name, client, config.stateTimeout) + if err != nil { + err := fmt.Errorf("Error creating instance: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + // Update the state. + state.Put("instance_name", name) + s.instanceName = name + return multistep.ActionContinue +} + +// Cleanup destroys the GCE instance created during the image creation process. +func (s *stepCreateInstance) Cleanup(state multistep.StateBag) { + var ( + client = state.Get("client").(*GoogleComputeClient) + config = state.Get("config").(config) + ui = state.Get("ui").(packer.Ui) + ) + if s.instanceName == "" { + return + } + ui.Say("Destroying instance...") + operation, err := client.DeleteInstance(config.Zone, s.instanceName) + if err != nil { + ui.Error(fmt.Sprintf("Error destroying instance. Please destroy it manually: %v", s.instanceName)) + } + ui.Say("Waiting for the instance to be deleted...") + for { + status, err := client.ZoneOperationStatus(config.Zone, operation.Name) + if err != nil { + ui.Error(fmt.Sprintf("Error destroying instance. Please destroy it manually: %v", s.instanceName)) + } + if status == "DONE" { + break + } + } + return +} diff --git a/builder/googlecompute/step_create_ssh_key.go b/builder/googlecompute/step_create_ssh_key.go new file mode 100644 index 000000000..240fb55aa --- /dev/null +++ b/builder/googlecompute/step_create_ssh_key.go @@ -0,0 +1,55 @@ +// Copyright (c) 2013 Kelsey Hightower. All rights reserved. +// Use of this source code is governed by the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package googlecompute + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + + "code.google.com/p/go.crypto/ssh" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +// stepCreateSSHKey represents a Packer build step that generates SSH key pairs. +type stepCreateSSHKey int + +// Run executes the Packer build step that generates SSH key pairs. +func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction { + var ( + ui = state.Get("ui").(packer.Ui) + ) + ui.Say("Creating temporary ssh key for instance...") + priv, err := rsa.GenerateKey(rand.Reader, 2014) + if err != nil { + err := fmt.Errorf("Error creating temporary ssh key: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + priv_der := x509.MarshalPKCS1PrivateKey(priv) + priv_blk := pem.Block{ + Type: "RSA PRIVATE KEY", + Headers: nil, + Bytes: priv_der, + } + pub, err := ssh.NewPublicKey(&priv.PublicKey) + if err != nil { + err := fmt.Errorf("Error creating temporary ssh key: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + state.Put("ssh_private_key", string(pem.EncodeToMemory(&priv_blk))) + state.Put("ssh_public_key", string(ssh.MarshalAuthorizedKey(pub))) + return multistep.ActionContinue +} + +// Cleanup. +// Nothing to clean up. SSH keys are associated with a single GCE instance. +func (s *stepCreateSSHKey) Cleanup(state multistep.StateBag) {} diff --git a/builder/googlecompute/step_instance_info.go b/builder/googlecompute/step_instance_info.go new file mode 100644 index 000000000..7b89d0761 --- /dev/null +++ b/builder/googlecompute/step_instance_info.go @@ -0,0 +1,44 @@ +// Copyright (c) 2013 Kelsey Hightower. All rights reserved. +// Use of this source code is governed by the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package googlecompute + +import ( + "fmt" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +// stepInstanceInfo represents a Packer build step that gathers GCE instance info. +type stepInstanceInfo int + +// Run executes the Packer build step that gathers GCE instance info. +func (s *stepInstanceInfo) Run(state multistep.StateBag) multistep.StepAction { + var ( + client = state.Get("client").(*GoogleComputeClient) + config = state.Get("config").(config) + ui = state.Get("ui").(packer.Ui) + ) + instanceName := state.Get("instance_name").(string) + err := waitForInstanceState("RUNNING", config.Zone, instanceName, client, config.stateTimeout) + if err != nil { + err := fmt.Errorf("Error creating instance: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + ip, err := client.GetNatIP(config.Zone, instanceName) + if err != nil { + err := fmt.Errorf("Error retrieving instance nat ip address: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + state.Put("instance_ip", ip) + return multistep.ActionContinue +} + +// Cleanup. +func (s *stepInstanceInfo) Cleanup(state multistep.StateBag) {} diff --git a/builder/googlecompute/step_register_image.go b/builder/googlecompute/step_register_image.go new file mode 100644 index 000000000..fad55c3f9 --- /dev/null +++ b/builder/googlecompute/step_register_image.go @@ -0,0 +1,46 @@ +// Copyright (c) 2013 Kelsey Hightower. All rights reserved. +// Use of this source code is governed by the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package googlecompute + +import ( + "fmt" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +// stepRegisterImage represents a Packer build step that registers GCE machine images. +type stepRegisterImage int + +// Run executes the Packer build step that registers a GCE machine image. +func (s *stepRegisterImage) Run(state multistep.StateBag) multistep.StepAction { + var ( + client = state.Get("client").(*GoogleComputeClient) + config = state.Get("config").(config) + ui = state.Get("ui").(packer.Ui) + ) + ui.Say("Adding image to the project...") + imageURL := fmt.Sprintf("https://storage.cloud.google.com/%s/%s.tar.gz", config.BucketName, config.ImageName) + operation, err := client.CreateImage(config.ImageName, config.ImageDescription, imageURL) + if err != nil { + err := fmt.Errorf("Error creating image: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + ui.Say("Waiting for image to become available...") + err = waitForGlobalOperationState("DONE", operation.Name, client, config.stateTimeout) + if err != nil { + err := fmt.Errorf("Error creating image: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + state.Put("image_name", config.ImageName) + return multistep.ActionContinue +} + +// Cleanup. +func (s *stepRegisterImage) Cleanup(state multistep.StateBag) {} diff --git a/builder/googlecompute/step_update_gsutil.go b/builder/googlecompute/step_update_gsutil.go new file mode 100644 index 000000000..7b2d3ff4a --- /dev/null +++ b/builder/googlecompute/step_update_gsutil.go @@ -0,0 +1,49 @@ +// Copyright (c) 2013 Kelsey Hightower. All rights reserved. +// Use of this source code is governed by the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package googlecompute + +import ( + "fmt" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +// stepUpdateGsutil represents a Packer build step that updates the gsutil +// utility to the latest version available. +type stepUpdateGsutil int + +// Run executes the Packer build step that updates the gsutil utility to the +// latest version available. +// +// This step is required to prevent the image creation process from hanging; +// the image creation process utilizes the gcimagebundle cli tool which will +// prompt to update gsutil if a newer version is available. +func (s *stepUpdateGsutil) Run(state multistep.StateBag) multistep.StepAction { + var ( + config = state.Get("config").(config) + comm = state.Get("communicator").(packer.Communicator) + sudoPrefix = "" + ui = state.Get("ui").(packer.Ui) + ) + ui.Say("Updating gsutil...") + if config.SSHUsername != "root" { + sudoPrefix = "sudo " + } + gsutilUpdateCmd := "/usr/local/bin/gsutil update -n -f" + cmd := new(packer.RemoteCmd) + cmd.Command = fmt.Sprintf("%s%s", sudoPrefix, gsutilUpdateCmd) + err := cmd.StartWithUi(comm, ui) + if err != nil { + err := fmt.Errorf("Error updating gsutil: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + return multistep.ActionContinue +} + +// Cleanup. +func (s *stepUpdateGsutil) Cleanup(state multistep.StateBag) {} diff --git a/builder/googlecompute/step_upload_image.go b/builder/googlecompute/step_upload_image.go new file mode 100644 index 000000000..278fff414 --- /dev/null +++ b/builder/googlecompute/step_upload_image.go @@ -0,0 +1,44 @@ +// Copyright (c) 2013 Kelsey Hightower. All rights reserved. +// Use of this source code is governed by the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package googlecompute + +import ( + "fmt" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +// stepUploadImage represents a Packer build step that uploads GCE machine images. +type stepUploadImage int + +// Run executes the Packer build step that uploads a GCE machine image. +func (s *stepUploadImage) Run(state multistep.StateBag) multistep.StepAction { + var ( + config = state.Get("config").(config) + comm = state.Get("communicator").(packer.Communicator) + sudoPrefix = "" + ui = state.Get("ui").(packer.Ui) + imageFilename = state.Get("image_file_name").(string) + ) + ui.Say("Uploading image...") + if config.SSHUsername != "root" { + sudoPrefix = "sudo " + } + cmd := new(packer.RemoteCmd) + cmd.Command = fmt.Sprintf("%s/usr/local/bin/gsutil cp %s gs://%s", + sudoPrefix, imageFilename, config.BucketName) + err := cmd.StartWithUi(comm, ui) + if err != nil { + err := fmt.Errorf("Error uploading image: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + return multistep.ActionContinue +} + +// Cleanup. +func (s *stepUploadImage) Cleanup(state multistep.StateBag) {} diff --git a/builder/googlecompute/wait.go b/builder/googlecompute/wait.go new file mode 100644 index 000000000..8b5ed3eb0 --- /dev/null +++ b/builder/googlecompute/wait.go @@ -0,0 +1,76 @@ +// Copyright (c) 2013 Kelsey Hightower. All rights reserved. +// Use of this source code is governed by the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package googlecompute + +import ( + "fmt" + "log" + "time" +) + +// statusFunc. +type statusFunc func() (string, error) + +// waitForInstanceState. +func waitForInstanceState(desiredState string, zone string, name string, client *GoogleComputeClient, timeout time.Duration) error { + f := func() (string, error) { + return client.InstanceStatus(zone, name) + } + return waitForState("instance", desiredState, f, timeout) +} + +// waitForZoneOperationState. +func waitForZoneOperationState(desiredState string, zone string, name string, client *GoogleComputeClient, timeout time.Duration) error { + f := func() (string, error) { + return client.ZoneOperationStatus(zone, name) + } + return waitForState("operation", desiredState, f, timeout) +} + +// waitForGlobalOperationState. +func waitForGlobalOperationState(desiredState string, name string, client *GoogleComputeClient, timeout time.Duration) error { + f := func() (string, error) { + return client.GlobalOperationStatus(name) + } + return waitForState("operation", desiredState, f, timeout) +} + +// waitForState. +func waitForState(kind string, desiredState string, f statusFunc, timeout time.Duration) error { + done := make(chan struct{}) + defer close(done) + result := make(chan error, 1) + go func() { + attempts := 0 + for { + attempts += 1 + log.Printf("Checking %s state... (attempt: %d)", kind, attempts) + status, err := f() + if err != nil { + result <- err + return + } + if status == desiredState { + result <- nil + return + } + time.Sleep(3 * time.Second) + select { + case <-done: + return + default: + continue + } + } + }() + log.Printf("Waiting for up to %d seconds for %s to become %s", timeout, kind, desiredState) + select { + case err := <-result: + return err + case <-time.After(timeout): + err := fmt.Errorf("Timeout while waiting to for the %s to become '%s'", kind, desiredState) + return err + } +} diff --git a/plugin/builder-googlecompute/main.go b/plugin/builder-googlecompute/main.go new file mode 100644 index 000000000..d6a8739a2 --- /dev/null +++ b/plugin/builder-googlecompute/main.go @@ -0,0 +1,14 @@ +// Copyright (c) 2013 Kelsey Hightower. All rights reserved. +// Use of this source code is governed by the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package main + +import ( + "github.com/kelseyhightower/packer-builder-googlecompute/builder/googlecompute" + "github.com/mitchellh/packer/packer/plugin" +) + +func main() { + plugin.ServeBuilder(new(googlecompute.Builder)) +} diff --git a/plugin/builder-googlecompute/main_test.go b/plugin/builder-googlecompute/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/plugin/builder-googlecompute/main_test.go @@ -0,0 +1 @@ +package main