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.
pull/4325/head
James Nugent 10 years ago committed by Jasper Siepkes
parent f6fe8e8755
commit e15be036d7

@ -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
}

@ -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",
}
}

@ -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
}

@ -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()
}
}

@ -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
}

@ -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),
}
}

@ -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
}

@ -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
}

@ -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")
}

@ -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
}

@ -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,
}
}

@ -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
}

@ -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
}

@ -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")
}
}

@ -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
}
}
}

@ -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")
}
}

@ -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...
}

@ -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")
}
}

@ -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.
}

@ -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")
}
}

@ -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
}

@ -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...
}

@ -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
}

@ -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",
},
}
}

@ -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),

Loading…
Cancel
Save