Commit 58c73727 authored by Kelsey Hightower's avatar Kelsey Hightower Committed by Mitchell Hashimoto

builder/googlecompute: Add new googlecompute builder

parent 9307d8a8
// 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, " ")
}
// 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)
}
// 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() {}
// 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
// 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
}
// 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
}
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
}
// 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) {}
// 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
}
// 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) {}
// 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) {}
// 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) {}
// 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) {}
// 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) {}
// 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
}
}
// 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))
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment