diff --git a/builder/openstack/access_config.go b/builder/openstack/access_config.go new file mode 100644 index 000000000..4602ecb75 --- /dev/null +++ b/builder/openstack/access_config.go @@ -0,0 +1,73 @@ +package openstack + +import ( + "fmt" + "github.com/mitchellh/packer/packer" + "github.com/rackspace/gophercloud" + "os" +) + +// AccessConfig is for common configuration related to openstack access +type AccessConfig struct { + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` + Provider string `mapstructure:"provider"` +} + +// Auth returns a valid Auth object for access to openstack services, or +// an error if the authentication couldn't be resolved. +func (c *AccessConfig) Auth() (gophercloud.AccessProvider, error) { + username := c.Username + password := c.Password + provider := c.Provider + + if username == "" { + username = os.Getenv("SDK_USERNAME") + } + if password == "" { + password = os.Getenv("SDK_PASSWORD") + } + if provider == "" { + provider = os.Getenv("SDK_PROVIDER") + } + + authoptions := gophercloud.AuthOptions{ + Username: username, + Password: password, + AllowReauth: true, + } + + return gophercloud.Authenticate(provider, authoptions) +} + +func (c *AccessConfig) Prepare(t *packer.ConfigTemplate) []error { + if t == nil { + var err error + t, err = packer.NewConfigTemplate() + if err != nil { + return []error{err} + } + } + + templates := map[string]*string{ + "username": &c.Username, + "password": &c.Password, + "provider": &c.Provider, + } + + errs := make([]error, 0) + for n, ptr := range templates { + var err error + *ptr, err = t.Process(*ptr, nil) + if err != nil { + errs = append( + errs, fmt.Errorf("Error processing %s: %s", n, err)) + } + } + + if len(errs) > 0 { + return errs + } + + return nil +} diff --git a/builder/openstack/access_config_test.go b/builder/openstack/access_config_test.go new file mode 100644 index 000000000..3113edf52 --- /dev/null +++ b/builder/openstack/access_config_test.go @@ -0,0 +1,16 @@ +package openstack + +import ( + "testing" +) + +func testAccessConfig() *AccessConfig { + return &AccessConfig{} +} + +func TestAccessConfigPrepare_Region(t *testing.T) { + c := testAccessConfig() + if err := c.Prepare(nil); err != nil { + t.Fatalf("shouldn't have err: %s", err) + } +} diff --git a/builder/openstack/builder.go b/builder/openstack/builder.go new file mode 100644 index 000000000..cb24f9761 --- /dev/null +++ b/builder/openstack/builder.go @@ -0,0 +1,122 @@ +// The openstack package contains a packer.Builder implementation that +// builds Images for openstack. + +package openstack + +import ( + //"fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/packer" + "github.com/rackspace/gophercloud" + "log" +) + +// The unique ID for this builder +const BuilderId = "mitchellh.openstack" + +type config struct { + common.PackerConfig `mapstructure:",squash"` + AccessConfig `mapstructure:",squash"` + ImageConfig `mapstructure:",squash"` + RunConfig `mapstructure:",squash"` + + tpl *packer.ConfigTemplate +} + +type Builder struct { + config config + runner multistep.Runner +} + +func (b *Builder) Prepare(raws ...interface{}) error { + md, err := common.DecodeConfig(&b.config, raws...) + if err != nil { + return err + } + + b.config.tpl, err = packer.NewConfigTemplate() + if err != nil { + return err + } + b.config.tpl.UserVars = b.config.PackerUserVars + + // Accumulate any errors + errs := common.CheckUnusedConfig(md) + errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(b.config.tpl)...) + errs = packer.MultiErrorAppend(errs, b.config.ImageConfig.Prepare(b.config.tpl)...) + errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare(b.config.tpl)...) + + if errs != nil && len(errs.Errors) > 0 { + return errs + } + + log.Printf("Config: %+v", b.config) + return nil +} + +func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { + auth, err := b.config.AccessConfig.Auth() + if err != nil { + return nil, err + } + + // Setup the state bag and initial state for the steps + state := make(map[string]interface{}) + state["config"] = b.config + state["accessor"] = auth + api := &gophercloud.ApiCriteria{ + Name: "cloudServersOpenStack", + Region: "DFW", + VersionId: "2", + UrlChoice: gophercloud.PublicURL, + } + state["api"] = api + + state["hook"] = hook + state["ui"] = ui + + // Build the steps + steps := []multistep.Step{ + &StepKeyPair{}, + &StepRunSourceServer{ + Name: b.config.ImageName, + Flavor: b.config.Flavor, + SourceImage: b.config.SourceImage, + }, + &common.StepConnectSSH{ + SSHAddress: SSHAddress(&auth, api, b.config.SSHPort), + SSHConfig: SSHConfig(b.config.SSHUsername), + SSHWaitTimeout: b.config.SSHTimeout(), + }, + &common.StepProvision{}, + &stepCreateImage{}, + } + + // Run! + 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["error"]; ok { + return nil, rawErr.(error) + } + + // XXX - add artifact + return nil, nil +} + +func (b *Builder) Cancel() { + if b.runner != nil { + log.Println("Cancelling the step runner...") + b.runner.Cancel() + } +} diff --git a/builder/openstack/builder_test.go b/builder/openstack/builder_test.go new file mode 100644 index 000000000..38bf4b5f6 --- /dev/null +++ b/builder/openstack/builder_test.go @@ -0,0 +1,78 @@ +package openstack + +import ( + "github.com/mitchellh/packer/packer" + "testing" +) + +func testConfig() map[string]interface{} { + return map[string]interface{}{ + "username": "foo", + "password": "bar", + "provider": "foo", + "image_name": "foo", + "source_image": "foo", + "flavor": "foo", + "ssh_username": "root", + } +} + +func TestBuilder_ImplementsBuilder(t *testing.T) { + var raw interface{} + raw = &Builder{} + if _, ok := raw.(packer.Builder); !ok { + t.Fatalf("Builder should be a builder") + } +} + +func TestBuilder_Prepare_BadType(t *testing.T) { + b := &Builder{} + c := map[string]interface{}{ + "password": []string{}, + } + + err := b.Prepare(c) + if err == nil { + t.Fatalf("prepare should fail") + } +} + +func TestBuilderPrepare_ImageName(t *testing.T) { + var b Builder + config := testConfig() + + // Test good + config["image_name"] = "foo" + err := b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + // Test bad + config["image_name"] = "foo {{" + b = Builder{} + err = b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Test bad + delete(config, "image_name") + b = Builder{} + err = b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } +} + +func TestBuilderPrepare_InvalidKey(t *testing.T) { + var b Builder + config := testConfig() + + // Add a random key + config["i_should_not_be_valid"] = true + err := b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } +} diff --git a/builder/openstack/image_config.go b/builder/openstack/image_config.go new file mode 100644 index 000000000..652029b26 --- /dev/null +++ b/builder/openstack/image_config.go @@ -0,0 +1,45 @@ +package openstack + +import ( + "fmt" + "github.com/mitchellh/packer/packer" +) + +// ImageConfig is for common configuration related to creating Images. +type ImageConfig struct { + ImageName string `mapstructure:"image_name"` +} + +func (c *ImageConfig) Prepare(t *packer.ConfigTemplate) []error { + if t == nil { + var err error + t, err = packer.NewConfigTemplate() + if err != nil { + return []error{err} + } + } + + templates := map[string]*string{ + "image_name": &c.ImageName, + } + + errs := make([]error, 0) + for n, ptr := range templates { + var err error + *ptr, err = t.Process(*ptr, nil) + if err != nil { + errs = append( + errs, fmt.Errorf("Error processing %s: %s", n, err)) + } + } + + if c.ImageName == "" { + errs = append(errs, fmt.Errorf("An image_name must be specified")) + } + + if len(errs) > 0 { + return errs + } + + return nil +} diff --git a/builder/openstack/image_config_test.go b/builder/openstack/image_config_test.go new file mode 100644 index 000000000..4d81ecd94 --- /dev/null +++ b/builder/openstack/image_config_test.go @@ -0,0 +1,23 @@ +package openstack + +import ( + "testing" +) + +func testImageConfig() *ImageConfig { + return &ImageConfig{ + ImageName: "foo", + } +} + +func TestImageConfigPrepare_Region(t *testing.T) { + c := testImageConfig() + if err := c.Prepare(nil); err != nil { + t.Fatalf("shouldn't have err: %s", err) + } + + c.ImageName = "" + if err := c.Prepare(nil); err == nil { + t.Fatal("should have error") + } +} diff --git a/builder/openstack/run_config.go b/builder/openstack/run_config.go new file mode 100644 index 000000000..6c281a35f --- /dev/null +++ b/builder/openstack/run_config.go @@ -0,0 +1,86 @@ +package openstack + +import ( + "errors" + "fmt" + "github.com/mitchellh/packer/packer" + "time" +) + +// RunConfig contains configuration for running an instance from a source +// image and details on how to access that launched image. +type RunConfig struct { + SourceImage string `mapstructure:"source_image"` + Flavor string `mapstructure:"flavor"` + RawSSHTimeout string `mapstructure:"ssh_timeout"` + SSHUsername string `mapstructure:"ssh_username"` + SSHPort int `mapstructure:"ssh_port"` + + // Unexported fields that are calculated from others + sshTimeout time.Duration +} + +func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error { + if t == nil { + var err error + t, err = packer.NewConfigTemplate() + if err != nil { + return []error{err} + } + } + + // Defaults + if c.SSHUsername == "" { + c.SSHUsername = "root" + } + + if c.SSHPort == 0 { + c.SSHPort = 22 + } + + if c.RawSSHTimeout == "" { + c.RawSSHTimeout = "1m" + } + + // Validation + var err error + errs := make([]error, 0) + if c.SourceImage == "" { + errs = append(errs, errors.New("A source_image must be specified")) + } + + if c.Flavor == "" { + errs = append(errs, errors.New("A flavor must be specified")) + } + + if c.SSHUsername == "" { + errs = append(errs, errors.New("An ssh_username must be specified")) + } + + templates := map[string]*string{ + "flavlor": &c.Flavor, + "ssh_timeout": &c.RawSSHTimeout, + "ssh_username": &c.SSHUsername, + "source_image": &c.SourceImage, + } + + for n, ptr := range templates { + var err error + *ptr, err = t.Process(*ptr, nil) + if err != nil { + errs = append( + errs, fmt.Errorf("Error processing %s: %s", n, err)) + } + } + + c.sshTimeout, err = time.ParseDuration(c.RawSSHTimeout) + if err != nil { + errs = append(errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err)) + } + + return errs +} + +func (c *RunConfig) SSHTimeout() time.Duration { + return c.sshTimeout +} diff --git a/builder/openstack/run_config_test.go b/builder/openstack/run_config_test.go new file mode 100644 index 000000000..16b89b352 --- /dev/null +++ b/builder/openstack/run_config_test.go @@ -0,0 +1,88 @@ +package openstack + +import ( + "os" + "testing" +) + +func init() { + // Clear out the openstack env vars so they don't + // affect our tests. + os.Setenv("SDK_USERNAME", "") + os.Setenv("SDK_PASSWORD", "") + os.Setenv("SDK_PROVIDER", "") +} + +func testRunConfig() *RunConfig { + return &RunConfig{ + SourceImage: "abcd", + Flavor: "m1.small", + SSHUsername: "root", + } +} + +func TestRunConfigPrepare(t *testing.T) { + c := testRunConfig() + err := c.Prepare(nil) + if len(err) > 0 { + t.Fatalf("err: %s", err) + } +} + +func TestRunConfigPrepare_InstanceType(t *testing.T) { + c := testRunConfig() + c.Flavor = "" + if err := c.Prepare(nil); len(err) != 1 { + t.Fatalf("err: %s", err) + } +} + +func TestRunConfigPrepare_SourceImage(t *testing.T) { + c := testRunConfig() + c.SourceImage = "" + if err := c.Prepare(nil); len(err) != 1 { + t.Fatalf("err: %s", err) + } +} + +func TestRunConfigPrepare_SSHPort(t *testing.T) { + c := testRunConfig() + c.SSHPort = 0 + if err := c.Prepare(nil); len(err) != 0 { + t.Fatalf("err: %s", err) + } + + if c.SSHPort != 22 { + t.Fatalf("invalid value: %d", c.SSHPort) + } + + c.SSHPort = 44 + if err := c.Prepare(nil); len(err) != 0 { + t.Fatalf("err: %s", err) + } + + if c.SSHPort != 44 { + t.Fatalf("invalid value: %d", c.SSHPort) + } +} + +func TestRunConfigPrepare_SSHTimeout(t *testing.T) { + c := testRunConfig() + c.RawSSHTimeout = "" + if err := c.Prepare(nil); len(err) != 0 { + t.Fatalf("err: %s", err) + } + + c.RawSSHTimeout = "bad" + if err := c.Prepare(nil); len(err) != 1 { + t.Fatalf("err: %s", err) + } +} + +func TestRunConfigPrepare_SSHUsername(t *testing.T) { + c := testRunConfig() + c.SSHUsername = "" + if err := c.Prepare(nil); len(err) != 0 { + t.Fatalf("err: %s", err) + } +} diff --git a/builder/openstack/server.go b/builder/openstack/server.go new file mode 100644 index 000000000..66703bb44 --- /dev/null +++ b/builder/openstack/server.go @@ -0,0 +1,90 @@ +package openstack + +import ( + "errors" + "fmt" + "github.com/mitchellh/multistep" + "github.com/rackspace/gophercloud" + "log" + "time" +) + +// StateRefreshFunc is a function type used for StateChangeConf that is +// responsible for refreshing the item being watched for a state change. +// +// It returns three results. `result` is any object that will be returned +// as the final object after waiting for state change. This allows you to +// return the final updated object, for example an openstack instance after +// refreshing it. +// +// `state` is the latest state of that object. And `err` is any error that +// may have happened while refreshing the state. +type StateRefreshFunc func() (result interface{}, state string, progress int, err error) + +// StateChangeConf is the configuration struct used for `WaitForState`. +type StateChangeConf struct { + Accessor *gophercloud.Access + Api *gophercloud.ApiCriteria + Pending []string + Refresh StateRefreshFunc + StepState map[string]interface{} + Target string +} + +// ServerStateRefreshFunc returns a StateRefreshFunc that is used to watch +// an openstacn server. +func ServerStateRefreshFunc(accessor *gophercloud.Access, api *gophercloud.ApiCriteria, s *gophercloud.Server) StateRefreshFunc { + return func() (interface{}, string, int, error) { + csp, err := gophercloud.ServersApi(accessor, *api) + resp, err := csp.ServerById(s.Id) + if err != nil { + log.Printf("Error on ServerStateRefresh: %s", err) + return nil, "", 0, err + } + + return resp, resp.Status, resp.Progress, nil + } +} + +// WaitForState watches an object and waits for it to achieve a certain +// state. +func WaitForState(conf *StateChangeConf) (i interface{}, err error) { + log.Printf("Waiting for state to become: %s", conf.Target) + + for { + var currentProgress int + var currentState string + i, currentState, currentProgress, err = conf.Refresh() + if err != nil { + return + } + + if currentState == conf.Target { + return + } + + if conf.StepState != nil { + if _, ok := conf.StepState[multistep.StateCancelled]; ok { + return nil, errors.New("interrupted") + } + } + + found := false + for _, allowed := range conf.Pending { + if currentState == allowed { + found = true + break + } + } + + if !found { + fmt.Errorf("unexpected state '%s', wanted target '%s'", currentState, conf.Target) + return + } + + log.Printf("Waiting for state to become: %s currently %s (%d%%)", conf.Target, currentState, currentProgress) + time.Sleep(2 * time.Second) + } + + return +} diff --git a/builder/openstack/ssh.go b/builder/openstack/ssh.go new file mode 100644 index 000000000..0c415d074 --- /dev/null +++ b/builder/openstack/ssh.go @@ -0,0 +1,55 @@ +package openstack + +import ( + gossh "code.google.com/p/go.crypto/ssh" + "errors" + "fmt" + "github.com/mitchellh/packer/communicator/ssh" + "github.com/rackspace/gophercloud" + "time" +) + +// SSHAddress returns a function that can be given to the SSH communicator +// for determining the SSH address based on the server AccessIPv4 setting.. +func SSHAddress(accessor *gophercloud.AccessProvider, api *gophercloud.ApiCriteria, port int) func(map[string]interface{}) (string, error) { + return func(state map[string]interface{}) (string, error) { + for j := 0; j < 2; j++ { + s := state["server"].(*gophercloud.Server) + if s.AccessIPv4 != "" { + return fmt.Sprintf("%s:%d", s.AccessIPv4, port), nil + } + csp, err := gophercloud.ServersApi(*accessor, *api) + serverState, err := csp.ServerById(s.Id) + + if err != nil { + return "", err + } + + state["server"] = serverState + time.Sleep(1 * time.Second) + } + + return "", errors.New("couldn't determine IP address for server") + } +} + +// SSHConfig returns a function that can be used for the SSH communicator +// config for connecting to the instance created over SSH using the generated +// private key. +func SSHConfig(username string) func(map[string]interface{}) (*gossh.ClientConfig, error) { + return func(state map[string]interface{}) (*gossh.ClientConfig, error) { + privateKey := state["privateKey"].(string) + + keyring := new(ssh.SimpleKeychain) + if err := keyring.AddPEMKey(privateKey); err != nil { + return nil, fmt.Errorf("Error setting up SSH config: %s", err) + } + + return &gossh.ClientConfig{ + User: username, + Auth: []gossh.ClientAuth{ + gossh.ClientAuthKeyring(keyring), + }, + }, nil + } +} diff --git a/builder/openstack/step_create_image.go b/builder/openstack/step_create_image.go new file mode 100644 index 000000000..761fa4954 --- /dev/null +++ b/builder/openstack/step_create_image.go @@ -0,0 +1,71 @@ +package openstack + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "github.com/rackspace/gophercloud" + "log" + "time" +) + +type stepCreateImage struct{} + +func (s *stepCreateImage) Run(state map[string]interface{}) multistep.StepAction { + accessor := state["accessor"].(*gophercloud.Access) + api := state["api"].(*gophercloud.ApiCriteria) + config := state["config"].(config) + server := state["server"].(*gophercloud.Server) + ui := state["ui"].(packer.Ui) + + // Create the image + ui.Say(fmt.Sprintf("Creating the image: %s", config.ImageName)) + createOpts := gophercloud.CreateImage{ + Name: config.ImageName, + } + csp, err := gophercloud.ServersApi(accessor, *api) + imageId, err := csp.CreateImage(server.Id, createOpts) + if err != nil { + err := fmt.Errorf("Error creating image: %s", err) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // Set the Image ID in the state + ui.Say(fmt.Sprintf("Image: %s", imageId)) + state["image"] = imageId + + // Wait for the image to become ready + ui.Say("Waiting for image to become ready...") + if err := WaitForImage(accessor, api, imageId); err != nil { + err := fmt.Errorf("Error waiting for image: %s", err) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *stepCreateImage) Cleanup(map[string]interface{}) { + // No cleanup... +} + +// WaitForImage waits for the given Image ID to become ready. +func WaitForImage(accessor *gophercloud.Access, api *gophercloud.ApiCriteria, imageId string) error { + for { + csp, err := gophercloud.ServersApi(accessor, *api) + image, err := csp.ImageById(imageId) + if err != nil { + return err + } + + if image.Status == "ACTIVE" { + return nil + } + + log.Printf("Waiting for image creation status: %s (%d%%)", image.Status, image.Progress) + time.Sleep(2 * time.Second) + } +} diff --git a/builder/openstack/step_key_pair.go b/builder/openstack/step_key_pair.go new file mode 100644 index 000000000..ea630527c --- /dev/null +++ b/builder/openstack/step_key_pair.go @@ -0,0 +1,59 @@ +package openstack + +import ( + "cgl.tideland.biz/identifier" + "encoding/hex" + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "github.com/rackspace/gophercloud" + "log" +) + +type StepKeyPair struct { + keyName string +} + +func (s *StepKeyPair) Run(state map[string]interface{}) multistep.StepAction { + accessor := state["accessor"].(*gophercloud.Access) + api := state["api"].(*gophercloud.ApiCriteria) + ui := state["ui"].(packer.Ui) + + ui.Say("Creating temporary keypair for this instance...") + keyName := fmt.Sprintf("packer %s", hex.EncodeToString(identifier.NewUUID().Raw())) + log.Printf("temporary keypair name: %s", keyName) + csp, err := gophercloud.ServersApi(accessor, *api) + keyResp, err := csp.CreateKeyPair(gophercloud.NewKeyPair{Name: keyName}) + if err != nil { + state["error"] = fmt.Errorf("Error creating temporary keypair: %s", err) + return multistep.ActionHalt + } + + // Set the keyname so we know to delete it later + s.keyName = keyName + + // Set some state data for use in future steps + state["keyPair"] = keyName + state["privateKey"] = keyResp.PrivateKey + + return multistep.ActionContinue +} + +func (s *StepKeyPair) Cleanup(state map[string]interface{}) { + // If no key name is set, then we never created it, so just return + if s.keyName == "" { + return + } + + accessor := state["accessor"].(*gophercloud.Access) + api := state["api"].(*gophercloud.ApiCriteria) + ui := state["ui"].(packer.Ui) + + ui.Say("Deleting temporary keypair...") + csp, err := gophercloud.ServersApi(accessor, *api) + err = csp.DeleteKeyPair(s.keyName) + if err != nil { + ui.Error(fmt.Sprintf( + "Error cleaning up keypair. Please delete the key manually: %s", s.keyName)) + } +} diff --git a/builder/openstack/step_run_source_server.go b/builder/openstack/step_run_source_server.go new file mode 100644 index 000000000..11f47afbd --- /dev/null +++ b/builder/openstack/step_run_source_server.go @@ -0,0 +1,108 @@ +package openstack + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "github.com/rackspace/gophercloud" + "log" +) + +type StepRunSourceServer struct { + Flavor string + Name string + SourceImage string + + server *gophercloud.Server +} + +func (s *StepRunSourceServer) Run(state map[string]interface{}) multistep.StepAction { + accessor := state["accessor"].(*gophercloud.Access) + api := state["api"].(*gophercloud.ApiCriteria) + keyName := state["keyPair"].(string) + ui := state["ui"].(packer.Ui) + + csp, err := gophercloud.ServersApi(accessor, *api) + if err != nil { + err := fmt.Errorf("Error connecting to api: %s", err) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // XXX - validate image and flavor is available + + server := gophercloud.NewServer{ + Name: s.Name, + ImageRef: s.SourceImage, + FlavorRef: s.Flavor, + KeyPairName: keyName, + } + + serverResp, err := csp.CreateServer(server) + if err != nil { + err := fmt.Errorf("Error launching source server: %s", err) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + s.server, err = csp.ServerById(serverResp.Id) + log.Printf("server id: %s", s.server.Id) + + ui.Say(fmt.Sprintf("Waiting for server (%s) to become ready...", s.server.Id)) + stateChange := StateChangeConf{ + Accessor: accessor, + Api: api, + Pending: []string{"BUILD"}, + Target: "ACTIVE", + Refresh: ServerStateRefreshFunc(accessor, api, s.server), + StepState: state, + } + latestServer, err := WaitForState(&stateChange) + if err != nil { + err := fmt.Errorf("Error waiting for server (%s) to become ready: %s", s.server.Id, err) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + s.server = latestServer.(*gophercloud.Server) + state["server"] = s.server + + return multistep.ActionContinue +} + +func (s *StepRunSourceServer) Cleanup(state map[string]interface{}) { + if s.server == nil { + return + } + + accessor := state["accessor"].(*gophercloud.Access) + api := state["api"].(*gophercloud.ApiCriteria) + ui := state["ui"].(packer.Ui) + + csp, err := gophercloud.ServersApi(accessor, *api) + if err != nil { + err := fmt.Errorf("Error connecting to api: %s", err) + state["error"] = err + ui.Error(err.Error()) + return + } + + ui.Say("Terminating the source server...") + if err := csp.DeleteServerById(s.server.Id); err != nil { + ui.Error(fmt.Sprintf("Error terminating server, may still be around: %s", err)) + return + } + + stateChange := StateChangeConf{ + Accessor: accessor, + Api: api, + Pending: []string{"ACTIVE", "BUILD", "REBUILD", "SUSPENDED"}, + Refresh: ServerStateRefreshFunc(accessor, api, s.server), + Target: "DELETED", + } + + WaitForState(&stateChange) +} diff --git a/config.go b/config.go index a0ba29548..9b2c22a44 100644 --- a/config.go +++ b/config.go @@ -23,6 +23,7 @@ const defaultConfig = ` "amazon-chroot": "packer-builder-amazon-chroot", "amazon-instance": "packer-builder-amazon-instance", "digitalocean": "packer-builder-digitalocean", + "openstack": "packer-builder-openstack", "virtualbox": "packer-builder-virtualbox", "vmware": "packer-builder-vmware" }, diff --git a/plugin/builder-openstack/main.go b/plugin/builder-openstack/main.go new file mode 100644 index 000000000..076011003 --- /dev/null +++ b/plugin/builder-openstack/main.go @@ -0,0 +1,10 @@ +package main + +import ( + "github.com/mitchellh/packer/builder/openstack" + "github.com/mitchellh/packer/packer/plugin" +) + +func main() { + plugin.ServeBuilder(new(openstack.Builder)) +} diff --git a/plugin/builder-openstack/main_test.go b/plugin/builder-openstack/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/plugin/builder-openstack/main_test.go @@ -0,0 +1 @@ +package main