Commit 4f150dcc authored by Mitchell Hashimoto's avatar Mitchell Hashimoto

Merge pull request #715 from mitchellh/f-gce-builder

Google Compute Builder
parents 9307d8a8 bae9c4d2
package googlecompute
import (
"fmt"
"log"
)
// Artifact represents a GCE image as the result of a Packer build.
type Artifact struct {
imageName string
driver Driver
}
// 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)
errCh := a.driver.DeleteImage(a.imageName)
return <-errCh
}
// 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)
}
package googlecompute
import (
"github.com/mitchellh/packer/packer"
"testing"
)
func TestArtifact_impl(t *testing.T) {
var _ packer.Artifact = new(Artifact)
}
// The googlecompute package contains a packer.Builder implementation that
// builds images for Google Compute Engine.
package googlecompute
import (
"log"
"time"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
)
// The unique ID for this builder.
const BuilderId = "packer.googlecompute"
// Builder represents a Packer Builder.
type Builder struct {
config *Config
runner multistep.Runner
}
// Prepare processes the build configuration parameters.
func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
c, warnings, errs := NewConfig(raws...)
if errs != nil {
return warnings, errs
}
b.config = c
return warnings, 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) {
driver, err := NewDriverGCE(
ui, b.config.ProjectId, b.config.clientSecrets, b.config.privateKeyBytes)
if err != nil {
return nil, err
}
// Set up the state.
state := new(multistep.BasicStateBag)
state.Put("config", b.config)
state.Put("driver", driver)
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),
driver: driver,
}
return artifact, nil
}
// Cancel.
func (b *Builder) Cancel() {
if b.runner != nil {
log.Println("Cancelling the step runner...")
b.runner.Cancel()
}
}
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
}
package googlecompute
import (
"io/ioutil"
"testing"
)
func testClientSecretsFile(t *testing.T) string {
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
defer tf.Close()
if _, err := tf.Write([]byte(testClientSecretsContent)); err != nil {
t.Fatalf("err: %s", err)
}
return tf.Name()
}
func TestLoadClientSecrets(t *testing.T) {
_, err := loadClientSecrets(testClientSecretsFile(t))
if err != nil {
t.Fatalf("err: %s", err)
}
}
// This is just some dummy data that doesn't actually work (it was revoked
// a long time ago).
const testClientSecretsContent = `{"web":{"auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://accounts.google.com/o/oauth2/token","client_email":"774313886706-eorlsj0r4eqkh5e7nvea5fuf59ifr873@developer.gserviceaccount.com","client_x509_cert_url":"https://www.googleapis.com/robot/v1/metadata/x509/774313886706-eorlsj0r4eqkh5e7nvea5fuf59ifr873@developer.gserviceaccount.com","client_id":"774313886706-eorlsj0r4eqkh5e7nvea5fuf59ifr873.apps.googleusercontent.com","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs"}}`
package googlecompute
import (
"errors"
"fmt"
"time"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
)
// Config is the configuration structure for the GCE builder. It stores
// both the publicly settable state as well as the privately generated
// state of the config object.
type Config struct {
common.PackerConfig `mapstructure:",squash"`
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
instanceName string
privateKeyBytes []byte
sshTimeout time.Duration
stateTimeout time.Duration
tpl *packer.ConfigTemplate
}
func NewConfig(raws ...interface{}) (*Config, []string, error) {
c := new(Config)
md, err := common.DecodeConfig(c, raws...)
if err != nil {
return nil, nil, err
}
c.tpl, err = packer.NewConfigTemplate()
if err != nil {
return nil, nil, err
}
c.tpl.UserVars = c.PackerUserVars
// Prepare the errors
errs := common.CheckUnusedConfig(md)
// Set defaults.
if c.Network == "" {
c.Network = "default"
}
if c.ImageDescription == "" {
c.ImageDescription = "Created by Packer"
}
if c.ImageName == "" {
c.ImageName = "packer-{{timestamp}}"
}
if c.MachineType == "" {
c.MachineType = "n1-standard-1"
}
if c.RawSSHTimeout == "" {
c.RawSSHTimeout = "5m"
}
if c.RawStateTimeout == "" {
c.RawStateTimeout = "5m"
}
if c.SSHUsername == "" {
c.SSHUsername = "root"
}
if c.SSHPort == 0 {
c.SSHPort = 22
}
// Process Templates
templates := map[string]*string{
"bucket_name": &c.BucketName,
"client_secrets_file": &c.ClientSecretsFile,
"image_name": &c.ImageName,
"image_description": &c.ImageDescription,
"machine_type": &c.MachineType,
"network": &c.Network,
"passphrase": &c.Passphrase,
"private_key_file": &c.PrivateKeyFile,
"project_id": &c.ProjectId,
"source_image": &c.SourceImage,
"ssh_username": &c.SSHUsername,
"ssh_timeout": &c.RawSSHTimeout,
"state_timeout": &c.RawStateTimeout,
"zone": &c.Zone,
}
for n, ptr := range templates {
var err error
*ptr, err = c.tpl.Process(*ptr, nil)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error processing %s: %s", n, err))
}
}
// Process required parameters.
if c.BucketName == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("a bucket_name must be specified"))
}
if c.ClientSecretsFile == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("a client_secrets_file must be specified"))
}
if c.PrivateKeyFile == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("a private_key_file must be specified"))
}
if c.ProjectId == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("a project_id must be specified"))
}
if c.SourceImage == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("a source_image must be specified"))
}
if c.Zone == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("a zone must be specified"))
}
// Process timeout settings.
sshTimeout, err := time.ParseDuration(c.RawSSHTimeout)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
}
c.sshTimeout = sshTimeout
stateTimeout, err := time.ParseDuration(c.RawStateTimeout)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing state_timeout: %s", err))
}
c.stateTimeout = stateTimeout
// Load the client secrets file.
cs, err := loadClientSecrets(c.ClientSecretsFile)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing client secrets file: %s", err))
}
c.clientSecrets = cs
// Load the private key.
c.privateKeyBytes, err = processPrivateKeyFile(c.PrivateKeyFile, c.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, nil, errs
}
return c, nil, nil
}
package googlecompute
import (
"testing"
)
func testConfig(t *testing.T) map[string]interface{} {
return map[string]interface{}{
"bucket_name": "foo",
"client_secrets_file": testClientSecretsFile(t),
"private_key_file": testPrivateKeyFile(t),
"project_id": "hashicorp",
"source_image": "foo",
"zone": "us-east-1a",
}
}
func testConfigStruct(t *testing.T) *Config {
c, warns, errs := NewConfig(testConfig(t))
if len(warns) > 0 {
t.Fatalf("bad: %#v", len(warns))
}
if errs != nil {
t.Fatalf("bad: %#v", errs)
}
return c
}
func testConfigErr(t *testing.T, warns []string, err error, extra string) {
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatalf("should error: %s", extra)
}
}
func testConfigOk(t *testing.T, warns []string, err error) {
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("bad: %s", err)
}
}
func TestConfigPrepare(t *testing.T) {
cases := []struct {
Key string
Value interface{}
Err bool
}{
{
"unknown_key",
"bad",
true,
},
{
"bucket_name",
nil,
true,
},
{
"bucket_name",
"good",
false,
},
{
"client_secrets_file",
nil,
true,
},
{
"client_secrets_file",
testClientSecretsFile(t),
false,
},
{
"client_secrets_file",
"/tmp/i/should/not/exist",
true,
},
{
"private_key_file",
nil,
true,
},
{
"private_key_file",
testPrivateKeyFile(t),
false,
},
{
"private_key_file",
"/tmp/i/should/not/exist",
true,
},
{
"project_id",
nil,
true,
},
{
"project_id",
"foo",
false,
},
{
"source_image",
nil,
true,
},
{
"source_image",
"foo",
false,
},
{
"zone",
nil,
true,
},
{
"zone",
"foo",
false,
},
{
"ssh_timeout",
"SO BAD",
true,
},
{
"ssh_timeout",
"5s",
false,
},
{
"state_timeout",
"SO BAD",
true,
},
{
"state_timeout",
"5s",
false,
},
}
for _, tc := range cases {
raw := testConfig(t)
if tc.Value == nil {
delete(raw, tc.Key)
} else {
raw[tc.Key] = tc.Value
}
_, warns, errs := NewConfig(raw)
if tc.Err {
testConfigErr(t, warns, errs, tc.Key)
} else {
testConfigOk(t, warns, errs)
}
}
}
package googlecompute
// Driver is the interface that has to be implemented to communicate
// with GCE. The Driver interface exists mostly to allow a mock implementation
// to be used to test the steps.
type Driver interface {
// CreateImage creates an image with the given URL in Google Storage.
CreateImage(name, description, url string) <-chan error
// DeleteImage deletes the image with the given name.
DeleteImage(name string) <-chan error
// DeleteInstance deletes the given instance.
DeleteInstance(zone, name string) (<-chan error, error)
// GetNatIP gets the NAT IP address for the instance.
GetNatIP(zone, name string) (string, error)
// RunInstance takes the given config and launches an instance.
RunInstance(*InstanceConfig) (<-chan error, error)
// WaitForInstance waits for an instance to reach the given state.
WaitForInstance(state, zone, name string) <-chan error
}
type InstanceConfig struct {
Description string
Image string
MachineType string
Metadata map[string]string
Name string
Network string
Tags []string
Zone string
}
package googlecompute
import (
"fmt"
"log"
"net/http"
"time"
"code.google.com/p/goauth2/oauth"
"code.google.com/p/goauth2/oauth/jwt"
"code.google.com/p/google-api-go-client/compute/v1beta16"
"github.com/mitchellh/packer/packer"
)
// driverGCE is a Driver implementation that actually talks to GCE.
// Create an instance using NewDriverGCE.
type driverGCE struct {
projectId string
service *compute.Service
ui packer.Ui
}
const DriverScopes string = "https://www.googleapis.com/auth/compute " +
"https://www.googleapis.com/auth/devstorage.full_control"
func NewDriverGCE(ui packer.Ui, projectId string, c *clientSecrets, key []byte) (Driver, error) {
log.Printf("[INFO] Requesting token...")
log.Printf("[INFO] -- Email: %s", c.Web.ClientEmail)
log.Printf("[INFO] -- Scopes: %s", DriverScopes)
log.Printf("[INFO] -- Private Key Length: %d", len(key))
log.Printf("[INFO] -- Token URL: %s", c.Web.TokenURI)
jwtTok := jwt.NewToken(c.Web.ClientEmail, DriverScopes, key)
jwtTok.ClaimSet.Aud = c.Web.TokenURI
token, err := jwtTok.Assert(new(http.Client))
if err != nil {
return nil, err
}
transport := &oauth.Transport{
Config: &oauth.Config{
ClientId: c.Web.ClientId,
Scope: DriverScopes,
TokenURL: c.Web.TokenURI,
AuthURL: c.Web.AuthURI,
},
Token: token,
}
log.Printf("[INFO] Instantiating client...")
service, err := compute.New(transport.Client())
if err != nil {
return nil, err
}
return &driverGCE{
projectId: projectId,
service: service,
ui: ui,
}, nil
}
func (d *driverGCE) CreateImage(name, description, url string) <-chan error {
image := &compute.Image{
Description: description,
Name: name,
RawDisk: &compute.ImageRawDisk{
ContainerType: "TAR",
Source: url,
},
SourceType: "RAW",
}
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))
}
return errCh
}
func (d *driverGCE) DeleteImage(name string) <-chan error {
errCh := make(chan error, 1)
op, err := d.service.Images.Delete(d.projectId, name).Do()
if err != nil {
errCh <- err
} else {
go waitForState(errCh, "DONE", d.refreshGlobalOp(op))
}
return errCh
}
func (d *driverGCE) DeleteInstance(zone, name string) (<-chan error, error) {
op, err := d.service.Instances.Delete(d.projectId, zone, name).Do()
if err != nil {
return nil, err
}
errCh := make(chan error, 1)
go waitForState(errCh, "DONE", d.refreshZoneOp(zone, op))
return errCh, nil
}
func (d *driverGCE) GetNatIP(zone, name string) (string, error) {
instance, err := d.service.Instances.Get(d.projectId, zone, name).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
}
func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) {
// Get the zone
d.ui.Message(fmt.Sprintf("Loading zone: %s", c.Zone))
zone, err := d.service.Zones.Get(d.projectId, c.Zone).Do()
if err != nil {
return nil, err
}
// Get the image
d.ui.Message(fmt.Sprintf("Loading image: %s", c.Image))
image, err := d.getImage(c.Image)
if err != nil {
return nil, err
}
// Get the machine type
d.ui.Message(fmt.Sprintf("Loading machine type: %s", c.MachineType))
machineType, err := d.service.MachineTypes.Get(
d.projectId, zone.Name, c.MachineType).Do()
if err != nil {
return nil, err
}
// TODO(mitchellh): deprecation warnings
// Get the network
d.ui.Message(fmt.Sprintf("Loading network: %s", c.Network))
network, err := d.service.Networks.Get(d.projectId, c.Network).Do()
if err != nil {
return nil, err
}
// Build up the metadata
metadata := make([]*compute.MetadataItems, len(c.Metadata))
for k, v := range c.Metadata {
metadata = append(metadata, &compute.MetadataItems{
Key: k,
Value: v,
})
}
// Create the instance information
instance := compute.Instance{
Description: c.Description,
Image: image.SelfLink,
MachineType: machineType.SelfLink,
Metadata: &compute.Metadata{
Items: metadata,
},
Name: c.Name,
NetworkInterfaces: []*compute.NetworkInterface{
&compute.NetworkInterface{
AccessConfigs: []*compute.AccessConfig{
&compute.AccessConfig{
Name: "AccessConfig created by Packer",
Type: "ONE_TO_ONE_NAT",
},
},
Network: network.SelfLink,
},
},
ServiceAccounts: []*compute.ServiceAccount{
&compute.ServiceAccount{
Email: "default",
Scopes: []string{
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/compute",
"https://www.googleapis.com/auth/devstorage.full_control",
},
},
},
Tags: &compute.Tags{
Items: c.Tags,
},
}
d.ui.Message("Requesting instance creation...")
op, err := d.service.Instances.Insert(d.projectId, zone.Name, &instance).Do()
if err != nil {
return nil, err
}
errCh := make(chan error, 1)
go waitForState(errCh, "DONE", d.refreshZoneOp(zone.Name, op))
return errCh, nil
}
func (d *driverGCE) WaitForInstance(state, zone, name string) <-chan error {
errCh := make(chan error, 1)
go waitForState(errCh, state, d.refreshInstanceState(zone, name))
return errCh
}
func (d *driverGCE) getImage(name string) (image *compute.Image, err error) {
projects := []string{d.projectId, "debian-cloud", "centos-cloud"}
for _, project := range projects {
image, err = d.service.Images.Get(project, name).Do()
if err == nil && image != nil && image.SelfLink != "" {
return
}
image = nil
}
if err == nil {
err = fmt.Errorf("Image could not be found: %s", name)
}
return
}
func (d *driverGCE) refreshInstanceState(zone, name string) stateRefreshFunc {
return func() (string, error) {
instance, err := d.service.Instances.Get(d.projectId, zone, name).Do()
if err != nil {
return "", err
}
return instance.Status, nil
}
}
func (d *driverGCE) refreshGlobalOp(op *compute.Operation) stateRefreshFunc {
return func() (string, error) {
newOp, err := d.service.GlobalOperations.Get(d.projectId, op.Name).Do()
if err != nil {
return "", err
}
// If the op is done, check for errors
err = nil
if newOp.Status == "DONE" {
if newOp.Error != nil {
for _, e := range newOp.Error.Errors {
err = packer.MultiErrorAppend(err, fmt.Errorf(e.Message))
}
}
}
return newOp.Status, err
}
}
func (d *driverGCE) refreshZoneOp(zone string, op *compute.Operation) stateRefreshFunc {
return func() (string, error) {
newOp, err := d.service.ZoneOperations.Get(d.projectId, zone, op.Name).Do()
if err != nil {
return "", err
}
// If the op is done, check for errors
err = nil
if newOp.Status == "DONE" {
if newOp.Error != nil {
for _, e := range newOp.Error.Errors {
err = packer.MultiErrorAppend(err, fmt.Errorf(e.Message))
}
}
}
return newOp.Status, err
}
}
// stateRefreshFunc is used to refresh the state of a thing and is
// used in conjunction with waitForState.
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 {
state, err := refresh()
if err != nil {
errCh <- err
return
}
if state == target {
errCh <- nil
return
}
time.Sleep(2 * time.Second)
}
}
package googlecompute
// DriverMock is a Driver implementation that is a mocked out so that
// it can be used for tests.
type DriverMock struct {
CreateImageName string
CreateImageDesc string
CreateImageURL string
CreateImageErrCh <-chan error
DeleteImageName string
DeleteImageErrCh <-chan error
DeleteInstanceZone string
DeleteInstanceName string
DeleteInstanceErrCh <-chan error
DeleteInstanceErr error
GetNatIPZone string
GetNatIPName string
GetNatIPResult string
GetNatIPErr error
RunInstanceConfig *InstanceConfig
RunInstanceErrCh <-chan error
RunInstanceErr error
WaitForInstanceState string
WaitForInstanceZone string
WaitForInstanceName string
WaitForInstanceErrCh <-chan error
}
func (d *DriverMock) CreateImage(name, description, url string) <-chan error {
d.CreateImageName = name
d.CreateImageDesc = description
d.CreateImageURL = url
resultCh := d.CreateImageErrCh
if resultCh == nil {
ch := make(chan error)
close(ch)
resultCh = ch
}
return resultCh
}
func (d *DriverMock) DeleteImage(name string) <-chan error {
d.DeleteImageName = name
resultCh := d.DeleteImageErrCh
if resultCh == nil {
ch := make(chan error)
close(ch)
resultCh = ch
}
return resultCh
}
func (d *DriverMock) DeleteInstance(zone, name string) (<-chan error, error) {
d.DeleteInstanceZone = zone
d.DeleteInstanceName = name
resultCh := d.DeleteInstanceErrCh
if resultCh == nil {
ch := make(chan error)
close(ch)
resultCh = ch
}
return resultCh, d.DeleteInstanceErr
}
func (d *DriverMock) GetNatIP(zone, name string) (string, error) {
d.GetNatIPZone = zone
d.GetNatIPName = name
return d.GetNatIPResult, d.GetNatIPErr
}
func (d *DriverMock) RunInstance(c *InstanceConfig) (<-chan error, error) {
d.RunInstanceConfig = c
resultCh := d.RunInstanceErrCh
if resultCh == nil {
ch := make(chan error)
close(ch)
resultCh = ch
}
return resultCh, d.RunInstanceErr
}
func (d *DriverMock) WaitForInstance(state, zone, name string) <-chan error {
d.WaitForInstanceState = state
d.WaitForInstanceZone = zone
d.WaitForInstanceName = name
resultCh := d.WaitForInstanceErrCh
if resultCh == nil {
ch := make(chan error)
close(ch)
resultCh = ch
}
return resultCh
}
package googlecompute
import (
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
)
// processPrivateKeyFile takes a private key file and an optional passphrase
// and decodes it to a byte slice.
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 (
"crypto/rand"
"crypto/x509"
"encoding/pem"
"io/ioutil"
"os"
"testing"
)
func testPrivateKeyFile(t *testing.T) string {
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("bad: %s", err)
}
defer tf.Close()
b := &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: []byte("what"),
}
if err := pem.Encode(tf, b); err != nil {
t.Fatalf("err: %s", err)
}
return tf.Name()
}
func TestProcesssPrivateKeyFile(t *testing.T) {
path := testPrivateKeyFile(t)
defer os.Remove(path)
data, err := processPrivateKeyFile(path, "")
if err != nil {
t.Fatalf("err: %s", err)
}
if len(data) <= 0 {
t.Fatalf("bad: %#v", data)
}
}
func TestProcessPrivateKeyFile_encrypted(t *testing.T) {
// Encrypt the file
b, err := x509.EncryptPEMBlock(rand.Reader,
"RSA PRIVATE KEY",
[]byte("what"),
[]byte("password"),
x509.PEMCipherAES128)
if err != nil {
t.Fatalf("err: %s", err)
}
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("bad: %s", err)
}
defer os.Remove(tf.Name())
err = pem.Encode(tf, b)
tf.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
path := tf.Name()
// Should have an error with a bad password
if _, err := processPrivateKeyFile(path, "bad"); err == nil {
t.Fatal("should error")
}
if _, err := processPrivateKeyFile(path, "password"); err != nil {
t.Fatalf("bad: %s", err)
}
}
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
}
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 {
config := state.Get("config").(*Config)
comm := state.Get("communicator").(packer.Communicator)
ui := state.Get("ui").(packer.Ui)
sudoPrefix := ""
if config.SSHUsername != "root" {
sudoPrefix = "sudo "
}
imageFilename := fmt.Sprintf("%s.tar.gz", config.ImageName)
imageBundleCmd := "/usr/bin/gcimagebundle -d /dev/sda -o /tmp/"
ui.Say("Creating image...")
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 && cmd.ExitStatus != 0 {
err = fmt.Errorf(
"gcimagebundle exited with non-zero exit status: %d", cmd.ExitStatus)
}
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) {}
package googlecompute
import (
"strings"
"testing"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
func TestStepCreateImage_impl(t *testing.T) {
var _ multistep.Step = new(StepCreateImage)
}
func TestStepCreateImage(t *testing.T) {
state := testState(t)
step := new(StepCreateImage)
defer step.Cleanup(state)
comm := new(packer.MockCommunicator)
state.Put("communicator", comm)
// run the step
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
// Verify
if !comm.StartCalled {
t.Fatal("start should be called")
}
if strings.HasPrefix(comm.StartCmd.Command, "sudo") {
t.Fatal("should not sudo")
}
if !strings.Contains(comm.StartCmd.Command, "gcimagebundle") {
t.Fatalf("bad command: %#v", comm.StartCmd.Command)
}
if _, ok := state.GetOk("image_file_name"); !ok {
t.Fatal("should have image")
}
}
func TestStepCreateImage_badExitStatus(t *testing.T) {
state := testState(t)
step := new(StepCreateImage)
defer step.Cleanup(state)
comm := new(packer.MockCommunicator)
comm.StartExitStatus = 12
state.Put("communicator", comm)
// run the step
if action := step.Run(state); action != multistep.ActionHalt {
t.Fatalf("bad action: %#v", action)
}
if _, ok := state.GetOk("error"); !ok {
t.Fatal("should have error")
}
if _, ok := state.GetOk("image_file_name"); ok {
t.Fatal("should NOT have image")
}
}
func TestStepCreateImage_nonRoot(t *testing.T) {
state := testState(t)
step := new(StepCreateImage)
defer step.Cleanup(state)
comm := new(packer.MockCommunicator)
state.Put("communicator", comm)
config := state.Get("config").(*Config)
config.SSHUsername = "bob"
// run the step
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
// Verify
if !comm.StartCalled {
t.Fatal("start should be called")
}
if !strings.HasPrefix(comm.StartCmd.Command, "sudo") {
t.Fatal("should sudo")
}
if !strings.Contains(comm.StartCmd.Command, "gcimagebundle") {
t.Fatalf("bad command: %#v", comm.StartCmd.Command)
}
if _, ok := state.GetOk("image_file_name"); !ok {
t.Fatal("should have image")
}
}
package googlecompute
import (
"errors"
"fmt"
"time"
"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 {
config := state.Get("config").(*Config)
driver := state.Get("driver").(Driver)
sshPublicKey := state.Get("ssh_public_key").(string)
ui := state.Get("ui").(packer.Ui)
ui.Say("Creating instance...")
name := fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
errCh, err := driver.RunInstance(&InstanceConfig{
Description: "New instance created by Packer",
Image: config.SourceImage,
MachineType: config.MachineType,
Metadata: map[string]string{
"sshKeys": fmt.Sprintf("%s:%s", config.SSHUsername, sshPublicKey),
},
Name: name,
Network: config.Network,
Tags: config.Tags,
Zone: config.Zone,
})
if err == nil {
ui.Message("Waiting for creation operation to complete...")
select {
case err = <-errCh:
case <-time.After(config.stateTimeout):
err = errors.New("time out while waiting for instance to create")
}
}
if err != nil {
err := fmt.Errorf("Error creating instance: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Message("Instance has been created!")
// Things succeeded, store the name so we can remove it later
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) {
if s.instanceName == "" {
return
}
config := state.Get("config").(*Config)
driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui)
ui.Say("Deleting instance...")
errCh, err := driver.DeleteInstance(config.Zone, s.instanceName)
if err == nil {
select {
case err = <-errCh:
case <-time.After(config.stateTimeout):
err = errors.New("time out while waiting for instance to delete")
}
}
if err != nil {
ui.Error(fmt.Sprintf(
"Error deleting instance. Please delete it manually.\n\n"+
"Name: %s\n"+
"Error: %s", s.instanceName, err))
}
s.instanceName = ""
return
}
package googlecompute
import (
"errors"
"github.com/mitchellh/multistep"
"testing"
"time"
)
func TestStepCreateInstance_impl(t *testing.T) {
var _ multistep.Step = new(StepCreateInstance)
}
func TestStepCreateInstance(t *testing.T) {
state := testState(t)
step := new(StepCreateInstance)
defer step.Cleanup(state)
state.Put("ssh_public_key", "key")
config := state.Get("config").(*Config)
driver := state.Get("driver").(*DriverMock)
// run the step
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
// Verify state
nameRaw, ok := state.GetOk("instance_name")
if !ok {
t.Fatal("should have instance name")
}
// cleanup
step.Cleanup(state)
if driver.DeleteInstanceName != nameRaw.(string) {
t.Fatal("should've deleted instance")
}
if driver.DeleteInstanceZone != config.Zone {
t.Fatal("bad zone: %#v", driver.DeleteInstanceZone)
}
}
func TestStepCreateInstance_error(t *testing.T) {
state := testState(t)
step := new(StepCreateInstance)
defer step.Cleanup(state)
state.Put("ssh_public_key", "key")
driver := state.Get("driver").(*DriverMock)
driver.RunInstanceErr = errors.New("error")
// run the step
if action := step.Run(state); action != multistep.ActionHalt {
t.Fatalf("bad action: %#v", action)
}
// Verify state
if _, ok := state.GetOk("error"); !ok {
t.Fatal("should have error")
}
if _, ok := state.GetOk("instance_name"); ok {
t.Fatal("should NOT have instance name")
}
}
func TestStepCreateInstance_errorOnChannel(t *testing.T) {
state := testState(t)
step := new(StepCreateInstance)
defer step.Cleanup(state)
errCh := make(chan error, 1)
errCh <- errors.New("error")
state.Put("ssh_public_key", "key")
driver := state.Get("driver").(*DriverMock)
driver.RunInstanceErrCh = errCh
// run the step
if action := step.Run(state); action != multistep.ActionHalt {
t.Fatalf("bad action: %#v", action)
}
// Verify state
if _, ok := state.GetOk("error"); !ok {
t.Fatal("should have error")
}
if _, ok := state.GetOk("instance_name"); ok {
t.Fatal("should NOT have instance name")
}
}
func TestStepCreateInstance_errorTimeout(t *testing.T) {
state := testState(t)
step := new(StepCreateInstance)
defer step.Cleanup(state)
errCh := make(chan error, 1)
go func() {
<-time.After(10 * time.Millisecond)
errCh <- nil
}()
state.Put("ssh_public_key", "key")
config := state.Get("config").(*Config)
config.stateTimeout = 1 * time.Microsecond
driver := state.Get("driver").(*DriverMock)
driver.RunInstanceErrCh = errCh
// run the step
if action := step.Run(state); action != multistep.ActionHalt {
t.Fatalf("bad action: %#v", action)
}
// Verify state
if _, ok := state.GetOk("error"); !ok {
t.Fatal("should have error")
}
if _, ok := state.GetOk("instance_name"); ok {
t.Fatal("should NOT have instance name")
}
}
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 {
ui := state.Get("ui").(packer.Ui)
ui.Say("Creating temporary SSH key for instance...")
priv, err := rsa.GenerateKey(rand.Reader, 2048)
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_blk := pem.Block{
Type: "RSA PRIVATE KEY",
Headers: nil,
Bytes: x509.MarshalPKCS1PrivateKey(priv),
}
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
}
// Nothing to clean up. SSH keys are associated with a single GCE instance.
func (s *StepCreateSSHKey) Cleanup(state multistep.StateBag) {}
package googlecompute
import (
"github.com/mitchellh/multistep"
"testing"
)
func TestStepCreateSSHKey_impl(t *testing.T) {
var _ multistep.Step = new(StepCreateSSHKey)
}
func TestStepCreateSSHKey(t *testing.T) {
state := testState(t)
step := new(StepCreateSSHKey)
defer step.Cleanup(state)
// run the step
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
// Verify that we have a public/private key
if _, ok := state.GetOk("ssh_private_key"); !ok {
t.Fatal("should have key")
}
if _, ok := state.GetOk("ssh_public_key"); !ok {
t.Fatal("should have key")
}
}
package googlecompute
import (
"errors"
"fmt"
"time"
"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 {
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 the instance to become running...")
errCh := driver.WaitForInstance("RUNNING", config.Zone, instanceName)
var err error
select {
case err = <-errCh:
case <-time.After(config.stateTimeout):
err = errors.New("time out while waiting for instance to become running")
}
if err != nil {
err := fmt.Errorf("Error waiting for instance: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ip, err := driver.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
}
ui.Message(fmt.Sprintf("IP: %s", ip))
state.Put("instance_ip", ip)
return multistep.ActionContinue
}
// Cleanup.
func (s *StepInstanceInfo) Cleanup(state multistep.StateBag) {}
package googlecompute
import (
"errors"
"github.com/mitchellh/multistep"
"testing"
"time"
)
func TestStepInstanceInfo_impl(t *testing.T) {
var _ multistep.Step = new(StepInstanceInfo)
}
func TestStepInstanceInfo(t *testing.T) {
state := testState(t)
step := new(StepInstanceInfo)
defer step.Cleanup(state)
state.Put("instance_name", "foo")
config := state.Get("config").(*Config)
driver := state.Get("driver").(*DriverMock)
driver.GetNatIPResult = "1.2.3.4"
// run the step
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
// Verify state
if driver.WaitForInstanceState != "RUNNING" {
t.Fatalf("bad: %#v", driver.WaitForInstanceState)
}
if driver.WaitForInstanceZone != config.Zone {
t.Fatalf("bad: %#v", driver.WaitForInstanceZone)
}
if driver.WaitForInstanceName != "foo" {
t.Fatalf("bad: %#v", driver.WaitForInstanceName)
}
ipRaw, ok := state.GetOk("instance_ip")
if !ok {
t.Fatal("should have ip")
}
if ip, ok := ipRaw.(string); !ok {
t.Fatal("ip is not a string")
} else if ip != "1.2.3.4" {
t.Fatalf("bad ip: %s", ip)
}
}
func TestStepInstanceInfo_getNatIPError(t *testing.T) {
state := testState(t)
step := new(StepInstanceInfo)
defer step.Cleanup(state)
state.Put("instance_name", "foo")
driver := state.Get("driver").(*DriverMock)
driver.GetNatIPErr = errors.New("error")
// run the step
if action := step.Run(state); action != multistep.ActionHalt {
t.Fatalf("bad action: %#v", action)
}
// Verify state
if _, ok := state.GetOk("error"); !ok {
t.Fatal("should have error")
}
if _, ok := state.GetOk("instance_ip"); ok {
t.Fatal("should NOT have instance IP")
}
}
func TestStepInstanceInfo_waitError(t *testing.T) {
state := testState(t)
step := new(StepInstanceInfo)
defer step.Cleanup(state)
state.Put("instance_name", "foo")
errCh := make(chan error, 1)
errCh <- errors.New("error")
driver := state.Get("driver").(*DriverMock)
driver.WaitForInstanceErrCh = errCh
// run the step
if action := step.Run(state); action != multistep.ActionHalt {
t.Fatalf("bad action: %#v", action)
}
// Verify state
if _, ok := state.GetOk("error"); !ok {
t.Fatal("should have error")
}
if _, ok := state.GetOk("instance_ip"); ok {
t.Fatal("should NOT have instance IP")
}
}
func TestStepInstanceInfo_errorTimeout(t *testing.T) {
state := testState(t)
step := new(StepInstanceInfo)
defer step.Cleanup(state)
errCh := make(chan error, 1)
go func() {
<-time.After(10 * time.Millisecond)
errCh <- nil
}()
state.Put("instance_name", "foo")
config := state.Get("config").(*Config)
config.stateTimeout = 1 * time.Microsecond
driver := state.Get("driver").(*DriverMock)
driver.WaitForInstanceErrCh = errCh
// run the step
if action := step.Run(state); action != multistep.ActionHalt {
t.Fatalf("bad action: %#v", action)
}
// Verify state
if _, ok := state.GetOk("error"); !ok {
t.Fatal("should have error")
}
if _, ok := state.GetOk("instance_ip"); ok {
t.Fatal("should NOT have instance IP")
}
}
package googlecompute
import (
"errors"
"fmt"
"time"
"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 {
config := state.Get("config").(*Config)
driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui)
var err error
imageURL := fmt.Sprintf(
"https://storage.cloud.google.com/%s/%s.tar.gz",
config.BucketName, config.ImageName)
ui.Say("Registering image...")
errCh := driver.CreateImage(config.ImageName, config.ImageDescription, imageURL)
select {
case err = <-errCh:
case <-time.After(config.stateTimeout):
err = errors.New("time out while waiting for image to register")
}
if err != nil {
err := fmt.Errorf("Error waiting for 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) {}
package googlecompute
import (
"errors"
"github.com/mitchellh/multistep"
"testing"
"time"
)
func TestStepRegisterImage_impl(t *testing.T) {
var _ multistep.Step = new(StepRegisterImage)
}
func TestStepRegisterImage(t *testing.T) {
state := testState(t)
step := new(StepRegisterImage)
defer step.Cleanup(state)
config := state.Get("config").(*Config)
driver := state.Get("driver").(*DriverMock)
// run the step
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
// Verify state
if driver.CreateImageName != config.ImageName {
t.Fatalf("bad: %#v", driver.CreateImageName)
}
if driver.CreateImageDesc != config.ImageDescription {
t.Fatalf("bad: %#v", driver.CreateImageDesc)
}
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 TestStepRegisterImage_waitError(t *testing.T) {
state := testState(t)
step := new(StepRegisterImage)
defer step.Cleanup(state)
errCh := make(chan error, 1)
errCh <- errors.New("error")
driver := state.Get("driver").(*DriverMock)
driver.CreateImageErrCh = errCh
// run the step
if action := step.Run(state); action != multistep.ActionHalt {
t.Fatalf("bad action: %#v", action)
}
// Verify state
if _, ok := state.GetOk("error"); !ok {
t.Fatal("should have error")
}
if _, ok := state.GetOk("image_name"); ok {
t.Fatal("should NOT have image_name")
}
}
func TestStepRegisterImage_errorTimeout(t *testing.T) {
state := testState(t)
step := new(StepRegisterImage)
defer step.Cleanup(state)
errCh := make(chan error, 1)
go func() {
<-time.After(10 * time.Millisecond)
errCh <- nil
}()
config := state.Get("config").(*Config)
config.stateTimeout = 1 * time.Microsecond
driver := state.Get("driver").(*DriverMock)
driver.CreateImageErrCh = errCh
// run the step
if action := step.Run(state); action != multistep.ActionHalt {
t.Fatalf("bad action: %#v", action)
}
// Verify state
if _, ok := state.GetOk("error"); !ok {
t.Fatal("should have error")
}
if _, ok := state.GetOk("image_name"); ok {
t.Fatal("should NOT have image name")
}
}
package googlecompute
import (
"bytes"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"testing"
)
func testState(t *testing.T) multistep.StateBag {
state := new(multistep.BasicStateBag)
state.Put("config", testConfigStruct(t))
state.Put("driver", &DriverMock{})
state.Put("hook", &packer.MockHook{})
state.Put("ui", &packer.BasicUi{
Reader: new(bytes.Buffer),
Writer: new(bytes.Buffer),
})
return state
}
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 {
comm := state.Get("communicator").(packer.Communicator)
config := state.Get("config").(*Config)
ui := state.Get("ui").(packer.Ui)
sudoPrefix := ""
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)
ui.Say("Updating gsutil...")
err := cmd.StartWithUi(comm, ui)
if err == nil && cmd.ExitStatus != 0 {
err = fmt.Errorf(
"gsutil update exited with non-zero exit status: %d", cmd.ExitStatus)
}
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) {}
package googlecompute
import (
"strings"
"testing"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
func TestStepUpdateGsutil_impl(t *testing.T) {
var _ multistep.Step = new(StepUpdateGsutil)
}
func TestStepUpdateGsutil(t *testing.T) {
state := testState(t)
step := new(StepUpdateGsutil)
defer step.Cleanup(state)
comm := new(packer.MockCommunicator)
state.Put("communicator", comm)
// run the step
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
// Verify
if !comm.StartCalled {
t.Fatal("start should be called")
}
if strings.HasPrefix(comm.StartCmd.Command, "sudo") {
t.Fatal("should not sudo")
}
if !strings.Contains(comm.StartCmd.Command, "gsutil update") {
t.Fatalf("bad command: %#v", comm.StartCmd.Command)
}
}
func TestStepUpdateGsutil_badExitStatus(t *testing.T) {
state := testState(t)
step := new(StepUpdateGsutil)
defer step.Cleanup(state)
comm := new(packer.MockCommunicator)
comm.StartExitStatus = 12
state.Put("communicator", comm)
// run the step
if action := step.Run(state); action != multistep.ActionHalt {
t.Fatalf("bad action: %#v", action)
}
if _, ok := state.GetOk("error"); !ok {
t.Fatal("should have error")
}
}
func TestStepUpdateGsutil_nonRoot(t *testing.T) {
state := testState(t)
step := new(StepUpdateGsutil)
defer step.Cleanup(state)
comm := new(packer.MockCommunicator)
state.Put("communicator", comm)
config := state.Get("config").(*Config)
config.SSHUsername = "bob"
// run the step
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
// Verify
if !comm.StartCalled {
t.Fatal("start should be called")
}
if !strings.HasPrefix(comm.StartCmd.Command, "sudo") {
t.Fatal("should sudo")
}
if !strings.Contains(comm.StartCmd.Command, "gsutil update") {
t.Fatalf("bad command: %#v", comm.StartCmd.Command)
}
}
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 {
comm := state.Get("communicator").(packer.Communicator)
config := state.Get("config").(*Config)
imageFilename := state.Get("image_file_name").(string)
ui := state.Get("ui").(packer.Ui)
sudoPrefix := ""
if config.SSHUsername != "root" {
sudoPrefix = "sudo "
}
ui.Say("Uploading image...")
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 && cmd.ExitStatus != 0 {
err = fmt.Errorf(
"gsutil exited with non-zero exit status: %d", cmd.ExitStatus)
}
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) {}
package googlecompute
import (
"strings"
"testing"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
func TestStepUploadImage_impl(t *testing.T) {
var _ multistep.Step = new(StepUploadImage)
}
func TestStepUploadImage(t *testing.T) {
state := testState(t)
step := new(StepUploadImage)
defer step.Cleanup(state)
comm := new(packer.MockCommunicator)
state.Put("communicator", comm)
state.Put("image_file_name", "foo")
// run the step
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
// Verify
if !comm.StartCalled {
t.Fatal("start should be called")
}
if strings.HasPrefix(comm.StartCmd.Command, "sudo") {
t.Fatal("should not sudo")
}
if !strings.Contains(comm.StartCmd.Command, "gsutil cp") {
t.Fatalf("bad command: %#v", comm.StartCmd.Command)
}
}
func TestStepUploadImage_badExitStatus(t *testing.T) {
state := testState(t)
step := new(StepUploadImage)
defer step.Cleanup(state)
comm := new(packer.MockCommunicator)
comm.StartExitStatus = 12
state.Put("communicator", comm)
state.Put("image_file_name", "foo")
// run the step
if action := step.Run(state); action != multistep.ActionHalt {
t.Fatalf("bad action: %#v", action)
}
if _, ok := state.GetOk("error"); !ok {
t.Fatal("should have error")
}
}
func TestStepUploadImage_nonRoot(t *testing.T) {
state := testState(t)
step := new(StepUploadImage)
defer step.Cleanup(state)
comm := new(packer.MockCommunicator)
state.Put("communicator", comm)
state.Put("image_file_name", "foo")
config := state.Get("config").(*Config)
config.SSHUsername = "bob"
// run the step
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
// Verify
if !comm.StartCalled {
t.Fatal("start should be called")
}
if !strings.HasPrefix(comm.StartCmd.Command, "sudo") {
t.Fatal("should sudo")
}
if !strings.Contains(comm.StartCmd.Command, "gsutil cp") {
t.Fatalf("bad command: %#v", comm.StartCmd.Command)
}
}
......@@ -24,6 +24,7 @@ const defaultConfig = `
"amazon-instance": "packer-builder-amazon-instance",
"digitalocean": "packer-builder-digitalocean",
"docker": "packer-builder-docker",
"googlecompute": "packer-builder-googlecompute",
"openstack": "packer-builder-openstack",
"qemu": "packer-builder-qemu",
"virtualbox": "packer-builder-virtualbox",
......
package main
import (
"github.com/mitchellh/packer/builder/googlecompute"
"github.com/mitchellh/packer/packer/plugin"
)
func main() {
server, err := plugin.Server()
if err != nil {
panic(err)
}
server.RegisterBuilder(new(googlecompute.Builder))
server.Serve()
}
---
layout: "docs"
---
# Google Compute Builder
Type: `googlecompute`
The `googlecompute` 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.
## Setting Up API Access
There is a small setup step required in order to obtain the credentials
that Packer needs to use Google Compute Engine. This needs to be done only
once if you intend to share the credentials.
In order for Packer to talk to Google Compute Engine, it will need
a _client secrets_ JSON file and a _client private key_. Both of these are
obtained from the [Google Cloud Console](https://cloud.google.com/console).
Follow the steps below:
1. Log into the [Google Cloud Console](https://cloud.google.com/console)
2. Click on the project you want to use Packer with (or create one if you
don't have one yet).
3. Click "APIs & auth" in the left sidebar
4. Click "Registered apps" in the left sidebar
5. Click "Register App" and register a "Web Application". Choose any
name you'd like.
7. After creating the app, click "Certificate" (below the OAuth 2.0 Client
ID section), and click "Download JSON". This is your _client secrets JSON_
file. Make sure you didn't download the JSON from the "OAuth 2.0" section!
This is a common mistake and will cause the builder to not work.
8. Next, click "Generate Certificate". You should be prompted to download
a private key. Note the password for the private key! This private key
is your _client private key_.
Finally, one last step, you'll have to convert the `p12` file you
got from Google into the PEM format. You can do this with OpenSSL, which
is installed standard on most Unixes:
```
$ openssl pkcs12 -in <path to .p12> -nocerts -passin pass:notasecret \
-nodes -out private_key.pem
```
The client secrets JSON you downloaded along with the new "private\_key.pem"
file are the two files you need to configure Packer with to talk to GCE.
## 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 client secrets file and private key file are the
files obtained in the previous section.
<pre class="prettyprint">
{
"type": "googlecompute",
"bucket_name": "packer-images",
"client_secrets_file": "client_secret.json",
"private_key_file": "XXXXXX-privatekey.p12",
"project_id": "my-project",
"source_image": "debian-7-wheezy-v20131014",
"zone": "us-central1-a"
}
</pre>
## Configuration Reference
Configuration options are organized below into two categories: required and optional. Within
each category, the available options are alphabetized and described.
Required:
* `bucket_name` (string) - The Google Cloud Storage bucket to store the
images that are created.
* `client_secrets_file` (string) - The client secrets JSON file that
was set up in the section above.
* `private_key_file` (string) - The client private key file that was
generated in the section above.
* `project_id` (string) - The project ID that will be used to launch instances
and store images.
* `source_image` (string) - The source image to use to create the new image
from. Example: "debian-7"
* `zone` (string) - The zone in which to launch the instance used to create
the image. Example: "us-central1-a"
Optional:
* `image_name` (string) - The unique name of the resulting image.
Defaults to `packer-{{timestamp}}`.
* `image_description` (string) - The description of the resulting image.
* `machine_type` (string) - The machine type. Defaults to `n1-standard-1`.
* `network` (string) - The Google Compute network to use for the launched
instance. Defaults to `default`.
* `passphrase` (string) - The passphrase to use if the `private_key_file`
is encrypted.
* `ssh_port` (int) - The SSH port. Defaults to 22.
* `ssh_timeout` (string) - The time to wait for SSH to become available.
Defaults to "1m".
* `ssh_username` (string) - The SSH username. Defaults to "root".
* `state_timeout` (string) - The time to wait for instance state changes.
Defaults to "5m".
## Gotchas
Centos images have root ssh access disabled by default. Set `ssh_username` to any user, which will be created by packer with sudo access.
The machine type must have a scratch disk, which means you can't use an `f1-micro` or `g1-small` to build images.
......@@ -33,6 +33,7 @@
<li><a href="/docs/builders/amazon.html">Amazon EC2 (AMI)</a></li>
<li><a href="/docs/builders/digitalocean.html">DigitalOcean</a></li>
<li><a href="/docs/builders/docker.html">Docker</a></li>
<li><a href="/docs/builders/googlecompute.html">Google Compute Engine</a></li>
<li><a href="/docs/builders/openstack.html">OpenStack</a></li>
<li><a href="/docs/builders/qemu.html">QEMU</a></li>
<li><a href="/docs/builders/virtualbox.html">VirtualBox</a></li>
......
......@@ -174,7 +174,6 @@ header .header {
ul,
ol {
list-style-type: circle;
list-style-position: inside;
margin-top: $baseline;
margin-left: 20px;
......@@ -188,6 +187,14 @@ header .header {
}
}
ul {
list-style-type: circle;
}
ol {
list-style-type: decimal;
}
div.alert {
font-family: $serif;
font-size: 17px;
......
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