From e15be036d75a0283bf6b3c5a138ae4c278ffff09 Mon Sep 17 00:00:00 2001 From: James Nugent Date: Thu, 14 Apr 2016 17:29:27 -0700 Subject: [PATCH] builder: add Triton builder This is a builder for Joyent's Triton system. It was originally at jen20/packer-builder-triton, and subsequently at joyent/packer-builder-triton on GitHub. The following commit vendors the dependencies. --- builder/triton/access_config.go | 97 ++++++++++ builder/triton/access_config_test.go | 43 +++++ builder/triton/artifact.go | 49 +++++ builder/triton/builder.go | 113 ++++++++++++ builder/triton/config.go | 18 ++ builder/triton/config_test.go | 13 ++ builder/triton/driver.go | 17 ++ builder/triton/driver_mock.go | 96 ++++++++++ builder/triton/driver_triton.go | 167 ++++++++++++++++++ builder/triton/source_machine_config.go | 54 ++++++ builder/triton/source_machine_config_test.go | 57 ++++++ builder/triton/ssh.go | 36 ++++ .../triton/step_create_image_from_machine.go | 45 +++++ .../step_create_image_from_machine_test.go | 73 ++++++++ builder/triton/step_create_source_machine.go | 68 +++++++ .../triton/step_create_source_machine_test.go | 159 +++++++++++++++++ builder/triton/step_delete_machine.go | 41 +++++ builder/triton/step_delete_machine_test.go | 79 +++++++++ builder/triton/step_stop_machine.go | 41 +++++ builder/triton/step_stop_machine_test.go | 71 ++++++++ builder/triton/step_test.go | 20 +++ .../triton/step_wait_for_stop_to_not_fail.go | 24 +++ builder/triton/target_image_config.go | 38 ++++ builder/triton/target_image_config_test.go | 46 +++++ command/plugin.go | 2 + 25 files changed, 1467 insertions(+) create mode 100644 builder/triton/access_config.go create mode 100644 builder/triton/access_config_test.go create mode 100644 builder/triton/artifact.go create mode 100644 builder/triton/builder.go create mode 100644 builder/triton/config.go create mode 100644 builder/triton/config_test.go create mode 100644 builder/triton/driver.go create mode 100644 builder/triton/driver_mock.go create mode 100644 builder/triton/driver_triton.go create mode 100644 builder/triton/source_machine_config.go create mode 100644 builder/triton/source_machine_config_test.go create mode 100644 builder/triton/ssh.go create mode 100644 builder/triton/step_create_image_from_machine.go create mode 100644 builder/triton/step_create_image_from_machine_test.go create mode 100644 builder/triton/step_create_source_machine.go create mode 100644 builder/triton/step_create_source_machine_test.go create mode 100644 builder/triton/step_delete_machine.go create mode 100644 builder/triton/step_delete_machine_test.go create mode 100644 builder/triton/step_stop_machine.go create mode 100644 builder/triton/step_stop_machine_test.go create mode 100644 builder/triton/step_test.go create mode 100644 builder/triton/step_wait_for_stop_to_not_fail.go create mode 100644 builder/triton/target_image_config.go create mode 100644 builder/triton/target_image_config_test.go diff --git a/builder/triton/access_config.go b/builder/triton/access_config.go new file mode 100644 index 000000000..d733ff3fb --- /dev/null +++ b/builder/triton/access_config.go @@ -0,0 +1,97 @@ +package triton + +import ( + "fmt" + "io/ioutil" + "log" + "os" + + "github.com/joyent/gocommon/client" + "github.com/joyent/gosdc/cloudapi" + "github.com/joyent/gosign/auth" + "github.com/mitchellh/packer/helper/communicator" + "github.com/mitchellh/packer/template/interpolate" +) + +// AccessConfig is for common configuration related to Triton access +type AccessConfig struct { + Endpoint string `mapstructure:"triton_url"` + Account string `mapstructure:"triton_account"` + KeyID string `mapstructure:"triton_key_id"` + KeyMaterial string `mapstructure:"triton_key_material"` +} + +// Prepare performs basic validation on the AccessConfig +func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error { + var errs []error + + if c.Endpoint == "" { + // Use Joyent public cloud as the default endpoint if none is in environment + c.Endpoint = "https://us-east-1.api.joyent.com" + } + + if c.Account == "" { + errs = append(errs, fmt.Errorf("triton_account is required to use the triton builder")) + } + + if c.KeyID == "" { + errs = append(errs, fmt.Errorf("triton_key_id is required to use the triton builder")) + } + + if c.KeyMaterial == "" { + errs = append(errs, fmt.Errorf("triton_key_material is required to use the triton builder")) + } + + if len(errs) > 0 { + return errs + } + + return nil +} + +// CreateTritonClient returns an SDC client configured with the appropriate client credentials +// or an error if creating the client fails. +func (c *AccessConfig) CreateTritonClient() (*cloudapi.Client, error) { + keyData, err := processKeyMaterial(c.KeyMaterial) + if err != nil { + return nil, err + } + + userauth, err := auth.NewAuth(c.Account, string(keyData), "rsa-sha256") + if err != nil { + return nil, err + } + + creds := &auth.Credentials{ + UserAuthentication: userauth, + SdcKeyId: c.KeyID, + SdcEndpoint: auth.Endpoint{URL: c.Endpoint}, + } + + return cloudapi.New(client.NewClient( + c.Endpoint, + cloudapi.DefaultAPIVersion, + creds, + log.New(os.Stdout, "", log.Flags()), + )), nil +} + +func (c *AccessConfig) Comm() communicator.Config { + return communicator.Config{} +} + +func processKeyMaterial(keyMaterial string) (string, error) { + // Check for keyMaterial being a file path + if _, err := os.Stat(keyMaterial); err != nil { + // Not a valid file. Assume that keyMaterial is the key data + return keyMaterial, nil + } + + b, err := ioutil.ReadFile(keyMaterial) + if err != nil { + return "", fmt.Errorf("Error reading key_material from path '%s': %s", + keyMaterial, err) + } + + return string(b), nil +} diff --git a/builder/triton/access_config_test.go b/builder/triton/access_config_test.go new file mode 100644 index 000000000..ce190fba4 --- /dev/null +++ b/builder/triton/access_config_test.go @@ -0,0 +1,43 @@ +package triton + +import ( + "testing" +) + +func TestAccessConfig_Prepare(t *testing.T) { + ac := testAccessConfig(t) + errs := ac.Prepare(nil) + if errs != nil { + t.Fatal("should not error") + } + + ac = testAccessConfig(t) + ac.Account = "" + errs = ac.Prepare(nil) + if errs == nil { + t.Fatal("should error") + } + + ac = testAccessConfig(t) + ac.KeyID = "" + errs = ac.Prepare(nil) + if errs == nil { + t.Fatal("should error") + } + + ac = testAccessConfig(t) + ac.KeyMaterial = "" + errs = ac.Prepare(nil) + if errs == nil { + t.Fatal("should error") + } +} + +func testAccessConfig(t *testing.T) AccessConfig { + return AccessConfig{ + Endpoint: "test-endpoint", + Account: "test-account", + KeyID: "test-id", + KeyMaterial: "test-private-key", + } +} diff --git a/builder/triton/artifact.go b/builder/triton/artifact.go new file mode 100644 index 000000000..c36f7ab5c --- /dev/null +++ b/builder/triton/artifact.go @@ -0,0 +1,49 @@ +package triton + +import ( + "fmt" + "log" +) + +// Artifact is an artifact implementation that contains built Triton images. +type Artifact struct { + // ImageID is the image ID of the artifact + ImageID string + + // BuilderIDValue is the unique ID for the builder that created this Image + BuilderIDValue string + + // SDC connection for cleanup etc + Driver Driver +} + +func (a *Artifact) BuilderId() string { + return a.BuilderIDValue +} + +func (*Artifact) Files() []string { + return nil +} + +func (a *Artifact) Id() string { + return a.ImageID +} + +func (a *Artifact) String() string { + return fmt.Sprintf("Image was created: %s", a.ImageID) +} + +func (a *Artifact) State(name string) interface{} { + //TODO(jen20): Figure out how to make this work with Atlas + return nil +} + +func (a *Artifact) Destroy() error { + log.Printf("Deleting image ID (%s)", a.ImageID) + err := a.Driver.DeleteImage(a.ImageID) + if err != nil { + return err + } + + return nil +} diff --git a/builder/triton/builder.go b/builder/triton/builder.go new file mode 100644 index 000000000..b7a9b1641 --- /dev/null +++ b/builder/triton/builder.go @@ -0,0 +1,113 @@ +package triton + +import ( + "log" + + "github.com/hashicorp/go-multierror" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/helper/communicator" + "github.com/mitchellh/packer/helper/config" + "github.com/mitchellh/packer/packer" +) + +const ( + BuilderId = "joyent.triton" +) + +type Builder struct { + config Config + runner multistep.Runner +} + +func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { + errs := &multierror.Error{} + + err := config.Decode(&b.config, &config.DecodeOpts{ + Interpolate: true, + InterpolateContext: &b.config.ctx, + }, raws...) + if err != nil { + errs = multierror.Append(errs, err) + } + + if b.config.Comm.SSHUsername == "" { + b.config.Comm.SSHUsername = "root" + } + + errs = multierror.Append(errs, b.config.AccessConfig.Prepare(&b.config.ctx)...) + errs = multierror.Append(errs, b.config.SourceMachineConfig.Prepare(&b.config.ctx)...) + errs = multierror.Append(errs, b.config.Comm.Prepare(&b.config.ctx)...) + errs = multierror.Append(errs, b.config.TargetImageConfig.Prepare(&b.config.ctx)...) + + if b.config.Comm.SSHPrivateKey == "" { + b.config.Comm.SSHPrivateKey = b.config.KeyMaterial + } + return nil, errs.ErrorOrNil() +} + +func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { + config := b.config + + driver, err := NewDriverTriton(ui, config) + if err != nil { + return nil, err + } + + state := new(multistep.BasicStateBag) + state.Put("config", b.config) + state.Put("driver", driver) + state.Put("hook", hook) + state.Put("ui", ui) + + steps := []multistep.Step{ + &StepCreateSourceMachine{}, + &communicator.StepConnect{ + Config: &config.Comm, + Host: commHost, + SSHConfig: sshConfig, + }, + &common.StepProvision{}, + &StepStopMachine{}, + &StepCreateImageFromMachine{}, + &StepDeleteMachine{}, + } + + 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) + + // If there was an error, return that + if rawErr, ok := state.GetOk("error"); ok { + return nil, rawErr.(error) + } + + // If there is no image, just return + if _, ok := state.GetOk("image"); !ok { + return nil, nil + } + + artifact := &Artifact{ + ImageID: state.Get("image").(string), + BuilderIDValue: BuilderId, + Driver: driver, + } + + return artifact, nil +} + +// Cancel cancels a possibly running Builder. This should block until +// the builder actually cancels and cleans up after itself. +func (b *Builder) Cancel() { + if b.runner != nil { + log.Println("Cancelling the step runner...") + b.runner.Cancel() + } +} diff --git a/builder/triton/config.go b/builder/triton/config.go new file mode 100644 index 000000000..683fb807c --- /dev/null +++ b/builder/triton/config.go @@ -0,0 +1,18 @@ +package triton + +import ( + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/helper/communicator" + "github.com/mitchellh/packer/template/interpolate" +) + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + AccessConfig `mapstructure:",squash"` + SourceMachineConfig `mapstructure:",squash"` + TargetImageConfig `mapstructure:",squash"` + + Comm communicator.Config `mapstructure:",squash"` + + ctx interpolate.Context +} diff --git a/builder/triton/config_test.go b/builder/triton/config_test.go new file mode 100644 index 000000000..4c52a0c24 --- /dev/null +++ b/builder/triton/config_test.go @@ -0,0 +1,13 @@ +package triton + +import ( + "testing" +) + +func testConfig(t *testing.T) Config { + return Config{ + AccessConfig: testAccessConfig(t), + SourceMachineConfig: testSourceMachineConfig(t), + TargetImageConfig: testTargetImageConfig(t), + } +} diff --git a/builder/triton/driver.go b/builder/triton/driver.go new file mode 100644 index 000000000..1cd30b90b --- /dev/null +++ b/builder/triton/driver.go @@ -0,0 +1,17 @@ +package triton + +import ( + "time" +) + +type Driver interface { + CreateImageFromMachine(machineId string, config Config) (string, error) + CreateMachine(config Config) (string, error) + DeleteImage(imageId string) error + DeleteMachine(machineId string) error + GetMachine(machineId string) (string, error) + StopMachine(machineId string) error + WaitForImageCreation(imageId string, timeout time.Duration) error + WaitForMachineDeletion(machineId string, timeout time.Duration) error + WaitForMachineState(machineId string, state string, timeout time.Duration) error +} diff --git a/builder/triton/driver_mock.go b/builder/triton/driver_mock.go new file mode 100644 index 000000000..33bc618a6 --- /dev/null +++ b/builder/triton/driver_mock.go @@ -0,0 +1,96 @@ +package triton + +import ( + "time" +) + +type DriverMock struct { + CreateImageFromMachineId string + CreateImageFromMachineErr error + + CreateMachineId string + CreateMachineErr error + + DeleteImageId string + DeleteImageErr error + + DeleteMachineId string + DeleteMachineErr error + + GetMachineErr error + + StopMachineId string + StopMachineErr error + + WaitForImageCreationErr error + + WaitForMachineDeletionErr error + + WaitForMachineStateErr error +} + +func (d *DriverMock) CreateImageFromMachine(machineId string, config Config) (string, error) { + if d.CreateImageFromMachineErr != nil { + return "", d.CreateImageFromMachineErr + } + + d.CreateImageFromMachineId = config.ImageName + + return d.CreateImageFromMachineId, nil +} + +func (d *DriverMock) CreateMachine(config Config) (string, error) { + if d.CreateMachineErr != nil { + return "", d.CreateMachineErr + } + + d.CreateMachineId = config.MachineName + + return d.CreateMachineId, nil +} + +func (d *DriverMock) DeleteImage(imageId string) error { + if d.DeleteImageErr != nil { + return d.DeleteImageErr + } + + d.DeleteImageId = imageId + + return nil +} + +func (d *DriverMock) DeleteMachine(machineId string) error { + if d.DeleteMachineErr != nil { + return d.DeleteMachineErr + } + + d.DeleteMachineId = machineId + + return nil +} + +func (d *DriverMock) GetMachine(machineId string) (string, error) { + if d.GetMachineErr != nil { + return "", d.GetMachineErr + } + + return "ip", nil +} + +func (d *DriverMock) StopMachine(machineId string) error { + d.StopMachineId = machineId + + return d.StopMachineErr +} + +func (d *DriverMock) WaitForImageCreation(machineId string, timeout time.Duration) error { + return d.WaitForImageCreationErr +} + +func (d *DriverMock) WaitForMachineDeletion(machineId string, timeout time.Duration) error { + return d.WaitForMachineDeletionErr +} + +func (d *DriverMock) WaitForMachineState(machineId string, state string, timeout time.Duration) error { + return d.WaitForMachineStateErr +} diff --git a/builder/triton/driver_triton.go b/builder/triton/driver_triton.go new file mode 100644 index 000000000..f445556bd --- /dev/null +++ b/builder/triton/driver_triton.go @@ -0,0 +1,167 @@ +package triton + +import ( + "errors" + "strings" + "time" + + "github.com/joyent/gosdc/cloudapi" + "github.com/mitchellh/packer/packer" +) + +type driverTriton struct { + client *cloudapi.Client + ui packer.Ui +} + +func NewDriverTriton(ui packer.Ui, config Config) (Driver, error) { + client, err := config.AccessConfig.CreateTritonClient() + if err != nil { + return nil, err + } + + return &driverTriton{ + client: client, + ui: ui, + }, nil +} + +func (d *driverTriton) CreateImageFromMachine(machineId string, config Config) (string, error) { + opts := cloudapi.CreateImageFromMachineOpts{ + Machine: machineId, + Name: config.ImageName, + Version: config.ImageVersion, + Description: config.ImageDescription, + Homepage: config.ImageHomepage, + EULA: config.ImageEULA, + ACL: config.ImageACL, + Tags: config.ImageTags, + } + + image, err := d.client.CreateImageFromMachine(opts) + if err != nil { + return "", err + } + + return image.Id, err +} + +func (d *driverTriton) CreateMachine(config Config) (string, error) { + opts := cloudapi.CreateMachineOpts{ + Package: config.MachinePackage, + Image: config.MachineImage, + Networks: config.MachineNetworks, + Metadata: config.MachineMetadata, + Tags: config.MachineTags, + FirewallEnabled: config.MachineFirewallEnabled, + } + + if config.MachineName != "" { + opts.Name = config.MachineName + } + + machine, err := d.client.CreateMachine(opts) + if err != nil { + return "", err + } + + return machine.Id, nil +} + +func (d *driverTriton) DeleteImage(imageId string) error { + return d.client.DeleteImage(imageId) +} + +func (d *driverTriton) DeleteMachine(machineId string) error { + return d.client.DeleteMachine(machineId) +} + +func (d *driverTriton) GetMachine(machineId string) (string, error) { + machine, err := d.client.GetMachine(machineId) + if err != nil { + return "", err + } + + return machine.PrimaryIP, nil +} + +func (d *driverTriton) StopMachine(machineId string) error { + return d.client.StopMachine(machineId) +} + +// waitForMachineState uses the supplied client to wait for the state of +// the machine with the given ID to reach the state described in state. +// If timeout is reached before the machine reaches the required state, an +// error is returned. If the machine reaches the target state within the +// timeout, nil is returned. +func (d *driverTriton) WaitForMachineState(machineId string, state string, timeout time.Duration) error { + return waitFor( + func() (bool, error) { + machine, err := d.client.GetMachine(machineId) + if machine == nil { + return false, err + } + return machine.State == state, err + }, + 3*time.Second, + timeout, + ) +} + +// waitForMachineDeletion uses the supplied client to wait for the machine +// with the given ID to be deleted. It is expected that the API call to delete +// the machine has already been issued at this point. +func (d *driverTriton) WaitForMachineDeletion(machineId string, timeout time.Duration) error { + return waitFor( + func() (bool, error) { + machine, err := d.client.GetMachine(machineId) + if machine != nil { + return false, nil + } + + if err != nil { + //TODO(jen20): is there a better way here than searching strings? + if strings.Contains(err.Error(), "410") || strings.Contains(err.Error(), "404") { + return true, nil + } + } + + return false, err + }, + 3*time.Second, + timeout, + ) +} + +func (d *driverTriton) WaitForImageCreation(imageId string, timeout time.Duration) error { + return waitFor( + func() (bool, error) { + image, err := d.client.GetImage(imageId) + if image == nil { + return false, err + } + return image.OS != "", err + }, + 3*time.Second, + timeout, + ) +} + +func waitFor(f func() (bool, error), every, timeout time.Duration) error { + start := time.Now() + + for time.Since(start) <= timeout { + stop, err := f() + if err != nil { + return err + } + + if stop { + return nil + } + + time.Sleep(every) + } + + return errors.New("Timed out while waiting for resource change") +} diff --git a/builder/triton/source_machine_config.go b/builder/triton/source_machine_config.go new file mode 100644 index 000000000..f95218710 --- /dev/null +++ b/builder/triton/source_machine_config.go @@ -0,0 +1,54 @@ +package triton + +import ( + "fmt" + + "github.com/mitchellh/packer/template/interpolate" +) + +// SourceMachineConfig represents the configuration to run a machine using +// the SDC API in order for provisioning to take place. +type SourceMachineConfig struct { + MachineName string `mapstructure:"source_machine_name"` + MachinePackage string `mapstructure:"source_machine_package"` + MachineImage string `mapstructure:"source_machine_image"` + MachineNetworks []string `mapstructure:"source_machine_networks"` + MachineMetadata map[string]string `mapstructure:"source_machine_metadata"` + MachineTags map[string]string `mapstructure:"source_machine_tags"` + MachineFirewallEnabled bool `mapstructure:"source_machine_firewall_enabled"` +} + +// Prepare performs basic validation on a SourceMachineConfig struct. +func (c *SourceMachineConfig) Prepare(ctx *interpolate.Context) []error { + var errs []error + + if c.MachineName == "" { + errs = append(errs, fmt.Errorf("A source_machine_name must be specified")) + } + + if c.MachinePackage == "" { + errs = append(errs, fmt.Errorf("A source_machine_package must be specified")) + } + + if c.MachineImage == "" { + errs = append(errs, fmt.Errorf("A source_machine_image must be specified")) + } + + if c.MachineNetworks == nil { + c.MachineNetworks = []string{} + } + + if c.MachineMetadata == nil { + c.MachineMetadata = make(map[string]string) + } + + if c.MachineTags == nil { + c.MachineTags = make(map[string]string) + } + + if len(errs) > 0 { + return errs + } + + return nil +} diff --git a/builder/triton/source_machine_config_test.go b/builder/triton/source_machine_config_test.go new file mode 100644 index 000000000..b9d4f6e0b --- /dev/null +++ b/builder/triton/source_machine_config_test.go @@ -0,0 +1,57 @@ +package triton + +import ( + "testing" +) + +func TestSourceMachineConfig_Prepare(t *testing.T) { + sc := testSourceMachineConfig(t) + errs := sc.Prepare(nil) + if errs != nil { + t.Fatalf("should not error: %#v", sc) + } + + sc = testSourceMachineConfig(t) + sc.MachineName = "" + errs = sc.Prepare(nil) + if errs == nil { + t.Fatalf("should error: %#v", sc) + } + + sc = testSourceMachineConfig(t) + sc.MachinePackage = "" + errs = sc.Prepare(nil) + if errs == nil { + t.Fatalf("should error: %#v", sc) + } + + sc = testSourceMachineConfig(t) + sc.MachineImage = "" + errs = sc.Prepare(nil) + if errs == nil { + t.Fatalf("should error: %#v", sc) + } +} + +func testSourceMachineConfig(t *testing.T) SourceMachineConfig { + return SourceMachineConfig{ + MachineName: "test-machine", + MachinePackage: "test-package", + MachineImage: "test-image", + MachineNetworks: []string{ + "test-network-1", + "test-network-2", + }, + MachineMetadata: map[string]string{ + "test-metadata-key1": "test-metadata-value1", + "test-metadata-key2": "test-metadata-value2", + "test-metadata-key3": "test-metadata-value3", + }, + MachineTags: map[string]string{ + "test-tags-key1": "test-tags-value1", + "test-tags-key2": "test-tags-value2", + "test-tags-key3": "test-tags-value3", + }, + MachineFirewallEnabled: false, + } +} diff --git a/builder/triton/ssh.go b/builder/triton/ssh.go new file mode 100644 index 000000000..8b3e9ae7b --- /dev/null +++ b/builder/triton/ssh.go @@ -0,0 +1,36 @@ +package triton + +import ( + "fmt" + + "github.com/mitchellh/multistep" + "golang.org/x/crypto/ssh" +) + +func commHost(state multistep.StateBag) (string, error) { + driver := state.Get("driver").(Driver) + machineID := state.Get("machine").(string) + + machine, err := driver.GetMachine(machineID) + if err != nil { + return "", err + } + + return machine, nil +} + +func sshConfig(state multistep.StateBag) (*ssh.ClientConfig, error) { + config := state.Get("config").(Config) + + signer, err := ssh.ParsePrivateKey([]byte(config.Comm.SSHPrivateKey)) + if err != nil { + return nil, fmt.Errorf("Error setting up SSH config: %s", err) + } + + return &ssh.ClientConfig{ + User: config.Comm.SSHUsername, + Auth: []ssh.AuthMethod{ + ssh.PublicKeys(signer), + }, + }, nil +} diff --git a/builder/triton/step_create_image_from_machine.go b/builder/triton/step_create_image_from_machine.go new file mode 100644 index 000000000..dd2201576 --- /dev/null +++ b/builder/triton/step_create_image_from_machine.go @@ -0,0 +1,45 @@ +package triton + +import ( + "fmt" + "time" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +// StepCreateImageFromMachine creates an image with the specified attributes +// from the machine with the given ID, and waits for the image to be created. +// The machine must be in the "stopped" state prior to this step being run. +type StepCreateImageFromMachine struct{} + +func (s *StepCreateImageFromMachine) Run(state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(Config) + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + + machineId := state.Get("machine").(string) + + ui.Say("Creating image from source machine...") + + imageId, err := driver.CreateImageFromMachine(machineId, config) + if err != nil { + state.Put("error", fmt.Errorf("Problem creating image from machine: %s", err)) + return multistep.ActionHalt + } + + ui.Say("Waiting for image to become available...") + err = driver.WaitForImageCreation(imageId, 10*time.Minute) + if err != nil { + state.Put("error", fmt.Errorf("Problem waiting for image to become available: %s", err)) + return multistep.ActionHalt + } + + state.Put("image", imageId) + + return multistep.ActionContinue +} + +func (s *StepCreateImageFromMachine) Cleanup(state multistep.StateBag) { + // No cleanup +} diff --git a/builder/triton/step_create_image_from_machine_test.go b/builder/triton/step_create_image_from_machine_test.go new file mode 100644 index 000000000..36ee43f82 --- /dev/null +++ b/builder/triton/step_create_image_from_machine_test.go @@ -0,0 +1,73 @@ +package triton + +import ( + "errors" + "testing" + + "github.com/mitchellh/multistep" +) + +func TestStepCreateImageFromMachine(t *testing.T) { + state := testState(t) + step := new(StepCreateImageFromMachine) + defer step.Cleanup(state) + + state.Put("machine", "test-machine-id") + + if action := step.Run(state); action != multistep.ActionContinue { + t.Fatalf("bad action: %#v", action) + } + + _, ok := state.GetOk("image") + if !ok { + t.Fatalf("should have image") + } + + step.Cleanup(state) +} + +func TestStepCreateImageFromMachine_CreateImageFromMachineError(t *testing.T) { + state := testState(t) + step := new(StepCreateImageFromMachine) + defer step.Cleanup(state) + + driver := state.Get("driver").(*DriverMock) + state.Put("machine", "test-machine-id") + + driver.CreateImageFromMachineErr = errors.New("error") + + if action := step.Run(state); action != multistep.ActionHalt { + t.Fatalf("bad action: %#v", action) + } + + if _, ok := state.GetOk("error"); !ok { + t.Fatalf("should have error") + } + + if _, ok := state.GetOk("image"); ok { + t.Fatalf("should NOT have image") + } +} + +func TestStepCreateImageFromMachine_WaitForImageCreationError(t *testing.T) { + state := testState(t) + step := new(StepCreateImageFromMachine) + defer step.Cleanup(state) + + driver := state.Get("driver").(*DriverMock) + state.Put("machine", "test-machine-id") + + driver.WaitForImageCreationErr = errors.New("error") + + if action := step.Run(state); action != multistep.ActionHalt { + t.Fatalf("bad action: %#v", action) + } + + if _, ok := state.GetOk("error"); !ok { + t.Fatalf("should have error") + } + + if _, ok := state.GetOk("image"); ok { + t.Fatalf("should NOT have image") + } +} diff --git a/builder/triton/step_create_source_machine.go b/builder/triton/step_create_source_machine.go new file mode 100644 index 000000000..6de30143e --- /dev/null +++ b/builder/triton/step_create_source_machine.go @@ -0,0 +1,68 @@ +package triton + +import ( + "fmt" + "time" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +// StepCreateSourceMachine creates an machine with the specified attributes +// and waits for it to become available for provisioners. +type StepCreateSourceMachine struct{} + +func (s *StepCreateSourceMachine) Run(state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(Config) + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + + ui.Say("Creating source machine...") + + machineId, err := driver.CreateMachine(config) + if err != nil { + state.Put("error", fmt.Errorf("Problem creating source machine: %s", err)) + return multistep.ActionHalt + } + + ui.Say("Waiting for source machine to become available...") + err = driver.WaitForMachineState(machineId, "running", 10*time.Minute) + if err != nil { + state.Put("error", fmt.Errorf("Problem waiting for source machine to become available: %s", err)) + return multistep.ActionHalt + } + + state.Put("machine", machineId) + + return multistep.ActionContinue +} + +func (s *StepCreateSourceMachine) Cleanup(state multistep.StateBag) { + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + + machineIdRaw, ok := state.GetOk("machine") + if ok && machineIdRaw.(string) != "" { + machineId := machineIdRaw.(string) + ui.Say(fmt.Sprintf("Stopping source machine (%s)...", machineId)) + err := driver.StopMachine(machineId) + if err != nil { + state.Put("error", fmt.Errorf("Problem stopping source machine: %s", err)) + return + } + + ui.Say(fmt.Sprintf("Waiting for source machine to stop (%s)...", machineId)) + err = driver.WaitForMachineState(machineId, "stopped", 10*time.Minute) + if err != nil { + state.Put("error", fmt.Errorf("Problem waiting for source machine to stop: %s", err)) + return + } + + ui.Say(fmt.Sprintf("Deleting source machine (%s)...", machineId)) + err = driver.DeleteMachine(machineId) + if err != nil { + state.Put("error", fmt.Errorf("Problem deleting source machine: %s", err)) + return + } + } +} diff --git a/builder/triton/step_create_source_machine_test.go b/builder/triton/step_create_source_machine_test.go new file mode 100644 index 000000000..56f5d5940 --- /dev/null +++ b/builder/triton/step_create_source_machine_test.go @@ -0,0 +1,159 @@ +package triton + +import ( + "errors" + "testing" + + "github.com/mitchellh/multistep" +) + +func TestStepCreateSourceMachine(t *testing.T) { + state := testState(t) + step := new(StepCreateSourceMachine) + defer step.Cleanup(state) + + driver := state.Get("driver").(*DriverMock) + + if action := step.Run(state); action != multistep.ActionContinue { + t.Fatalf("bad action: %#v", action) + } + + machineIdRaw, ok := state.GetOk("machine") + if !ok { + t.Fatalf("should have machine") + } + + step.Cleanup(state) + + if driver.DeleteMachineId != machineIdRaw.(string) { + t.Fatalf("should've deleted machine (%s != %s)", driver.DeleteMachineId, machineIdRaw.(string)) + } +} + +func TestStepCreateSourceMachine_CreateMachineError(t *testing.T) { + state := testState(t) + step := new(StepCreateSourceMachine) + defer step.Cleanup(state) + + driver := state.Get("driver").(*DriverMock) + + driver.CreateMachineErr = errors.New("error") + + if action := step.Run(state); action != multistep.ActionHalt { + t.Fatalf("bad action: %#v", action) + } + + if _, ok := state.GetOk("error"); !ok { + t.Fatalf("should have error") + } + + if _, ok := state.GetOk("machine"); ok { + t.Fatalf("should NOT have machine") + } +} + +func TestStepCreateSourceMachine_WaitForMachineStateError(t *testing.T) { + state := testState(t) + step := new(StepCreateSourceMachine) + defer step.Cleanup(state) + + driver := state.Get("driver").(*DriverMock) + + driver.WaitForMachineStateErr = errors.New("error") + + if action := step.Run(state); action != multistep.ActionHalt { + t.Fatalf("bad action: %#v", action) + } + + if _, ok := state.GetOk("error"); !ok { + t.Fatalf("should have error") + } + + if _, ok := state.GetOk("machine"); ok { + t.Fatalf("should NOT have machine") + } +} + +func TestStepCreateSourceMachine_StopMachineError(t *testing.T) { + state := testState(t) + step := new(StepCreateSourceMachine) + defer step.Cleanup(state) + + driver := state.Get("driver").(*DriverMock) + + if action := step.Run(state); action != multistep.ActionContinue { + t.Fatalf("bad action: %#v", action) + } + + _, ok := state.GetOk("machine") + if !ok { + t.Fatalf("should have machine") + } + + driver.StopMachineErr = errors.New("error") + step.Cleanup(state) + + if _, ok := state.GetOk("error"); !ok { + t.Fatalf("should have error") + } + + if _, ok := state.GetOk("machine"); !ok { + t.Fatalf("should have machine") + } +} + +func TestStepCreateSourceMachine_WaitForMachineStoppedError(t *testing.T) { + state := testState(t) + step := new(StepCreateSourceMachine) + defer step.Cleanup(state) + + driver := state.Get("driver").(*DriverMock) + + if action := step.Run(state); action != multistep.ActionContinue { + t.Fatalf("bad action: %#v", action) + } + + _, ok := state.GetOk("machine") + if !ok { + t.Fatalf("should have machine") + } + + driver.WaitForMachineStateErr = errors.New("error") + step.Cleanup(state) + + if _, ok := state.GetOk("error"); !ok { + t.Fatalf("should have error") + } + + if _, ok := state.GetOk("machine"); !ok { + t.Fatalf("should have machine") + } +} + +func TestStepCreateSourceMachine_DeleteMachineError(t *testing.T) { + state := testState(t) + step := new(StepCreateSourceMachine) + defer step.Cleanup(state) + + driver := state.Get("driver").(*DriverMock) + + if action := step.Run(state); action != multistep.ActionContinue { + t.Fatalf("bad action: %#v", action) + } + + _, ok := state.GetOk("machine") + if !ok { + t.Fatalf("should have machine") + } + + driver.DeleteMachineErr = errors.New("error") + step.Cleanup(state) + + if _, ok := state.GetOk("error"); !ok { + t.Fatalf("should have error") + } + + if _, ok := state.GetOk("machine"); !ok { + t.Fatalf("should have machine") + } +} diff --git a/builder/triton/step_delete_machine.go b/builder/triton/step_delete_machine.go new file mode 100644 index 000000000..1538212b6 --- /dev/null +++ b/builder/triton/step_delete_machine.go @@ -0,0 +1,41 @@ +package triton + +import ( + "fmt" + "time" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +// StepDeleteMachine deletes the machine with the ID specified in state["machine"] +type StepDeleteMachine struct{} + +func (s *StepDeleteMachine) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + + machineId := state.Get("machine").(string) + + ui.Say("Deleting source machine...") + err := driver.DeleteMachine(machineId) + if err != nil { + state.Put("error", fmt.Errorf("Problem deleting source machine: %s", err)) + return multistep.ActionHalt + } + + ui.Say("Waiting for source machine to be deleted...") + err = driver.WaitForMachineDeletion(machineId, 10*time.Minute) + if err != nil { + state.Put("error", fmt.Errorf("Problem waiting for source machine to be deleted: %s", err)) + return multistep.ActionHalt + } + + state.Put("machine", "") + + return multistep.ActionContinue +} + +func (s *StepDeleteMachine) Cleanup(state multistep.StateBag) { + // No clean up to do here... +} diff --git a/builder/triton/step_delete_machine_test.go b/builder/triton/step_delete_machine_test.go new file mode 100644 index 000000000..bca82b89a --- /dev/null +++ b/builder/triton/step_delete_machine_test.go @@ -0,0 +1,79 @@ +package triton + +import ( + "errors" + "testing" + + "github.com/mitchellh/multistep" +) + +func TestStepDeleteMachine(t *testing.T) { + state := testState(t) + step := new(StepDeleteMachine) + defer step.Cleanup(state) + + driver := state.Get("driver").(*DriverMock) + + machineId := "test-machine-id" + state.Put("machine", machineId) + + if action := step.Run(state); action != multistep.ActionContinue { + t.Fatalf("bad action: %#v", action) + } + + step.Cleanup(state) + + if driver.DeleteMachineId != machineId { + t.Fatalf("should've deleted machine (%s != %s)", driver.DeleteMachineId, machineId) + } +} + +func TestStepDeleteMachine_DeleteMachineError(t *testing.T) { + state := testState(t) + step := new(StepDeleteMachine) + defer step.Cleanup(state) + + driver := state.Get("driver").(*DriverMock) + + machineId := "test-machine-id" + state.Put("machine", machineId) + + driver.DeleteMachineErr = errors.New("error") + + if action := step.Run(state); action != multistep.ActionHalt { + t.Fatalf("bad action: %#v", action) + } + + if _, ok := state.GetOk("error"); !ok { + t.Fatalf("should have error") + } + + if _, ok := state.GetOk("machine"); !ok { + t.Fatalf("should have machine") + } +} + +func TestStepDeleteMachine_WaitForMachineDeletionError(t *testing.T) { + state := testState(t) + step := new(StepDeleteMachine) + defer step.Cleanup(state) + + driver := state.Get("driver").(*DriverMock) + + machineId := "test-machine-id" + state.Put("machine", machineId) + + driver.WaitForMachineDeletionErr = errors.New("error") + + if action := step.Run(state); action != multistep.ActionHalt { + t.Fatalf("bad action: %#v", action) + } + + if _, ok := state.GetOk("error"); !ok { + t.Fatalf("should have error") + } + + if _, ok := state.GetOk("machine"); !ok { + t.Fatalf("should have machine") + } +} diff --git a/builder/triton/step_stop_machine.go b/builder/triton/step_stop_machine.go new file mode 100644 index 000000000..1974eb733 --- /dev/null +++ b/builder/triton/step_stop_machine.go @@ -0,0 +1,41 @@ +package triton + +import ( + "fmt" + "time" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +// StepStopMachine stops the machine with the given Machine ID, and waits +// for it to reach the stopped state. +type StepStopMachine struct{} + +func (s *StepStopMachine) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + + machineId := state.Get("machine").(string) + + ui.Say(fmt.Sprintf("Stopping source machine (%s)...", machineId)) + err := driver.StopMachine(machineId) + if err != nil { + state.Put("error", fmt.Errorf("Problem stopping source machine: %s", err)) + return multistep.ActionHalt + } + + ui.Say(fmt.Sprintf("Waiting for source machine to stop (%s)...", machineId)) + err = driver.WaitForMachineState(machineId, "stopped", 10*time.Minute) + if err != nil { + state.Put("error", fmt.Errorf("Problem waiting for source machine to stop: %s", err)) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *StepStopMachine) Cleanup(state multistep.StateBag) { + // Explicitly don't clean up here as StepCreateSourceMachine will do it if necessary + // and there is no real meaning to cleaning this up. +} diff --git a/builder/triton/step_stop_machine_test.go b/builder/triton/step_stop_machine_test.go new file mode 100644 index 000000000..4609343e0 --- /dev/null +++ b/builder/triton/step_stop_machine_test.go @@ -0,0 +1,71 @@ +package triton + +import ( + "errors" + "testing" + + "github.com/mitchellh/multistep" +) + +func TestStepStopMachine(t *testing.T) { + state := testState(t) + step := new(StepStopMachine) + defer step.Cleanup(state) + + driver := state.Get("driver").(*DriverMock) + + machineId := "test-machine-id" + state.Put("machine", machineId) + + if action := step.Run(state); action != multistep.ActionContinue { + t.Fatalf("bad action: %#v", action) + } + + step.Cleanup(state) + + if driver.StopMachineId != machineId { + t.Fatalf("should've stopped machine (%s != %s)", driver.StopMachineId, machineId) + } +} + +func TestStepStopMachine_StopMachineError(t *testing.T) { + state := testState(t) + step := new(StepStopMachine) + defer step.Cleanup(state) + + driver := state.Get("driver").(*DriverMock) + + machineId := "test-machine-id" + state.Put("machine", machineId) + + driver.StopMachineErr = errors.New("error") + + if action := step.Run(state); action != multistep.ActionHalt { + t.Fatalf("bad action: %#v", action) + } + + if _, ok := state.GetOk("error"); !ok { + t.Fatalf("should have error") + } +} + +func TestStepStopMachine_WaitForMachineStoppedError(t *testing.T) { + state := testState(t) + step := new(StepStopMachine) + defer step.Cleanup(state) + + driver := state.Get("driver").(*DriverMock) + + machineId := "test-machine-id" + state.Put("machine", machineId) + + driver.WaitForMachineStateErr = errors.New("error") + + if action := step.Run(state); action != multistep.ActionHalt { + t.Fatalf("bad action: %#v", action) + } + + if _, ok := state.GetOk("error"); !ok { + t.Fatalf("should have error") + } +} diff --git a/builder/triton/step_test.go b/builder/triton/step_test.go new file mode 100644 index 000000000..1816af279 --- /dev/null +++ b/builder/triton/step_test.go @@ -0,0 +1,20 @@ +package triton + +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", testConfig(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 +} diff --git a/builder/triton/step_wait_for_stop_to_not_fail.go b/builder/triton/step_wait_for_stop_to_not_fail.go new file mode 100644 index 000000000..dfe719466 --- /dev/null +++ b/builder/triton/step_wait_for_stop_to_not_fail.go @@ -0,0 +1,24 @@ +package triton + +import ( + "time" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +// StepWaitForStopNotToFail waits for 10 seconds before returning with continue +// in order to prevent an observed issue where machines stopped immediately after +// they are started never actually stop. +type StepWaitForStopNotToFail struct{} + +func (s *StepWaitForStopNotToFail) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + ui.Say("Waiting 10 seconds to avoid potential SDC bug...") + time.Sleep(10 * time.Second) + return multistep.ActionContinue +} + +func (s *StepWaitForStopNotToFail) Cleanup(state multistep.StateBag) { + // No clean up required... +} diff --git a/builder/triton/target_image_config.go b/builder/triton/target_image_config.go new file mode 100644 index 000000000..3a180a8ee --- /dev/null +++ b/builder/triton/target_image_config.go @@ -0,0 +1,38 @@ +package triton + +import ( + "fmt" + + "github.com/mitchellh/packer/template/interpolate" +) + +// TargetImageConfig represents the configuration for the image to be created +// from the source machine. +type TargetImageConfig struct { + ImageName string `mapstructure:"image_name"` + ImageVersion string `mapstructure:"image_version"` + ImageDescription string `mapstructure:"image_description"` + ImageHomepage string `mapstructure:"image_homepage"` + ImageEULA string `mapstructure:"image_eula_url"` + ImageACL []string `mapstructure:"image_acls"` + ImageTags map[string]string `mapstructure:"image_tags"` +} + +// Prepare performs basic validation on a TargetImageConfig struct. +func (c *TargetImageConfig) Prepare(ctx *interpolate.Context) []error { + var errs []error + + if c.ImageName == "" { + errs = append(errs, fmt.Errorf("An image_name must be specified")) + } + + if c.ImageVersion == "" { + errs = append(errs, fmt.Errorf("An image_version must be specified")) + } + + if len(errs) > 0 { + return errs + } + + return nil +} diff --git a/builder/triton/target_image_config_test.go b/builder/triton/target_image_config_test.go new file mode 100644 index 000000000..87a3a879e --- /dev/null +++ b/builder/triton/target_image_config_test.go @@ -0,0 +1,46 @@ +package triton + +import ( + "testing" +) + +func TestTargetImageConfig_Prepare(t *testing.T) { + tic := testTargetImageConfig(t) + errs := tic.Prepare(nil) + if errs != nil { + t.Fatalf("should not error: %#v", tic) + } + + tic = testTargetImageConfig(t) + tic.ImageName = "" + errs = tic.Prepare(nil) + if errs == nil { + t.Fatalf("should error: %#v", tic) + } + + tic = testTargetImageConfig(t) + tic.ImageVersion = "" + errs = tic.Prepare(nil) + if errs == nil { + t.Fatalf("should error: %#v", tic) + } +} + +func testTargetImageConfig(t *testing.T) TargetImageConfig { + return TargetImageConfig{ + ImageName: "test-image", + ImageVersion: "test-version", + ImageDescription: "test-description", + ImageHomepage: "test-homepage", + ImageEULA: "test-eula", + ImageACL: []string{ + "test-acl-1", + "test-acl-2", + }, + ImageTags: map[string]string{ + "test-tags-key1": "test-tags-value1", + "test-tags-key2": "test-tags-value2", + "test-tags-key3": "test-tags-value3", + }, + } +} diff --git a/command/plugin.go b/command/plugin.go index 2a2fce88f..0c59c55b4 100644 --- a/command/plugin.go +++ b/command/plugin.go @@ -31,6 +31,7 @@ import ( parallelspvmbuilder "github.com/mitchellh/packer/builder/parallels/pvm" profitbricksbuilder "github.com/mitchellh/packer/builder/profitbricks" qemubuilder "github.com/mitchellh/packer/builder/qemu" + tritonbuilder "github.com/mitchellh/packer/builder/triton" virtualboxisobuilder "github.com/mitchellh/packer/builder/virtualbox/iso" virtualboxovfbuilder "github.com/mitchellh/packer/builder/virtualbox/ovf" vmwareisobuilder "github.com/mitchellh/packer/builder/vmware/iso" @@ -90,6 +91,7 @@ var Builders = map[string]packer.Builder{ "parallels-pvm": new(parallelspvmbuilder.Builder), "profitbricks": new(profitbricksbuilder.Builder), "qemu": new(qemubuilder.Builder), + "triton": new(tritonbuilder.Builder), "virtualbox-iso": new(virtualboxisobuilder.Builder), "virtualbox-ovf": new(virtualboxovfbuilder.Builder), "vmware-iso": new(vmwareisobuilder.Builder),