From c903579aaacc66c01c893e4dad21480f1188f415 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 11 Jun 2015 23:43:36 -0400 Subject: [PATCH 01/12] builder/openstack-new --- builder/openstack-new/access_config.go | 109 ++++++++++++++ builder/openstack-new/artifact.go | 47 ++++++ builder/openstack-new/artifact_test.go | 35 +++++ builder/openstack-new/builder.go | 134 ++++++++++++++++++ builder/openstack-new/builder_test.go | 94 ++++++++++++ builder/openstack-new/image_config.go | 25 ++++ builder/openstack-new/image_config_test.go | 23 +++ builder/openstack-new/run_config.go | 75 ++++++++++ builder/openstack-new/run_config_test.go | 88 ++++++++++++ builder/openstack-new/server.go | 100 +++++++++++++ builder/openstack-new/ssh.go | 89 ++++++++++++ builder/openstack-new/step_allocate_ip.go | 94 ++++++++++++ builder/openstack-new/step_create_image.go | 82 +++++++++++ builder/openstack-new/step_key_pair.go | 106 ++++++++++++++ .../openstack-new/step_run_source_server.go | 110 ++++++++++++++ .../step_wait_for_rackconnect.go | 52 +++++++ plugin/builder-openstack-new/main.go | 15 ++ 17 files changed, 1278 insertions(+) create mode 100644 builder/openstack-new/access_config.go create mode 100644 builder/openstack-new/artifact.go create mode 100644 builder/openstack-new/artifact_test.go create mode 100644 builder/openstack-new/builder.go create mode 100644 builder/openstack-new/builder_test.go create mode 100644 builder/openstack-new/image_config.go create mode 100644 builder/openstack-new/image_config_test.go create mode 100644 builder/openstack-new/run_config.go create mode 100644 builder/openstack-new/run_config_test.go create mode 100644 builder/openstack-new/server.go create mode 100644 builder/openstack-new/ssh.go create mode 100644 builder/openstack-new/step_allocate_ip.go create mode 100644 builder/openstack-new/step_create_image.go create mode 100644 builder/openstack-new/step_key_pair.go create mode 100644 builder/openstack-new/step_run_source_server.go create mode 100644 builder/openstack-new/step_wait_for_rackconnect.go create mode 100644 plugin/builder-openstack-new/main.go diff --git a/builder/openstack-new/access_config.go b/builder/openstack-new/access_config.go new file mode 100644 index 000000000..e0f962c50 --- /dev/null +++ b/builder/openstack-new/access_config.go @@ -0,0 +1,109 @@ +package openstack + +import ( + "crypto/tls" + "fmt" + "net/http" + "os" + + "github.com/mitchellh/packer/template/interpolate" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" +) + +// AccessConfig is for common configuration related to openstack access +type AccessConfig struct { + Username string `mapstructure:"username"` + UserID string `mapstructure:"user_id"` + Password string `mapstructure:"password"` + APIKey string `mapstructure:"api_key"` + IdentityEndpoint string `mapstructure:"identity_endpoint"` + TenantID string `mapstructure:"tenant_id"` + TenantName string `mapstructure:"tenant_name"` + DomainID string `mapstructure:"domain_id"` + DomainName string `mapstructure:"domain_name"` + Insecure bool `mapstructure:"insecure"` + Region string `mapstructure:"region"` + EndpointType string `mapstructure:"endpoint_type"` + + osClient *gophercloud.ProviderClient +} + +func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error { + if c.EndpointType != "internal" && c.EndpointType != "internalURL" && + c.EndpointType != "admin" && c.EndpointType != "adminURL" && + c.EndpointType != "public" && c.EndpointType != "publicURL" && + c.EndpointType != "" { + return []error{fmt.Errorf("Invalid endpoint type provided")} + } + + if c.Region == "" { + c.Region = os.Getenv("OS_REGION_NAME") + } + + // Get as much as possible from the end + ao, err := openstack.AuthOptionsFromEnv() + if err != nil { + return []error{err} + } + + // Override values if we have them in our config + overrides := []struct { + From, To *string + }{ + {&c.Username, &ao.Username}, + {&c.UserID, &ao.UserID}, + {&c.Password, &ao.Password}, + {&c.APIKey, &ao.APIKey}, + {&c.IdentityEndpoint, &ao.IdentityEndpoint}, + {&c.TenantID, &ao.TenantID}, + {&c.TenantName, &ao.TenantName}, + {&c.DomainID, &ao.DomainID}, + {&c.DomainName, &ao.DomainName}, + } + for _, s := range overrides { + if *s.From != "" { + *s.To = *s.From + } + } + + // Build the client itself + client, err := openstack.NewClient(ao.IdentityEndpoint) + if err != nil { + return []error{err} + } + + // If we have insecure set, then create a custom HTTP client that + // ignores SSL errors. + if c.Insecure { + config := &tls.Config{InsecureSkipVerify: true} + transport := &http.Transport{TLSClientConfig: config} + client.HTTPClient.Transport = transport + } + + // Auth + err = openstack.Authenticate(client, ao) + if err != nil { + return []error{err} + } + + c.osClient = client + return nil +} + +func (c *AccessConfig) computeV2Client() (*gophercloud.ServiceClient, error) { + return openstack.NewComputeV2(c.osClient, gophercloud.EndpointOpts{ + Region: c.Region, + Availability: c.getEndpointType(), + }) +} + +func (c *AccessConfig) getEndpointType() gophercloud.Availability { + if c.EndpointType == "internal" || c.EndpointType == "internalURL" { + return gophercloud.AvailabilityInternal + } + if c.EndpointType == "admin" || c.EndpointType == "adminURL" { + return gophercloud.AvailabilityAdmin + } + return gophercloud.AvailabilityPublic +} diff --git a/builder/openstack-new/artifact.go b/builder/openstack-new/artifact.go new file mode 100644 index 000000000..aa60d2641 --- /dev/null +++ b/builder/openstack-new/artifact.go @@ -0,0 +1,47 @@ +package openstack + +import ( + "fmt" + "log" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/compute/v2/images" +) + +// Artifact is an artifact implementation that contains built images. +type Artifact struct { + // ImageId of built image + ImageId string + + // BuilderId is the unique ID for the builder that created this image + BuilderIdValue string + + // OpenStack connection for performing API stuff. + Client *gophercloud.ServiceClient +} + +func (a *Artifact) BuilderId() string { + return a.BuilderIdValue +} + +func (*Artifact) Files() []string { + // We have no files + return nil +} + +func (a *Artifact) Id() string { + return a.ImageId +} + +func (a *Artifact) String() string { + return fmt.Sprintf("An image was created: %v", a.ImageId) +} + +func (a *Artifact) State(name string) interface{} { + return nil +} + +func (a *Artifact) Destroy() error { + log.Printf("Destroying image: %s", a.ImageId) + return images.Delete(a.Client, a.ImageId).ExtractErr() +} diff --git a/builder/openstack-new/artifact_test.go b/builder/openstack-new/artifact_test.go new file mode 100644 index 000000000..313fea7cf --- /dev/null +++ b/builder/openstack-new/artifact_test.go @@ -0,0 +1,35 @@ +package openstack + +import ( + "github.com/mitchellh/packer/packer" + "testing" +) + +func TestArtifact_Impl(t *testing.T) { + var _ packer.Artifact = new(Artifact) +} + +func TestArtifactId(t *testing.T) { + expected := `b8cdf55b-c916-40bd-b190-389ec144c4ed` + + a := &Artifact{ + ImageId: "b8cdf55b-c916-40bd-b190-389ec144c4ed", + } + + result := a.Id() + if result != expected { + t.Fatalf("bad: %s", result) + } +} + +func TestArtifactString(t *testing.T) { + expected := "An image was created: b8cdf55b-c916-40bd-b190-389ec144c4ed" + + a := &Artifact{ + ImageId: "b8cdf55b-c916-40bd-b190-389ec144c4ed", + } + result := a.String() + if result != expected { + t.Fatalf("bad: %s", result) + } +} diff --git a/builder/openstack-new/builder.go b/builder/openstack-new/builder.go new file mode 100644 index 000000000..bebb28452 --- /dev/null +++ b/builder/openstack-new/builder.go @@ -0,0 +1,134 @@ +// 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" + "log" + + "github.com/mitchellh/packer/helper/config" + "github.com/mitchellh/packer/packer" + "github.com/mitchellh/packer/template/interpolate" +) + +// 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"` + + ctx interpolate.Context +} + +type Builder struct { + config Config + runner multistep.Runner +} + +func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { + err := config.Decode(&b.config, &config.DecodeOpts{ + Interpolate: true, + }, raws...) + if err != nil { + return nil, err + } + + // Accumulate any errors + var errs *packer.MultiError + errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(&b.config.ctx)...) + errs = packer.MultiErrorAppend(errs, b.config.ImageConfig.Prepare(&b.config.ctx)...) + errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare(&b.config.ctx)...) + + if errs != nil && len(errs.Errors) > 0 { + return nil, errs + } + + log.Println(common.ScrubConfig(b.config, b.config.Password)) + return nil, nil +} + +func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { + computeClient, err := b.config.computeV2Client() + if err != nil { + return nil, fmt.Errorf("Error initializing compute client: %s", err) + } + + // Setup the state bag and initial state for the steps + state := new(multistep.BasicStateBag) + state.Put("config", b.config) + state.Put("hook", hook) + state.Put("ui", ui) + + // Build the steps + steps := []multistep.Step{ + &StepKeyPair{ + Debug: b.config.PackerDebug, + DebugKeyPath: fmt.Sprintf("os_%s.pem", b.config.PackerBuildName), + }, + &StepRunSourceServer{ + Name: b.config.ImageName, + Flavor: b.config.Flavor, + SourceImage: b.config.SourceImage, + SecurityGroups: b.config.SecurityGroups, + Networks: b.config.Networks, + }, + &StepWaitForRackConnect{ + Wait: b.config.RackconnectWait, + }, + &StepAllocateIp{ + FloatingIpPool: b.config.FloatingIpPool, + FloatingIp: b.config.FloatingIp, + }, + &common.StepConnectSSH{ + SSHAddress: SSHAddress(computeClient, b.config.SSHInterface, 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.GetOk("error"); ok { + return nil, rawErr.(error) + } + + // If there are no images, then just return + if _, ok := state.GetOk("image"); !ok { + return nil, nil + } + + // Build the artifact and return it + artifact := &Artifact{ + ImageId: state.Get("image").(string), + BuilderIdValue: BuilderId, + Client: computeClient, + } + + return artifact, nil +} + +func (b *Builder) Cancel() { + if b.runner != nil { + log.Println("Cancelling the step runner...") + b.runner.Cancel() + } +} diff --git a/builder/openstack-new/builder_test.go b/builder/openstack-new/builder_test.go new file mode 100644 index 000000000..badf9784d --- /dev/null +++ b/builder/openstack-new/builder_test.go @@ -0,0 +1,94 @@ +package openstack + +import ( + "github.com/mitchellh/packer/packer" + "testing" +) + +func testConfig() map[string]interface{} { + return map[string]interface{}{ + "username": "foo", + "password": "bar", + "provider": "foo", + "region": "DFW", + "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{}, + } + + warns, err := b.Prepare(c) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + 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" + warns, err := b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + // Test bad + config["image_name"] = "foo {{" + b = Builder{} + warns, err = b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } + + // Test bad + delete(config, "image_name") + b = Builder{} + warns, err = b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + 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 + warns, err := b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } +} diff --git a/builder/openstack-new/image_config.go b/builder/openstack-new/image_config.go new file mode 100644 index 000000000..124449eab --- /dev/null +++ b/builder/openstack-new/image_config.go @@ -0,0 +1,25 @@ +package openstack + +import ( + "fmt" + + "github.com/mitchellh/packer/template/interpolate" +) + +// ImageConfig is for common configuration related to creating Images. +type ImageConfig struct { + ImageName string `mapstructure:"image_name"` +} + +func (c *ImageConfig) Prepare(ctx *interpolate.Context) []error { + errs := make([]error, 0) + 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-new/image_config_test.go b/builder/openstack-new/image_config_test.go new file mode 100644 index 000000000..4d81ecd94 --- /dev/null +++ b/builder/openstack-new/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-new/run_config.go b/builder/openstack-new/run_config.go new file mode 100644 index 000000000..e5d73c9c1 --- /dev/null +++ b/builder/openstack-new/run_config.go @@ -0,0 +1,75 @@ +package openstack + +import ( + "errors" + "fmt" + "time" + + "github.com/mitchellh/packer/template/interpolate" +) + +// 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"` + SSHInterface string `mapstructure:"ssh_interface"` + OpenstackProvider string `mapstructure:"openstack_provider"` + UseFloatingIp bool `mapstructure:"use_floating_ip"` + RackconnectWait bool `mapstructure:"rackconnect_wait"` + FloatingIpPool string `mapstructure:"floating_ip_pool"` + FloatingIp string `mapstructure:"floating_ip"` + SecurityGroups []string `mapstructure:"security_groups"` + Networks []string `mapstructure:"networks"` + + // Unexported fields that are calculated from others + sshTimeout time.Duration +} + +func (c *RunConfig) Prepare(ctx *interpolate.Context) []error { + // Defaults + if c.SSHUsername == "" { + c.SSHUsername = "root" + } + + if c.SSHPort == 0 { + c.SSHPort = 22 + } + + if c.RawSSHTimeout == "" { + c.RawSSHTimeout = "5m" + } + + if c.UseFloatingIp && c.FloatingIpPool == "" { + c.FloatingIpPool = "public" + } + + // 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")) + } + + 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-new/run_config_test.go b/builder/openstack-new/run_config_test.go new file mode 100644 index 000000000..16b89b352 --- /dev/null +++ b/builder/openstack-new/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-new/server.go b/builder/openstack-new/server.go new file mode 100644 index 000000000..a87ef0110 --- /dev/null +++ b/builder/openstack-new/server.go @@ -0,0 +1,100 @@ +package openstack + +import ( + "errors" + "fmt" + "log" + "time" + + "github.com/mitchellh/multistep" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" +) + +// 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 { + Pending []string + Refresh StateRefreshFunc + StepState multistep.StateBag + Target string +} + +// ServerStateRefreshFunc returns a StateRefreshFunc that is used to watch +// an openstack server. +func ServerStateRefreshFunc( + client *gophercloud.ServiceClient, s *servers.Server) StateRefreshFunc { + return func() (interface{}, string, int, error) { + var serverNew *servers.Server + result := servers.Get(client, s.ID) + err := result.Err + if err == nil { + serverNew, err = result.Extract() + } + if result.Err != nil { + errCode, ok := result.Err.(*gophercloud.UnexpectedResponseCodeError) + if ok && errCode.Actual == 404 { + log.Printf("[INFO] 404 on ServerStateRefresh, returning DELETED") + return nil, "DELETED", 0, nil + } else { + log.Printf("[ERROR] Error on ServerStateRefresh: %s", result.Err) + return nil, "", 0, result.Err + } + } + + return serverNew, serverNew.Status, serverNew.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.GetOk(multistep.StateCancelled); ok { + return nil, errors.New("interrupted") + } + } + + found := false + for _, allowed := range conf.Pending { + if currentState == allowed { + found = true + break + } + } + + if !found { + return nil, fmt.Errorf("unexpected state '%s', wanted target '%s'", currentState, conf.Target) + } + + 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-new/ssh.go b/builder/openstack-new/ssh.go new file mode 100644 index 000000000..a3de654f6 --- /dev/null +++ b/builder/openstack-new/ssh.go @@ -0,0 +1,89 @@ +package openstack + +import ( + "errors" + "fmt" + "time" + + "github.com/mitchellh/multistep" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "golang.org/x/crypto/ssh" +) + +// 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( + client *gophercloud.ServiceClient, + sshinterface string, port int) func(multistep.StateBag) (string, error) { + return func(state multistep.StateBag) (string, error) { + s := state.Get("server").(*servers.Server) + + // If we have a floating IP, use that + if ip := state.Get("access_ip").(*floatingip.FloatingIP); ip.FixedIP != "" { + return fmt.Sprintf("%s:%d", ip.FixedIP, port), nil + } + + if s.AccessIPv4 != "" { + return fmt.Sprintf("%s:%d", s.AccessIPv4, port), nil + } + + // Get all the addresses associated with this server + /* + ip_pools, err := s.AllAddressPools() + if err != nil { + return "", errors.New("Error parsing SSH addresses") + } + for pool, addresses := range ip_pools { + if sshinterface != "" { + if pool != sshinterface { + continue + } + } + if pool != "" { + for _, address := range addresses { + if address.Addr != "" && address.Version == 4 { + return fmt.Sprintf("%s:%d", address.Addr, port), nil + } + } + } + } + */ + + result := servers.Get(client, s.ID) + err := result.Err + if err == nil { + s, err = result.Extract() + } + if err != nil { + return "", err + } + + state.Put("server", s) + 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(multistep.StateBag) (*ssh.ClientConfig, error) { + return func(state multistep.StateBag) (*ssh.ClientConfig, error) { + privateKey := state.Get("privateKey").(string) + + signer, err := ssh.ParsePrivateKey([]byte(privateKey)) + if err != nil { + return nil, fmt.Errorf("Error setting up SSH config: %s", err) + } + + return &ssh.ClientConfig{ + User: username, + Auth: []ssh.AuthMethod{ + ssh.PublicKeys(signer), + }, + }, nil + } +} diff --git a/builder/openstack-new/step_allocate_ip.go b/builder/openstack-new/step_allocate_ip.go new file mode 100644 index 000000000..adb15eb5b --- /dev/null +++ b/builder/openstack-new/step_allocate_ip.go @@ -0,0 +1,94 @@ +package openstack + +import ( + "fmt" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" +) + +type StepAllocateIp struct { + FloatingIpPool string + FloatingIp string +} + +func (s *StepAllocateIp) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + config := state.Get("config").(Config) + server := state.Get("server").(*servers.Server) + + // We need the v2 compute client + client, err := config.computeV2Client() + if err != nil { + err = fmt.Errorf("Error initializing compute client: %s", err) + state.Put("error", err) + return multistep.ActionHalt + } + + var instanceIp *floatingip.FloatingIP + // This is here in case we error out before putting instanceIp into the + // statebag below, because it is requested by Cleanup() + state.Put("access_ip", instanceIp) + + if s.FloatingIp != "" { + instanceIp.FixedIP = s.FloatingIp + } else if s.FloatingIpPool != "" { + newIp, err := floatingip.Create(client, floatingip.CreateOpts{ + Pool: s.FloatingIpPool, + }).Extract() + if err != nil { + err := fmt.Errorf("Error creating floating ip from pool '%s'", s.FloatingIpPool) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + instanceIp = newIp + ui.Say(fmt.Sprintf("Created temporary floating IP %s...", instanceIp.FixedIP)) + } + + if instanceIp.FixedIP != "" { + err := floatingip.Associate(client, server.ID, instanceIp.FixedIP).ExtractErr() + if err != nil { + err := fmt.Errorf( + "Error associating floating IP %s with instance.", + instanceIp.FixedIP) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + ui.Say(fmt.Sprintf( + "Added floating IP %s to instance...", instanceIp.FixedIP)) + } + + state.Put("access_ip", instanceIp) + + return multistep.ActionContinue +} + +func (s *StepAllocateIp) Cleanup(state multistep.StateBag) { + config := state.Get("config").(Config) + ui := state.Get("ui").(packer.Ui) + instanceIp := state.Get("access_ip").(*floatingip.FloatingIP) + + // We need the v2 compute client + client, err := config.computeV2Client() + if err != nil { + ui.Error(fmt.Sprintf( + "Error deleting temporary floating IP %s", instanceIp.FixedIP)) + return + } + + if s.FloatingIpPool != "" && instanceIp.ID != "" { + if err := floatingip.Delete(client, instanceIp.ID).ExtractErr(); err != nil { + ui.Error(fmt.Sprintf( + "Error deleting temporary floating IP %s", instanceIp.FixedIP)) + return + } + + ui.Say(fmt.Sprintf("Deleted temporary floating IP %s", instanceIp.FixedIP)) + } +} diff --git a/builder/openstack-new/step_create_image.go b/builder/openstack-new/step_create_image.go new file mode 100644 index 000000000..df540e311 --- /dev/null +++ b/builder/openstack-new/step_create_image.go @@ -0,0 +1,82 @@ +package openstack + +import ( + "fmt" + "log" + "time" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/compute/v2/images" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" +) + +type stepCreateImage struct{} + +func (s *stepCreateImage) Run(state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(Config) + server := state.Get("server").(*servers.Server) + ui := state.Get("ui").(packer.Ui) + + // We need the v2 compute client + client, err := config.computeV2Client() + if err != nil { + err = fmt.Errorf("Error initializing compute client: %s", err) + state.Put("error", err) + return multistep.ActionHalt + } + + // Create the image + ui.Say(fmt.Sprintf("Creating the image: %s", config.ImageName)) + imageId, err := servers.CreateImage(client, server.ID, servers.CreateImageOpts{ + Name: config.ImageName, + }).ExtractImageID() + if err != nil { + err := fmt.Errorf("Error creating image: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // Set the Image ID in the state + ui.Say(fmt.Sprintf("Image: %s", imageId)) + state.Put("image", imageId) + + // Wait for the image to become ready + ui.Say("Waiting for image to become ready...") + if err := WaitForImage(client, imageId); err != nil { + err := fmt.Errorf("Error waiting for image: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *stepCreateImage) Cleanup(multistep.StateBag) { + // No cleanup... +} + +// WaitForImage waits for the given Image ID to become ready. +func WaitForImage(client *gophercloud.ServiceClient, imageId string) error { + for { + var image *images.Image + result := images.Get(client, imageId) + err := result.Err + if err == nil { + image, err = result.Extract() + } + 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-new/step_key_pair.go b/builder/openstack-new/step_key_pair.go new file mode 100644 index 000000000..06bcbf9ea --- /dev/null +++ b/builder/openstack-new/step_key_pair.go @@ -0,0 +1,106 @@ +package openstack + +import ( + "fmt" + "os" + "runtime" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/common/uuid" + "github.com/mitchellh/packer/packer" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" +) + +type StepKeyPair struct { + Debug bool + DebugKeyPath string + keyName string +} + +func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(Config) + ui := state.Get("ui").(packer.Ui) + + // We need the v2 compute client + computeClient, err := config.computeV2Client() + if err != nil { + err = fmt.Errorf("Error initializing compute client: %s", err) + state.Put("error", err) + return multistep.ActionHalt + } + + ui.Say("Creating temporary keypair for this instance...") + keyName := fmt.Sprintf("packer %s", uuid.TimeOrderedUUID()) + keypair, err := keypairs.Create(computeClient, keypairs.CreateOpts{ + Name: keyName, + }).Extract() + if err != nil { + state.Put("error", fmt.Errorf("Error creating temporary keypair: %s", err)) + return multistep.ActionHalt + } + + if keypair.PrivateKey == "" { + state.Put("error", fmt.Errorf("The temporary keypair returned was blank")) + return multistep.ActionHalt + } + + // If we're in debug mode, output the private key to the working + // directory. + if s.Debug { + ui.Message(fmt.Sprintf("Saving key for debug purposes: %s", s.DebugKeyPath)) + f, err := os.Create(s.DebugKeyPath) + if err != nil { + state.Put("error", fmt.Errorf("Error saving debug key: %s", err)) + return multistep.ActionHalt + } + defer f.Close() + + // Write the key out + if _, err := f.Write([]byte(keypair.PrivateKey)); err != nil { + state.Put("error", fmt.Errorf("Error saving debug key: %s", err)) + return multistep.ActionHalt + } + + // Chmod it so that it is SSH ready + if runtime.GOOS != "windows" { + if err := f.Chmod(0600); err != nil { + state.Put("error", fmt.Errorf("Error setting permissions of debug key: %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.Put("keyPair", keyName) + state.Put("privateKey", keypair.PrivateKey) + + return multistep.ActionContinue +} + +func (s *StepKeyPair) Cleanup(state multistep.StateBag) { + // If no key name is set, then we never created it, so just return + if s.keyName == "" { + return + } + + config := state.Get("config").(Config) + ui := state.Get("ui").(packer.Ui) + + // We need the v2 compute client + computeClient, err := config.computeV2Client() + if err != nil { + ui.Error(fmt.Sprintf( + "Error cleaning up keypair. Please delete the key manually: %s", s.keyName)) + return + } + + ui.Say("Deleting temporary keypair...") + err = keypairs.Delete(computeClient, s.keyName).ExtractErr() + if err != nil { + ui.Error(fmt.Sprintf( + "Error cleaning up keypair. Please delete the key manually: %s", s.keyName)) + } +} diff --git a/builder/openstack-new/step_run_source_server.go b/builder/openstack-new/step_run_source_server.go new file mode 100644 index 000000000..e58e2c46b --- /dev/null +++ b/builder/openstack-new/step_run_source_server.go @@ -0,0 +1,110 @@ +package openstack + +import ( + "fmt" + "log" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" +) + +type StepRunSourceServer struct { + Flavor string + Name string + SourceImage string + SecurityGroups []string + Networks []string + + server *servers.Server +} + +func (s *StepRunSourceServer) Run(state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(Config) + keyName := state.Get("keyPair").(string) + ui := state.Get("ui").(packer.Ui) + + // We need the v2 compute client + computeClient, err := config.computeV2Client() + if err != nil { + err = fmt.Errorf("Error initializing compute client: %s", err) + state.Put("error", err) + return multistep.ActionHalt + } + + networks := make([]servers.Network, len(s.Networks)) + for i, networkUuid := range s.Networks { + networks[i].UUID = networkUuid + } + + s.server, err = servers.Create(computeClient, keypairs.CreateOptsExt{ + CreateOptsBuilder: servers.CreateOpts{ + Name: s.Name, + ImageRef: s.SourceImage, + FlavorRef: s.Flavor, + SecurityGroups: s.SecurityGroups, + Networks: networks, + }, + + KeyName: keyName, + }).Extract() + if err != nil { + err := fmt.Errorf("Error launching source server: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + log.Printf("server id: %s", s.server.ID) + + ui.Say(fmt.Sprintf("Waiting for server (%s) to become ready...", s.server.ID)) + stateChange := StateChangeConf{ + Pending: []string{"BUILD"}, + Target: "ACTIVE", + Refresh: ServerStateRefreshFunc(computeClient, 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.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + s.server = latestServer.(*servers.Server) + state.Put("server", s.server) + + return multistep.ActionContinue +} + +func (s *StepRunSourceServer) Cleanup(state multistep.StateBag) { + if s.server == nil { + return + } + + config := state.Get("config").(Config) + ui := state.Get("ui").(packer.Ui) + + // We need the v2 compute client + computeClient, err := config.computeV2Client() + if err != nil { + ui.Error(fmt.Sprintf("Error terminating server, may still be around: %s", err)) + return + } + + ui.Say("Terminating the source server...") + if err := servers.Delete(computeClient, s.server.ID).ExtractErr(); err != nil { + ui.Error(fmt.Sprintf("Error terminating server, may still be around: %s", err)) + return + } + + stateChange := StateChangeConf{ + Pending: []string{"ACTIVE", "BUILD", "REBUILD", "SUSPENDED"}, + Refresh: ServerStateRefreshFunc(computeClient, s.server), + Target: "DELETED", + } + + WaitForState(&stateChange) +} diff --git a/builder/openstack-new/step_wait_for_rackconnect.go b/builder/openstack-new/step_wait_for_rackconnect.go new file mode 100644 index 000000000..6263bd17d --- /dev/null +++ b/builder/openstack-new/step_wait_for_rackconnect.go @@ -0,0 +1,52 @@ +package openstack + +import ( + "fmt" + "time" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" +) + +type StepWaitForRackConnect struct { + Wait bool +} + +func (s *StepWaitForRackConnect) Run(state multistep.StateBag) multistep.StepAction { + if !s.Wait { + return multistep.ActionContinue + } + + config := state.Get("config").(Config) + server := state.Get("server").(*servers.Server) + ui := state.Get("ui").(packer.Ui) + + // We need the v2 compute client + computeClient, err := config.computeV2Client() + if err != nil { + err = fmt.Errorf("Error initializing compute client: %s", err) + state.Put("error", err) + return multistep.ActionHalt + } + + ui.Say(fmt.Sprintf( + "Waiting for server (%s) to become RackConnect ready...", server.ID)) + for { + server, err = servers.Get(computeClient, server.ID).Extract() + if err != nil { + return multistep.ActionHalt + } + + if server.Metadata["rackconnect_automation_status"] == "DEPLOYED" { + break + } + + time.Sleep(2 * time.Second) + } + + return multistep.ActionContinue +} + +func (s *StepWaitForRackConnect) Cleanup(state multistep.StateBag) { +} diff --git a/plugin/builder-openstack-new/main.go b/plugin/builder-openstack-new/main.go new file mode 100644 index 000000000..d8075c78d --- /dev/null +++ b/plugin/builder-openstack-new/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "github.com/mitchellh/packer/builder/openstack-new" + "github.com/mitchellh/packer/packer/plugin" +) + +func main() { + server, err := plugin.Server() + if err != nil { + panic(err) + } + server.RegisterBuilder(new(openstack.Builder)) + server.Serve() +} From 551e80774d0b0f28547a5373a59821fbf0cfeaf8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 12 Jun 2015 00:03:17 -0400 Subject: [PATCH 02/12] builder/openstack-new: fix some issues --- builder/openstack-new/ssh.go | 47 ++++++++++--------- builder/openstack-new/step_allocate_ip.go | 7 ++- .../openstack-new/step_run_source_server.go | 2 +- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/builder/openstack-new/ssh.go b/builder/openstack-new/ssh.go index a3de654f6..7b0510f98 100644 --- a/builder/openstack-new/ssh.go +++ b/builder/openstack-new/ssh.go @@ -3,6 +3,7 @@ package openstack import ( "errors" "fmt" + "log" "time" "github.com/mitchellh/multistep" @@ -21,7 +22,8 @@ func SSHAddress( s := state.Get("server").(*servers.Server) // If we have a floating IP, use that - if ip := state.Get("access_ip").(*floatingip.FloatingIP); ip.FixedIP != "" { + ip := state.Get("access_ip").(*floatingip.FloatingIP) + if ip != nil && ip.FixedIP != "" { return fmt.Sprintf("%s:%d", ip.FixedIP, port), nil } @@ -29,33 +31,34 @@ func SSHAddress( return fmt.Sprintf("%s:%d", s.AccessIPv4, port), nil } - // Get all the addresses associated with this server - /* - ip_pools, err := s.AllAddressPools() - if err != nil { - return "", errors.New("Error parsing SSH addresses") + // Get all the addresses associated with this server. This + // was taken directly from Terraform. + for _, networkAddresses := range s.Addresses { + elements, ok := networkAddresses.([]interface{}) + if !ok { + log.Printf( + "[ERROR] Unknown return type for address field: %#v", + networkAddresses) + continue } - for pool, addresses := range ip_pools { - if sshinterface != "" { - if pool != sshinterface { - continue + + for _, element := range elements { + var addr string + address := element.(map[string]interface{}) + if address["OS-EXT-IPS:type"] == "floating" { + addr = address["addr"].(string) + } else { + if address["version"].(float64) == 4 { + addr = address["addr"].(string) } } - if pool != "" { - for _, address := range addresses { - if address.Addr != "" && address.Version == 4 { - return fmt.Sprintf("%s:%d", address.Addr, port), nil - } - } + if addr != "" { + return fmt.Sprintf("%s:%d", addr, port), nil } } - */ - - result := servers.Get(client, s.ID) - err := result.Err - if err == nil { - s, err = result.Extract() } + + s, err := servers.Get(client, s.ID).Extract() if err != nil { return "", err } diff --git a/builder/openstack-new/step_allocate_ip.go b/builder/openstack-new/step_allocate_ip.go index adb15eb5b..16efe8d38 100644 --- a/builder/openstack-new/step_allocate_ip.go +++ b/builder/openstack-new/step_allocate_ip.go @@ -33,7 +33,7 @@ func (s *StepAllocateIp) Run(state multistep.StateBag) multistep.StepAction { state.Put("access_ip", instanceIp) if s.FloatingIp != "" { - instanceIp.FixedIP = s.FloatingIp + *instanceIp = floatingip.FloatingIP{FixedIP: s.FloatingIp} } else if s.FloatingIpPool != "" { newIp, err := floatingip.Create(client, floatingip.CreateOpts{ Pool: s.FloatingIpPool, @@ -45,11 +45,11 @@ func (s *StepAllocateIp) Run(state multistep.StateBag) multistep.StepAction { return multistep.ActionHalt } - instanceIp = newIp + *instanceIp = *newIp ui.Say(fmt.Sprintf("Created temporary floating IP %s...", instanceIp.FixedIP)) } - if instanceIp.FixedIP != "" { + if instanceIp != nil && instanceIp.FixedIP != "" { err := floatingip.Associate(client, server.ID, instanceIp.FixedIP).ExtractErr() if err != nil { err := fmt.Errorf( @@ -65,7 +65,6 @@ func (s *StepAllocateIp) Run(state multistep.StateBag) multistep.StepAction { } state.Put("access_ip", instanceIp) - return multistep.ActionContinue } diff --git a/builder/openstack-new/step_run_source_server.go b/builder/openstack-new/step_run_source_server.go index e58e2c46b..4432d5860 100644 --- a/builder/openstack-new/step_run_source_server.go +++ b/builder/openstack-new/step_run_source_server.go @@ -42,7 +42,7 @@ func (s *StepRunSourceServer) Run(state multistep.StateBag) multistep.StepAction CreateOptsBuilder: servers.CreateOpts{ Name: s.Name, ImageRef: s.SourceImage, - FlavorRef: s.Flavor, + FlavorName: s.Flavor, SecurityGroups: s.SecurityGroups, Networks: networks, }, From 7a46b80cfb24aa307286a2b67c253f7716b3d3fc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 12 Jun 2015 00:05:24 -0400 Subject: [PATCH 03/12] builder/openstack-new: better UI --- builder/openstack-new/step_create_image.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builder/openstack-new/step_create_image.go b/builder/openstack-new/step_create_image.go index df540e311..989db81e5 100644 --- a/builder/openstack-new/step_create_image.go +++ b/builder/openstack-new/step_create_image.go @@ -40,7 +40,7 @@ func (s *stepCreateImage) Run(state multistep.StateBag) multistep.StepAction { } // Set the Image ID in the state - ui.Say(fmt.Sprintf("Image: %s", imageId)) + ui.Message(fmt.Sprintf("Image: %s", imageId)) state.Put("image", imageId) // Wait for the image to become ready From 46f518f21df6a127d3e9e64eabfdc31441aa4a1c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 12 Jun 2015 00:09:01 -0400 Subject: [PATCH 04/12] builder/openstack: proper error extraction --- builder/openstack-new/server.go | 15 +++++---------- builder/openstack-new/step_create_image.go | 14 ++++++++------ 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/builder/openstack-new/server.go b/builder/openstack-new/server.go index a87ef0110..de8c9d103 100644 --- a/builder/openstack-new/server.go +++ b/builder/openstack-new/server.go @@ -36,20 +36,15 @@ type StateChangeConf struct { func ServerStateRefreshFunc( client *gophercloud.ServiceClient, s *servers.Server) StateRefreshFunc { return func() (interface{}, string, int, error) { - var serverNew *servers.Server - result := servers.Get(client, s.ID) - err := result.Err - if err == nil { - serverNew, err = result.Extract() - } - if result.Err != nil { - errCode, ok := result.Err.(*gophercloud.UnexpectedResponseCodeError) + serverNew, err := servers.Get(client, s.ID).Extract() + if err != nil { + errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError) if ok && errCode.Actual == 404 { log.Printf("[INFO] 404 on ServerStateRefresh, returning DELETED") return nil, "DELETED", 0, nil } else { - log.Printf("[ERROR] Error on ServerStateRefresh: %s", result.Err) - return nil, "", 0, result.Err + log.Printf("[ERROR] Error on ServerStateRefresh: %s", err) + return nil, "", 0, err } } diff --git a/builder/openstack-new/step_create_image.go b/builder/openstack-new/step_create_image.go index 989db81e5..b777e8b0b 100644 --- a/builder/openstack-new/step_create_image.go +++ b/builder/openstack-new/step_create_image.go @@ -62,13 +62,15 @@ func (s *stepCreateImage) Cleanup(multistep.StateBag) { // WaitForImage waits for the given Image ID to become ready. func WaitForImage(client *gophercloud.ServiceClient, imageId string) error { for { - var image *images.Image - result := images.Get(client, imageId) - err := result.Err - if err == nil { - image, err = result.Extract() - } + image, err := images.Get(client, imageId).Extract() if err != nil { + errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if ok && errCode.Actual == 500 { + log.Printf("[ERROR] 500 error received, will ignore and retry: %s", err) + time.Sleep(2 * time.Second) + continue + } + return err } From a0d41fcd14f2525df351d1764e3401d1f016ff53 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 12 Jun 2015 00:16:43 -0400 Subject: [PATCH 05/12] builder/openstack --- builder/openstack-new/access_config.go | 109 ------------- builder/openstack-new/artifact.go | 47 ------ builder/openstack-new/artifact_test.go | 35 ----- builder/openstack-new/builder.go | 134 ---------------- builder/openstack-new/builder_test.go | 94 ------------ builder/openstack-new/image_config.go | 25 --- builder/openstack-new/image_config_test.go | 23 --- builder/openstack-new/run_config.go | 75 --------- builder/openstack-new/run_config_test.go | 88 ----------- builder/openstack-new/server.go | 95 ------------ builder/openstack-new/ssh.go | 92 ----------- builder/openstack-new/step_allocate_ip.go | 93 ------------ builder/openstack-new/step_create_image.go | 84 ---------- builder/openstack-new/step_key_pair.go | 106 ------------- .../openstack-new/step_run_source_server.go | 110 -------------- .../step_wait_for_rackconnect.go | 52 ------- builder/openstack/access_config.go | 143 +++++++++--------- builder/openstack/access_config_test.go | 77 ---------- builder/openstack/artifact.go | 7 +- builder/openstack/builder.go | 23 +-- builder/openstack/server.go | 23 +-- builder/openstack/ssh.go | 62 +++++--- builder/openstack/step_allocate_ip.go | 68 ++++++--- builder/openstack/step_create_image.go | 39 +++-- builder/openstack/step_key_pair.go | 43 ++++-- builder/openstack/step_run_source_server.go | 72 +++++---- .../openstack/step_wait_for_rackconnect.go | 22 ++- 27 files changed, 294 insertions(+), 1547 deletions(-) delete mode 100644 builder/openstack-new/access_config.go delete mode 100644 builder/openstack-new/artifact.go delete mode 100644 builder/openstack-new/artifact_test.go delete mode 100644 builder/openstack-new/builder.go delete mode 100644 builder/openstack-new/builder_test.go delete mode 100644 builder/openstack-new/image_config.go delete mode 100644 builder/openstack-new/image_config_test.go delete mode 100644 builder/openstack-new/run_config.go delete mode 100644 builder/openstack-new/run_config_test.go delete mode 100644 builder/openstack-new/server.go delete mode 100644 builder/openstack-new/ssh.go delete mode 100644 builder/openstack-new/step_allocate_ip.go delete mode 100644 builder/openstack-new/step_create_image.go delete mode 100644 builder/openstack-new/step_key_pair.go delete mode 100644 builder/openstack-new/step_run_source_server.go delete mode 100644 builder/openstack-new/step_wait_for_rackconnect.go delete mode 100644 builder/openstack/access_config_test.go diff --git a/builder/openstack-new/access_config.go b/builder/openstack-new/access_config.go deleted file mode 100644 index e0f962c50..000000000 --- a/builder/openstack-new/access_config.go +++ /dev/null @@ -1,109 +0,0 @@ -package openstack - -import ( - "crypto/tls" - "fmt" - "net/http" - "os" - - "github.com/mitchellh/packer/template/interpolate" - "github.com/rackspace/gophercloud" - "github.com/rackspace/gophercloud/openstack" -) - -// AccessConfig is for common configuration related to openstack access -type AccessConfig struct { - Username string `mapstructure:"username"` - UserID string `mapstructure:"user_id"` - Password string `mapstructure:"password"` - APIKey string `mapstructure:"api_key"` - IdentityEndpoint string `mapstructure:"identity_endpoint"` - TenantID string `mapstructure:"tenant_id"` - TenantName string `mapstructure:"tenant_name"` - DomainID string `mapstructure:"domain_id"` - DomainName string `mapstructure:"domain_name"` - Insecure bool `mapstructure:"insecure"` - Region string `mapstructure:"region"` - EndpointType string `mapstructure:"endpoint_type"` - - osClient *gophercloud.ProviderClient -} - -func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error { - if c.EndpointType != "internal" && c.EndpointType != "internalURL" && - c.EndpointType != "admin" && c.EndpointType != "adminURL" && - c.EndpointType != "public" && c.EndpointType != "publicURL" && - c.EndpointType != "" { - return []error{fmt.Errorf("Invalid endpoint type provided")} - } - - if c.Region == "" { - c.Region = os.Getenv("OS_REGION_NAME") - } - - // Get as much as possible from the end - ao, err := openstack.AuthOptionsFromEnv() - if err != nil { - return []error{err} - } - - // Override values if we have them in our config - overrides := []struct { - From, To *string - }{ - {&c.Username, &ao.Username}, - {&c.UserID, &ao.UserID}, - {&c.Password, &ao.Password}, - {&c.APIKey, &ao.APIKey}, - {&c.IdentityEndpoint, &ao.IdentityEndpoint}, - {&c.TenantID, &ao.TenantID}, - {&c.TenantName, &ao.TenantName}, - {&c.DomainID, &ao.DomainID}, - {&c.DomainName, &ao.DomainName}, - } - for _, s := range overrides { - if *s.From != "" { - *s.To = *s.From - } - } - - // Build the client itself - client, err := openstack.NewClient(ao.IdentityEndpoint) - if err != nil { - return []error{err} - } - - // If we have insecure set, then create a custom HTTP client that - // ignores SSL errors. - if c.Insecure { - config := &tls.Config{InsecureSkipVerify: true} - transport := &http.Transport{TLSClientConfig: config} - client.HTTPClient.Transport = transport - } - - // Auth - err = openstack.Authenticate(client, ao) - if err != nil { - return []error{err} - } - - c.osClient = client - return nil -} - -func (c *AccessConfig) computeV2Client() (*gophercloud.ServiceClient, error) { - return openstack.NewComputeV2(c.osClient, gophercloud.EndpointOpts{ - Region: c.Region, - Availability: c.getEndpointType(), - }) -} - -func (c *AccessConfig) getEndpointType() gophercloud.Availability { - if c.EndpointType == "internal" || c.EndpointType == "internalURL" { - return gophercloud.AvailabilityInternal - } - if c.EndpointType == "admin" || c.EndpointType == "adminURL" { - return gophercloud.AvailabilityAdmin - } - return gophercloud.AvailabilityPublic -} diff --git a/builder/openstack-new/artifact.go b/builder/openstack-new/artifact.go deleted file mode 100644 index aa60d2641..000000000 --- a/builder/openstack-new/artifact.go +++ /dev/null @@ -1,47 +0,0 @@ -package openstack - -import ( - "fmt" - "log" - - "github.com/rackspace/gophercloud" - "github.com/rackspace/gophercloud/openstack/compute/v2/images" -) - -// Artifact is an artifact implementation that contains built images. -type Artifact struct { - // ImageId of built image - ImageId string - - // BuilderId is the unique ID for the builder that created this image - BuilderIdValue string - - // OpenStack connection for performing API stuff. - Client *gophercloud.ServiceClient -} - -func (a *Artifact) BuilderId() string { - return a.BuilderIdValue -} - -func (*Artifact) Files() []string { - // We have no files - return nil -} - -func (a *Artifact) Id() string { - return a.ImageId -} - -func (a *Artifact) String() string { - return fmt.Sprintf("An image was created: %v", a.ImageId) -} - -func (a *Artifact) State(name string) interface{} { - return nil -} - -func (a *Artifact) Destroy() error { - log.Printf("Destroying image: %s", a.ImageId) - return images.Delete(a.Client, a.ImageId).ExtractErr() -} diff --git a/builder/openstack-new/artifact_test.go b/builder/openstack-new/artifact_test.go deleted file mode 100644 index 313fea7cf..000000000 --- a/builder/openstack-new/artifact_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package openstack - -import ( - "github.com/mitchellh/packer/packer" - "testing" -) - -func TestArtifact_Impl(t *testing.T) { - var _ packer.Artifact = new(Artifact) -} - -func TestArtifactId(t *testing.T) { - expected := `b8cdf55b-c916-40bd-b190-389ec144c4ed` - - a := &Artifact{ - ImageId: "b8cdf55b-c916-40bd-b190-389ec144c4ed", - } - - result := a.Id() - if result != expected { - t.Fatalf("bad: %s", result) - } -} - -func TestArtifactString(t *testing.T) { - expected := "An image was created: b8cdf55b-c916-40bd-b190-389ec144c4ed" - - a := &Artifact{ - ImageId: "b8cdf55b-c916-40bd-b190-389ec144c4ed", - } - result := a.String() - if result != expected { - t.Fatalf("bad: %s", result) - } -} diff --git a/builder/openstack-new/builder.go b/builder/openstack-new/builder.go deleted file mode 100644 index bebb28452..000000000 --- a/builder/openstack-new/builder.go +++ /dev/null @@ -1,134 +0,0 @@ -// 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" - "log" - - "github.com/mitchellh/packer/helper/config" - "github.com/mitchellh/packer/packer" - "github.com/mitchellh/packer/template/interpolate" -) - -// 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"` - - ctx interpolate.Context -} - -type Builder struct { - config Config - runner multistep.Runner -} - -func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { - err := config.Decode(&b.config, &config.DecodeOpts{ - Interpolate: true, - }, raws...) - if err != nil { - return nil, err - } - - // Accumulate any errors - var errs *packer.MultiError - errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(&b.config.ctx)...) - errs = packer.MultiErrorAppend(errs, b.config.ImageConfig.Prepare(&b.config.ctx)...) - errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare(&b.config.ctx)...) - - if errs != nil && len(errs.Errors) > 0 { - return nil, errs - } - - log.Println(common.ScrubConfig(b.config, b.config.Password)) - return nil, nil -} - -func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { - computeClient, err := b.config.computeV2Client() - if err != nil { - return nil, fmt.Errorf("Error initializing compute client: %s", err) - } - - // Setup the state bag and initial state for the steps - state := new(multistep.BasicStateBag) - state.Put("config", b.config) - state.Put("hook", hook) - state.Put("ui", ui) - - // Build the steps - steps := []multistep.Step{ - &StepKeyPair{ - Debug: b.config.PackerDebug, - DebugKeyPath: fmt.Sprintf("os_%s.pem", b.config.PackerBuildName), - }, - &StepRunSourceServer{ - Name: b.config.ImageName, - Flavor: b.config.Flavor, - SourceImage: b.config.SourceImage, - SecurityGroups: b.config.SecurityGroups, - Networks: b.config.Networks, - }, - &StepWaitForRackConnect{ - Wait: b.config.RackconnectWait, - }, - &StepAllocateIp{ - FloatingIpPool: b.config.FloatingIpPool, - FloatingIp: b.config.FloatingIp, - }, - &common.StepConnectSSH{ - SSHAddress: SSHAddress(computeClient, b.config.SSHInterface, 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.GetOk("error"); ok { - return nil, rawErr.(error) - } - - // If there are no images, then just return - if _, ok := state.GetOk("image"); !ok { - return nil, nil - } - - // Build the artifact and return it - artifact := &Artifact{ - ImageId: state.Get("image").(string), - BuilderIdValue: BuilderId, - Client: computeClient, - } - - return artifact, nil -} - -func (b *Builder) Cancel() { - if b.runner != nil { - log.Println("Cancelling the step runner...") - b.runner.Cancel() - } -} diff --git a/builder/openstack-new/builder_test.go b/builder/openstack-new/builder_test.go deleted file mode 100644 index badf9784d..000000000 --- a/builder/openstack-new/builder_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package openstack - -import ( - "github.com/mitchellh/packer/packer" - "testing" -) - -func testConfig() map[string]interface{} { - return map[string]interface{}{ - "username": "foo", - "password": "bar", - "provider": "foo", - "region": "DFW", - "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{}, - } - - warns, err := b.Prepare(c) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - 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" - warns, err := b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err != nil { - t.Fatalf("should not have error: %s", err) - } - - // Test bad - config["image_name"] = "foo {{" - b = Builder{} - warns, err = b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err == nil { - t.Fatal("should have error") - } - - // Test bad - delete(config, "image_name") - b = Builder{} - warns, err = b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - 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 - warns, err := b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err == nil { - t.Fatal("should have error") - } -} diff --git a/builder/openstack-new/image_config.go b/builder/openstack-new/image_config.go deleted file mode 100644 index 124449eab..000000000 --- a/builder/openstack-new/image_config.go +++ /dev/null @@ -1,25 +0,0 @@ -package openstack - -import ( - "fmt" - - "github.com/mitchellh/packer/template/interpolate" -) - -// ImageConfig is for common configuration related to creating Images. -type ImageConfig struct { - ImageName string `mapstructure:"image_name"` -} - -func (c *ImageConfig) Prepare(ctx *interpolate.Context) []error { - errs := make([]error, 0) - 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-new/image_config_test.go b/builder/openstack-new/image_config_test.go deleted file mode 100644 index 4d81ecd94..000000000 --- a/builder/openstack-new/image_config_test.go +++ /dev/null @@ -1,23 +0,0 @@ -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-new/run_config.go b/builder/openstack-new/run_config.go deleted file mode 100644 index e5d73c9c1..000000000 --- a/builder/openstack-new/run_config.go +++ /dev/null @@ -1,75 +0,0 @@ -package openstack - -import ( - "errors" - "fmt" - "time" - - "github.com/mitchellh/packer/template/interpolate" -) - -// 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"` - SSHInterface string `mapstructure:"ssh_interface"` - OpenstackProvider string `mapstructure:"openstack_provider"` - UseFloatingIp bool `mapstructure:"use_floating_ip"` - RackconnectWait bool `mapstructure:"rackconnect_wait"` - FloatingIpPool string `mapstructure:"floating_ip_pool"` - FloatingIp string `mapstructure:"floating_ip"` - SecurityGroups []string `mapstructure:"security_groups"` - Networks []string `mapstructure:"networks"` - - // Unexported fields that are calculated from others - sshTimeout time.Duration -} - -func (c *RunConfig) Prepare(ctx *interpolate.Context) []error { - // Defaults - if c.SSHUsername == "" { - c.SSHUsername = "root" - } - - if c.SSHPort == 0 { - c.SSHPort = 22 - } - - if c.RawSSHTimeout == "" { - c.RawSSHTimeout = "5m" - } - - if c.UseFloatingIp && c.FloatingIpPool == "" { - c.FloatingIpPool = "public" - } - - // 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")) - } - - 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-new/run_config_test.go b/builder/openstack-new/run_config_test.go deleted file mode 100644 index 16b89b352..000000000 --- a/builder/openstack-new/run_config_test.go +++ /dev/null @@ -1,88 +0,0 @@ -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-new/server.go b/builder/openstack-new/server.go deleted file mode 100644 index de8c9d103..000000000 --- a/builder/openstack-new/server.go +++ /dev/null @@ -1,95 +0,0 @@ -package openstack - -import ( - "errors" - "fmt" - "log" - "time" - - "github.com/mitchellh/multistep" - "github.com/rackspace/gophercloud" - "github.com/rackspace/gophercloud/openstack/compute/v2/servers" -) - -// 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 { - Pending []string - Refresh StateRefreshFunc - StepState multistep.StateBag - Target string -} - -// ServerStateRefreshFunc returns a StateRefreshFunc that is used to watch -// an openstack server. -func ServerStateRefreshFunc( - client *gophercloud.ServiceClient, s *servers.Server) StateRefreshFunc { - return func() (interface{}, string, int, error) { - serverNew, err := servers.Get(client, s.ID).Extract() - if err != nil { - errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError) - if ok && errCode.Actual == 404 { - log.Printf("[INFO] 404 on ServerStateRefresh, returning DELETED") - return nil, "DELETED", 0, nil - } else { - log.Printf("[ERROR] Error on ServerStateRefresh: %s", err) - return nil, "", 0, err - } - } - - return serverNew, serverNew.Status, serverNew.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.GetOk(multistep.StateCancelled); ok { - return nil, errors.New("interrupted") - } - } - - found := false - for _, allowed := range conf.Pending { - if currentState == allowed { - found = true - break - } - } - - if !found { - return nil, fmt.Errorf("unexpected state '%s', wanted target '%s'", currentState, conf.Target) - } - - 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-new/ssh.go b/builder/openstack-new/ssh.go deleted file mode 100644 index 7b0510f98..000000000 --- a/builder/openstack-new/ssh.go +++ /dev/null @@ -1,92 +0,0 @@ -package openstack - -import ( - "errors" - "fmt" - "log" - "time" - - "github.com/mitchellh/multistep" - "github.com/rackspace/gophercloud" - "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip" - "github.com/rackspace/gophercloud/openstack/compute/v2/servers" - "golang.org/x/crypto/ssh" -) - -// 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( - client *gophercloud.ServiceClient, - sshinterface string, port int) func(multistep.StateBag) (string, error) { - return func(state multistep.StateBag) (string, error) { - s := state.Get("server").(*servers.Server) - - // If we have a floating IP, use that - ip := state.Get("access_ip").(*floatingip.FloatingIP) - if ip != nil && ip.FixedIP != "" { - return fmt.Sprintf("%s:%d", ip.FixedIP, port), nil - } - - if s.AccessIPv4 != "" { - return fmt.Sprintf("%s:%d", s.AccessIPv4, port), nil - } - - // Get all the addresses associated with this server. This - // was taken directly from Terraform. - for _, networkAddresses := range s.Addresses { - elements, ok := networkAddresses.([]interface{}) - if !ok { - log.Printf( - "[ERROR] Unknown return type for address field: %#v", - networkAddresses) - continue - } - - for _, element := range elements { - var addr string - address := element.(map[string]interface{}) - if address["OS-EXT-IPS:type"] == "floating" { - addr = address["addr"].(string) - } else { - if address["version"].(float64) == 4 { - addr = address["addr"].(string) - } - } - if addr != "" { - return fmt.Sprintf("%s:%d", addr, port), nil - } - } - } - - s, err := servers.Get(client, s.ID).Extract() - if err != nil { - return "", err - } - - state.Put("server", s) - 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(multistep.StateBag) (*ssh.ClientConfig, error) { - return func(state multistep.StateBag) (*ssh.ClientConfig, error) { - privateKey := state.Get("privateKey").(string) - - signer, err := ssh.ParsePrivateKey([]byte(privateKey)) - if err != nil { - return nil, fmt.Errorf("Error setting up SSH config: %s", err) - } - - return &ssh.ClientConfig{ - User: username, - Auth: []ssh.AuthMethod{ - ssh.PublicKeys(signer), - }, - }, nil - } -} diff --git a/builder/openstack-new/step_allocate_ip.go b/builder/openstack-new/step_allocate_ip.go deleted file mode 100644 index 16efe8d38..000000000 --- a/builder/openstack-new/step_allocate_ip.go +++ /dev/null @@ -1,93 +0,0 @@ -package openstack - -import ( - "fmt" - - "github.com/mitchellh/multistep" - "github.com/mitchellh/packer/packer" - "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip" - "github.com/rackspace/gophercloud/openstack/compute/v2/servers" -) - -type StepAllocateIp struct { - FloatingIpPool string - FloatingIp string -} - -func (s *StepAllocateIp) Run(state multistep.StateBag) multistep.StepAction { - ui := state.Get("ui").(packer.Ui) - config := state.Get("config").(Config) - server := state.Get("server").(*servers.Server) - - // We need the v2 compute client - client, err := config.computeV2Client() - if err != nil { - err = fmt.Errorf("Error initializing compute client: %s", err) - state.Put("error", err) - return multistep.ActionHalt - } - - var instanceIp *floatingip.FloatingIP - // This is here in case we error out before putting instanceIp into the - // statebag below, because it is requested by Cleanup() - state.Put("access_ip", instanceIp) - - if s.FloatingIp != "" { - *instanceIp = floatingip.FloatingIP{FixedIP: s.FloatingIp} - } else if s.FloatingIpPool != "" { - newIp, err := floatingip.Create(client, floatingip.CreateOpts{ - Pool: s.FloatingIpPool, - }).Extract() - if err != nil { - err := fmt.Errorf("Error creating floating ip from pool '%s'", s.FloatingIpPool) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } - - *instanceIp = *newIp - ui.Say(fmt.Sprintf("Created temporary floating IP %s...", instanceIp.FixedIP)) - } - - if instanceIp != nil && instanceIp.FixedIP != "" { - err := floatingip.Associate(client, server.ID, instanceIp.FixedIP).ExtractErr() - if err != nil { - err := fmt.Errorf( - "Error associating floating IP %s with instance.", - instanceIp.FixedIP) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } - - ui.Say(fmt.Sprintf( - "Added floating IP %s to instance...", instanceIp.FixedIP)) - } - - state.Put("access_ip", instanceIp) - return multistep.ActionContinue -} - -func (s *StepAllocateIp) Cleanup(state multistep.StateBag) { - config := state.Get("config").(Config) - ui := state.Get("ui").(packer.Ui) - instanceIp := state.Get("access_ip").(*floatingip.FloatingIP) - - // We need the v2 compute client - client, err := config.computeV2Client() - if err != nil { - ui.Error(fmt.Sprintf( - "Error deleting temporary floating IP %s", instanceIp.FixedIP)) - return - } - - if s.FloatingIpPool != "" && instanceIp.ID != "" { - if err := floatingip.Delete(client, instanceIp.ID).ExtractErr(); err != nil { - ui.Error(fmt.Sprintf( - "Error deleting temporary floating IP %s", instanceIp.FixedIP)) - return - } - - ui.Say(fmt.Sprintf("Deleted temporary floating IP %s", instanceIp.FixedIP)) - } -} diff --git a/builder/openstack-new/step_create_image.go b/builder/openstack-new/step_create_image.go deleted file mode 100644 index b777e8b0b..000000000 --- a/builder/openstack-new/step_create_image.go +++ /dev/null @@ -1,84 +0,0 @@ -package openstack - -import ( - "fmt" - "log" - "time" - - "github.com/mitchellh/multistep" - "github.com/mitchellh/packer/packer" - "github.com/rackspace/gophercloud" - "github.com/rackspace/gophercloud/openstack/compute/v2/images" - "github.com/rackspace/gophercloud/openstack/compute/v2/servers" -) - -type stepCreateImage struct{} - -func (s *stepCreateImage) Run(state multistep.StateBag) multistep.StepAction { - config := state.Get("config").(Config) - server := state.Get("server").(*servers.Server) - ui := state.Get("ui").(packer.Ui) - - // We need the v2 compute client - client, err := config.computeV2Client() - if err != nil { - err = fmt.Errorf("Error initializing compute client: %s", err) - state.Put("error", err) - return multistep.ActionHalt - } - - // Create the image - ui.Say(fmt.Sprintf("Creating the image: %s", config.ImageName)) - imageId, err := servers.CreateImage(client, server.ID, servers.CreateImageOpts{ - Name: config.ImageName, - }).ExtractImageID() - if err != nil { - err := fmt.Errorf("Error creating image: %s", err) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } - - // Set the Image ID in the state - ui.Message(fmt.Sprintf("Image: %s", imageId)) - state.Put("image", imageId) - - // Wait for the image to become ready - ui.Say("Waiting for image to become ready...") - if err := WaitForImage(client, imageId); err != nil { - err := fmt.Errorf("Error waiting for image: %s", err) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } - - return multistep.ActionContinue -} - -func (s *stepCreateImage) Cleanup(multistep.StateBag) { - // No cleanup... -} - -// WaitForImage waits for the given Image ID to become ready. -func WaitForImage(client *gophercloud.ServiceClient, imageId string) error { - for { - image, err := images.Get(client, imageId).Extract() - if err != nil { - errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError) - if ok && errCode.Actual == 500 { - log.Printf("[ERROR] 500 error received, will ignore and retry: %s", err) - time.Sleep(2 * time.Second) - continue - } - - 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-new/step_key_pair.go b/builder/openstack-new/step_key_pair.go deleted file mode 100644 index 06bcbf9ea..000000000 --- a/builder/openstack-new/step_key_pair.go +++ /dev/null @@ -1,106 +0,0 @@ -package openstack - -import ( - "fmt" - "os" - "runtime" - - "github.com/mitchellh/multistep" - "github.com/mitchellh/packer/common/uuid" - "github.com/mitchellh/packer/packer" - "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" -) - -type StepKeyPair struct { - Debug bool - DebugKeyPath string - keyName string -} - -func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction { - config := state.Get("config").(Config) - ui := state.Get("ui").(packer.Ui) - - // We need the v2 compute client - computeClient, err := config.computeV2Client() - if err != nil { - err = fmt.Errorf("Error initializing compute client: %s", err) - state.Put("error", err) - return multistep.ActionHalt - } - - ui.Say("Creating temporary keypair for this instance...") - keyName := fmt.Sprintf("packer %s", uuid.TimeOrderedUUID()) - keypair, err := keypairs.Create(computeClient, keypairs.CreateOpts{ - Name: keyName, - }).Extract() - if err != nil { - state.Put("error", fmt.Errorf("Error creating temporary keypair: %s", err)) - return multistep.ActionHalt - } - - if keypair.PrivateKey == "" { - state.Put("error", fmt.Errorf("The temporary keypair returned was blank")) - return multistep.ActionHalt - } - - // If we're in debug mode, output the private key to the working - // directory. - if s.Debug { - ui.Message(fmt.Sprintf("Saving key for debug purposes: %s", s.DebugKeyPath)) - f, err := os.Create(s.DebugKeyPath) - if err != nil { - state.Put("error", fmt.Errorf("Error saving debug key: %s", err)) - return multistep.ActionHalt - } - defer f.Close() - - // Write the key out - if _, err := f.Write([]byte(keypair.PrivateKey)); err != nil { - state.Put("error", fmt.Errorf("Error saving debug key: %s", err)) - return multistep.ActionHalt - } - - // Chmod it so that it is SSH ready - if runtime.GOOS != "windows" { - if err := f.Chmod(0600); err != nil { - state.Put("error", fmt.Errorf("Error setting permissions of debug key: %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.Put("keyPair", keyName) - state.Put("privateKey", keypair.PrivateKey) - - return multistep.ActionContinue -} - -func (s *StepKeyPair) Cleanup(state multistep.StateBag) { - // If no key name is set, then we never created it, so just return - if s.keyName == "" { - return - } - - config := state.Get("config").(Config) - ui := state.Get("ui").(packer.Ui) - - // We need the v2 compute client - computeClient, err := config.computeV2Client() - if err != nil { - ui.Error(fmt.Sprintf( - "Error cleaning up keypair. Please delete the key manually: %s", s.keyName)) - return - } - - ui.Say("Deleting temporary keypair...") - err = keypairs.Delete(computeClient, s.keyName).ExtractErr() - if err != nil { - ui.Error(fmt.Sprintf( - "Error cleaning up keypair. Please delete the key manually: %s", s.keyName)) - } -} diff --git a/builder/openstack-new/step_run_source_server.go b/builder/openstack-new/step_run_source_server.go deleted file mode 100644 index 4432d5860..000000000 --- a/builder/openstack-new/step_run_source_server.go +++ /dev/null @@ -1,110 +0,0 @@ -package openstack - -import ( - "fmt" - "log" - - "github.com/mitchellh/multistep" - "github.com/mitchellh/packer/packer" - "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" - "github.com/rackspace/gophercloud/openstack/compute/v2/servers" -) - -type StepRunSourceServer struct { - Flavor string - Name string - SourceImage string - SecurityGroups []string - Networks []string - - server *servers.Server -} - -func (s *StepRunSourceServer) Run(state multistep.StateBag) multistep.StepAction { - config := state.Get("config").(Config) - keyName := state.Get("keyPair").(string) - ui := state.Get("ui").(packer.Ui) - - // We need the v2 compute client - computeClient, err := config.computeV2Client() - if err != nil { - err = fmt.Errorf("Error initializing compute client: %s", err) - state.Put("error", err) - return multistep.ActionHalt - } - - networks := make([]servers.Network, len(s.Networks)) - for i, networkUuid := range s.Networks { - networks[i].UUID = networkUuid - } - - s.server, err = servers.Create(computeClient, keypairs.CreateOptsExt{ - CreateOptsBuilder: servers.CreateOpts{ - Name: s.Name, - ImageRef: s.SourceImage, - FlavorName: s.Flavor, - SecurityGroups: s.SecurityGroups, - Networks: networks, - }, - - KeyName: keyName, - }).Extract() - if err != nil { - err := fmt.Errorf("Error launching source server: %s", err) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } - - log.Printf("server id: %s", s.server.ID) - - ui.Say(fmt.Sprintf("Waiting for server (%s) to become ready...", s.server.ID)) - stateChange := StateChangeConf{ - Pending: []string{"BUILD"}, - Target: "ACTIVE", - Refresh: ServerStateRefreshFunc(computeClient, 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.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } - - s.server = latestServer.(*servers.Server) - state.Put("server", s.server) - - return multistep.ActionContinue -} - -func (s *StepRunSourceServer) Cleanup(state multistep.StateBag) { - if s.server == nil { - return - } - - config := state.Get("config").(Config) - ui := state.Get("ui").(packer.Ui) - - // We need the v2 compute client - computeClient, err := config.computeV2Client() - if err != nil { - ui.Error(fmt.Sprintf("Error terminating server, may still be around: %s", err)) - return - } - - ui.Say("Terminating the source server...") - if err := servers.Delete(computeClient, s.server.ID).ExtractErr(); err != nil { - ui.Error(fmt.Sprintf("Error terminating server, may still be around: %s", err)) - return - } - - stateChange := StateChangeConf{ - Pending: []string{"ACTIVE", "BUILD", "REBUILD", "SUSPENDED"}, - Refresh: ServerStateRefreshFunc(computeClient, s.server), - Target: "DELETED", - } - - WaitForState(&stateChange) -} diff --git a/builder/openstack-new/step_wait_for_rackconnect.go b/builder/openstack-new/step_wait_for_rackconnect.go deleted file mode 100644 index 6263bd17d..000000000 --- a/builder/openstack-new/step_wait_for_rackconnect.go +++ /dev/null @@ -1,52 +0,0 @@ -package openstack - -import ( - "fmt" - "time" - - "github.com/mitchellh/multistep" - "github.com/mitchellh/packer/packer" - "github.com/rackspace/gophercloud/openstack/compute/v2/servers" -) - -type StepWaitForRackConnect struct { - Wait bool -} - -func (s *StepWaitForRackConnect) Run(state multistep.StateBag) multistep.StepAction { - if !s.Wait { - return multistep.ActionContinue - } - - config := state.Get("config").(Config) - server := state.Get("server").(*servers.Server) - ui := state.Get("ui").(packer.Ui) - - // We need the v2 compute client - computeClient, err := config.computeV2Client() - if err != nil { - err = fmt.Errorf("Error initializing compute client: %s", err) - state.Put("error", err) - return multistep.ActionHalt - } - - ui.Say(fmt.Sprintf( - "Waiting for server (%s) to become RackConnect ready...", server.ID)) - for { - server, err = servers.Get(computeClient, server.ID).Extract() - if err != nil { - return multistep.ActionHalt - } - - if server.Metadata["rackconnect_automation_status"] == "DEPLOYED" { - break - } - - time.Sleep(2 * time.Second) - } - - return multistep.ActionContinue -} - -func (s *StepWaitForRackConnect) Cleanup(state multistep.StateBag) { -} diff --git a/builder/openstack/access_config.go b/builder/openstack/access_config.go index cb1c9d7bd..e0f962c50 100644 --- a/builder/openstack/access_config.go +++ b/builder/openstack/access_config.go @@ -4,99 +4,106 @@ import ( "crypto/tls" "fmt" "net/http" - "net/url" "os" - "strings" - "github.com/mitchellh/gophercloud-fork-40444fb" - "github.com/mitchellh/packer/common" "github.com/mitchellh/packer/template/interpolate" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" ) // AccessConfig is for common configuration related to openstack access type AccessConfig struct { - Username string `mapstructure:"username"` - Password string `mapstructure:"password"` - ApiKey string `mapstructure:"api_key"` - Project string `mapstructure:"project"` - Provider string `mapstructure:"provider"` - RawRegion string `mapstructure:"region"` - ProxyUrl string `mapstructure:"proxy_url"` - TenantId string `mapstructure:"tenant_id"` - Insecure bool `mapstructure:"insecure"` + Username string `mapstructure:"username"` + UserID string `mapstructure:"user_id"` + Password string `mapstructure:"password"` + APIKey string `mapstructure:"api_key"` + IdentityEndpoint string `mapstructure:"identity_endpoint"` + TenantID string `mapstructure:"tenant_id"` + TenantName string `mapstructure:"tenant_name"` + DomainID string `mapstructure:"domain_id"` + DomainName string `mapstructure:"domain_name"` + Insecure bool `mapstructure:"insecure"` + Region string `mapstructure:"region"` + EndpointType string `mapstructure:"endpoint_type"` + + osClient *gophercloud.ProviderClient } -// 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) { - c.Username = common.ChooseString(c.Username, os.Getenv("SDK_USERNAME"), os.Getenv("OS_USERNAME")) - c.Password = common.ChooseString(c.Password, os.Getenv("SDK_PASSWORD"), os.Getenv("OS_PASSWORD")) - c.ApiKey = common.ChooseString(c.ApiKey, os.Getenv("SDK_API_KEY")) - c.Project = common.ChooseString(c.Project, os.Getenv("SDK_PROJECT"), os.Getenv("OS_TENANT_NAME")) - c.Provider = common.ChooseString(c.Provider, os.Getenv("SDK_PROVIDER"), os.Getenv("OS_AUTH_URL")) - c.RawRegion = common.ChooseString(c.RawRegion, os.Getenv("SDK_REGION"), os.Getenv("OS_REGION_NAME")) - c.TenantId = common.ChooseString(c.TenantId, os.Getenv("OS_TENANT_ID")) - - // OpenStack's auto-generated openrc.sh files do not append the suffix - // /tokens to the authentication URL. This ensures it is present when - // specifying the URL. - if strings.Contains(c.Provider, "://") && !strings.HasSuffix(c.Provider, "/tokens") { - c.Provider += "/tokens" +func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error { + if c.EndpointType != "internal" && c.EndpointType != "internalURL" && + c.EndpointType != "admin" && c.EndpointType != "adminURL" && + c.EndpointType != "public" && c.EndpointType != "publicURL" && + c.EndpointType != "" { + return []error{fmt.Errorf("Invalid endpoint type provided")} } - authoptions := gophercloud.AuthOptions{ - AllowReauth: true, - - ApiKey: c.ApiKey, - TenantId: c.TenantId, - TenantName: c.Project, - Username: c.Username, - Password: c.Password, + if c.Region == "" { + c.Region = os.Getenv("OS_REGION_NAME") } - default_transport := &http.Transport{} - - if c.Insecure { - cfg := new(tls.Config) - cfg.InsecureSkipVerify = true - default_transport.TLSClientConfig = cfg + // Get as much as possible from the end + ao, err := openstack.AuthOptionsFromEnv() + if err != nil { + return []error{err} } - // For corporate networks it may be the case where we want our API calls - // to be sent through a separate HTTP proxy than external traffic. - if c.ProxyUrl != "" { - url, err := url.Parse(c.ProxyUrl) - if err != nil { - return nil, err + // Override values if we have them in our config + overrides := []struct { + From, To *string + }{ + {&c.Username, &ao.Username}, + {&c.UserID, &ao.UserID}, + {&c.Password, &ao.Password}, + {&c.APIKey, &ao.APIKey}, + {&c.IdentityEndpoint, &ao.IdentityEndpoint}, + {&c.TenantID, &ao.TenantID}, + {&c.TenantName, &ao.TenantName}, + {&c.DomainID, &ao.DomainID}, + {&c.DomainName, &ao.DomainName}, + } + for _, s := range overrides { + if *s.From != "" { + *s.To = *s.From } + } + + // Build the client itself + client, err := openstack.NewClient(ao.IdentityEndpoint) + if err != nil { + return []error{err} + } - // The gophercloud.Context has a UseCustomClient method which - // would allow us to override with a new instance of http.Client. - default_transport.Proxy = http.ProxyURL(url) + // If we have insecure set, then create a custom HTTP client that + // ignores SSL errors. + if c.Insecure { + config := &tls.Config{InsecureSkipVerify: true} + transport := &http.Transport{TLSClientConfig: config} + client.HTTPClient.Transport = transport } - if c.Insecure || c.ProxyUrl != "" { - http.DefaultTransport = default_transport + // Auth + err = openstack.Authenticate(client, ao) + if err != nil { + return []error{err} } - return gophercloud.Authenticate(c.Provider, authoptions) + c.osClient = client + return nil } -func (c *AccessConfig) Region() string { - return common.ChooseString(c.RawRegion, os.Getenv("SDK_REGION"), os.Getenv("OS_REGION_NAME")) +func (c *AccessConfig) computeV2Client() (*gophercloud.ServiceClient, error) { + return openstack.NewComputeV2(c.osClient, gophercloud.EndpointOpts{ + Region: c.Region, + Availability: c.getEndpointType(), + }) } -func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error { - errs := make([]error, 0) - if strings.HasPrefix(c.Provider, "rackspace") { - if c.Region() == "" { - errs = append(errs, fmt.Errorf("region must be specified when using rackspace")) - } +func (c *AccessConfig) getEndpointType() gophercloud.Availability { + if c.EndpointType == "internal" || c.EndpointType == "internalURL" { + return gophercloud.AvailabilityInternal } - - if len(errs) > 0 { - return errs + if c.EndpointType == "admin" || c.EndpointType == "adminURL" { + return gophercloud.AvailabilityAdmin } - - return nil + return gophercloud.AvailabilityPublic } diff --git a/builder/openstack/access_config_test.go b/builder/openstack/access_config_test.go deleted file mode 100644 index cf37448cc..000000000 --- a/builder/openstack/access_config_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package openstack - -import ( - "os" - "testing" -) - -func init() { - // Clear out the openstack env vars so they don't - // affect our tests. - os.Setenv("SDK_REGION", "") - os.Setenv("OS_REGION_NAME", "") -} - -func testAccessConfig() *AccessConfig { - return &AccessConfig{} -} - -func TestAccessConfigPrepare_NoRegion_Rackspace(t *testing.T) { - c := testAccessConfig() - c.Provider = "rackspace-us" - if err := c.Prepare(nil); err == nil { - t.Fatalf("shouldn't have err: %s", err) - } -} - -func TestAccessConfigRegionWithEmptyEnv(t *testing.T) { - c := testAccessConfig() - c.Prepare(nil) - if c.Region() != "" { - t.Fatalf("Region should be empty") - } -} - -func TestAccessConfigRegionWithSdkRegionEnv(t *testing.T) { - c := testAccessConfig() - c.Prepare(nil) - - expectedRegion := "sdk_region" - os.Setenv("SDK_REGION", expectedRegion) - os.Setenv("OS_REGION_NAME", "") - if c.Region() != expectedRegion { - t.Fatalf("Region should be: %s", expectedRegion) - } -} - -func TestAccessConfigRegionWithOsRegionNameEnv(t *testing.T) { - c := testAccessConfig() - c.Prepare(nil) - - expectedRegion := "os_region_name" - os.Setenv("SDK_REGION", "") - os.Setenv("OS_REGION_NAME", expectedRegion) - if c.Region() != expectedRegion { - t.Fatalf("Region should be: %s", expectedRegion) - } -} - -func TestAccessConfigPrepare_NoRegion_PrivateCloud(t *testing.T) { - c := testAccessConfig() - c.Provider = "http://some-keystone-server:5000/v2.0" - if err := c.Prepare(nil); err != nil { - t.Fatalf("shouldn't have err: %s", err) - } -} - -func TestAccessConfigPrepare_Region(t *testing.T) { - dfw := "DFW" - c := testAccessConfig() - c.RawRegion = dfw - if err := c.Prepare(nil); err != nil { - t.Fatalf("shouldn't have err: %s", err) - } - if dfw != c.Region() { - t.Fatalf("Regions do not match: %s %s", dfw, c.Region()) - } -} diff --git a/builder/openstack/artifact.go b/builder/openstack/artifact.go index 6e75fad3e..aa60d2641 100644 --- a/builder/openstack/artifact.go +++ b/builder/openstack/artifact.go @@ -4,7 +4,8 @@ import ( "fmt" "log" - "github.com/mitchellh/gophercloud-fork-40444fb" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/compute/v2/images" ) // Artifact is an artifact implementation that contains built images. @@ -16,7 +17,7 @@ type Artifact struct { BuilderIdValue string // OpenStack connection for performing API stuff. - Conn gophercloud.CloudServersProvider + Client *gophercloud.ServiceClient } func (a *Artifact) BuilderId() string { @@ -42,5 +43,5 @@ func (a *Artifact) State(name string) interface{} { func (a *Artifact) Destroy() error { log.Printf("Destroying image: %s", a.ImageId) - return a.Conn.DeleteImageById(a.ImageId) + return images.Delete(a.Client, a.ImageId).ExtractErr() } diff --git a/builder/openstack/builder.go b/builder/openstack/builder.go index e6e5c6675..bebb28452 100644 --- a/builder/openstack/builder.go +++ b/builder/openstack/builder.go @@ -9,7 +9,6 @@ import ( "github.com/mitchellh/packer/common" "log" - "github.com/mitchellh/gophercloud-fork-40444fb" "github.com/mitchellh/packer/helper/config" "github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/template/interpolate" @@ -55,28 +54,14 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { } func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { - auth, err := b.config.AccessConfig.Auth() + computeClient, err := b.config.computeV2Client() if err != nil { - return nil, err - } - //fetches the api requisites from gophercloud for the appropriate - //openstack variant - api, err := gophercloud.PopulateApi(b.config.RunConfig.OpenstackProvider) - if err != nil { - return nil, err - } - api.Region = b.config.AccessConfig.Region() - - csp, err := gophercloud.ServersApi(auth, api) - if err != nil { - log.Printf("Region: %s", b.config.AccessConfig.Region()) - return nil, err + return nil, fmt.Errorf("Error initializing compute client: %s", err) } // Setup the state bag and initial state for the steps state := new(multistep.BasicStateBag) state.Put("config", b.config) - state.Put("csp", csp) state.Put("hook", hook) state.Put("ui", ui) @@ -101,7 +86,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe FloatingIp: b.config.FloatingIp, }, &common.StepConnectSSH{ - SSHAddress: SSHAddress(csp, b.config.SSHInterface, b.config.SSHPort), + SSHAddress: SSHAddress(computeClient, b.config.SSHInterface, b.config.SSHPort), SSHConfig: SSHConfig(b.config.SSHUsername), SSHWaitTimeout: b.config.SSHTimeout(), }, @@ -135,7 +120,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe artifact := &Artifact{ ImageId: state.Get("image").(string), BuilderIdValue: BuilderId, - Conn: csp, + Client: computeClient, } return artifact, nil diff --git a/builder/openstack/server.go b/builder/openstack/server.go index ba22dd3e2..de8c9d103 100644 --- a/builder/openstack/server.go +++ b/builder/openstack/server.go @@ -3,12 +3,12 @@ package openstack import ( "errors" "fmt" - "github.com/mitchellh/multistep" - "github.com/racker/perigee" "log" "time" - "github.com/mitchellh/gophercloud-fork-40444fb" + "github.com/mitchellh/multistep" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" ) // StateRefreshFunc is a function type used for StateChangeConf that is @@ -33,21 +33,22 @@ type StateChangeConf struct { // ServerStateRefreshFunc returns a StateRefreshFunc that is used to watch // an openstack server. -func ServerStateRefreshFunc(csp gophercloud.CloudServersProvider, s *gophercloud.Server) StateRefreshFunc { +func ServerStateRefreshFunc( + client *gophercloud.ServiceClient, s *servers.Server) StateRefreshFunc { return func() (interface{}, string, int, error) { - resp, err := csp.ServerById(s.Id) + serverNew, err := servers.Get(client, s.ID).Extract() if err != nil { - urce, ok := err.(*perigee.UnexpectedResponseCodeError) - if ok && (urce.Actual == 404) { - log.Printf("404 on ServerStateRefresh, returning DELETED") - + errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if ok && errCode.Actual == 404 { + log.Printf("[INFO] 404 on ServerStateRefresh, returning DELETED") return nil, "DELETED", 0, nil } else { - log.Printf("Error on ServerStateRefresh: %s", err) + log.Printf("[ERROR] Error on ServerStateRefresh: %s", err) return nil, "", 0, err } } - return resp, resp.Status, resp.Progress, nil + + return serverNew, serverNew.Status, serverNew.Progress, nil } } diff --git a/builder/openstack/ssh.go b/builder/openstack/ssh.go index d20f24170..7b0510f98 100644 --- a/builder/openstack/ssh.go +++ b/builder/openstack/ssh.go @@ -3,49 +3,67 @@ package openstack import ( "errors" "fmt" - "github.com/mitchellh/multistep" - "golang.org/x/crypto/ssh" + "log" "time" - "github.com/mitchellh/gophercloud-fork-40444fb" + "github.com/mitchellh/multistep" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "golang.org/x/crypto/ssh" ) // 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(csp gophercloud.CloudServersProvider, sshinterface string, port int) func(multistep.StateBag) (string, error) { +func SSHAddress( + client *gophercloud.ServiceClient, + sshinterface string, port int) func(multistep.StateBag) (string, error) { return func(state multistep.StateBag) (string, error) { - s := state.Get("server").(*gophercloud.Server) + s := state.Get("server").(*servers.Server) - if ip := state.Get("access_ip").(gophercloud.FloatingIp); ip.Ip != "" { - return fmt.Sprintf("%s:%d", ip.Ip, port), nil + // If we have a floating IP, use that + ip := state.Get("access_ip").(*floatingip.FloatingIP) + if ip != nil && ip.FixedIP != "" { + return fmt.Sprintf("%s:%d", ip.FixedIP, port), nil } - ip_pools, err := s.AllAddressPools() - if err != nil { - return "", errors.New("Error parsing SSH addresses") + if s.AccessIPv4 != "" { + return fmt.Sprintf("%s:%d", s.AccessIPv4, port), nil } - for pool, addresses := range ip_pools { - if sshinterface != "" { - if pool != sshinterface { - continue - } + + // Get all the addresses associated with this server. This + // was taken directly from Terraform. + for _, networkAddresses := range s.Addresses { + elements, ok := networkAddresses.([]interface{}) + if !ok { + log.Printf( + "[ERROR] Unknown return type for address field: %#v", + networkAddresses) + continue } - if pool != "" { - for _, address := range addresses { - if address.Addr != "" && address.Version == 4 { - return fmt.Sprintf("%s:%d", address.Addr, port), nil + + for _, element := range elements { + var addr string + address := element.(map[string]interface{}) + if address["OS-EXT-IPS:type"] == "floating" { + addr = address["addr"].(string) + } else { + if address["version"].(float64) == 4 { + addr = address["addr"].(string) } } + if addr != "" { + return fmt.Sprintf("%s:%d", addr, port), nil + } } } - serverState, err := csp.ServerById(s.Id) - + s, err := servers.Get(client, s.ID).Extract() if err != nil { return "", err } - state.Put("server", serverState) + state.Put("server", s) time.Sleep(1 * time.Second) return "", errors.New("couldn't determine IP address for server") diff --git a/builder/openstack/step_allocate_ip.go b/builder/openstack/step_allocate_ip.go index b64f8b617..16efe8d38 100644 --- a/builder/openstack/step_allocate_ip.go +++ b/builder/openstack/step_allocate_ip.go @@ -2,10 +2,11 @@ package openstack import ( "fmt" + "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" - - "github.com/mitchellh/gophercloud-fork-40444fb" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" ) type StepAllocateIp struct { @@ -15,53 +16,78 @@ type StepAllocateIp struct { func (s *StepAllocateIp) Run(state multistep.StateBag) multistep.StepAction { ui := state.Get("ui").(packer.Ui) - csp := state.Get("csp").(gophercloud.CloudServersProvider) - server := state.Get("server").(*gophercloud.Server) + config := state.Get("config").(Config) + server := state.Get("server").(*servers.Server) + + // We need the v2 compute client + client, err := config.computeV2Client() + if err != nil { + err = fmt.Errorf("Error initializing compute client: %s", err) + state.Put("error", err) + return multistep.ActionHalt + } - var instanceIp gophercloud.FloatingIp + var instanceIp *floatingip.FloatingIP // This is here in case we error out before putting instanceIp into the // statebag below, because it is requested by Cleanup() state.Put("access_ip", instanceIp) if s.FloatingIp != "" { - instanceIp.Ip = s.FloatingIp + *instanceIp = floatingip.FloatingIP{FixedIP: s.FloatingIp} } else if s.FloatingIpPool != "" { - newIp, err := csp.CreateFloatingIp(s.FloatingIpPool) + newIp, err := floatingip.Create(client, floatingip.CreateOpts{ + Pool: s.FloatingIpPool, + }).Extract() if err != nil { err := fmt.Errorf("Error creating floating ip from pool '%s'", s.FloatingIpPool) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt } - instanceIp = newIp - ui.Say(fmt.Sprintf("Created temporary floating IP %s...", instanceIp.Ip)) + + *instanceIp = *newIp + ui.Say(fmt.Sprintf("Created temporary floating IP %s...", instanceIp.FixedIP)) } - if instanceIp.Ip != "" { - if err := csp.AssociateFloatingIp(server.Id, instanceIp); err != nil { - err := fmt.Errorf("Error associating floating IP %s with instance.", instanceIp.Ip) + if instanceIp != nil && instanceIp.FixedIP != "" { + err := floatingip.Associate(client, server.ID, instanceIp.FixedIP).ExtractErr() + if err != nil { + err := fmt.Errorf( + "Error associating floating IP %s with instance.", + instanceIp.FixedIP) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt - } else { - ui.Say(fmt.Sprintf("Added floating IP %s to instance...", instanceIp.Ip)) } + + ui.Say(fmt.Sprintf( + "Added floating IP %s to instance...", instanceIp.FixedIP)) } state.Put("access_ip", instanceIp) - return multistep.ActionContinue } func (s *StepAllocateIp) Cleanup(state multistep.StateBag) { + config := state.Get("config").(Config) ui := state.Get("ui").(packer.Ui) - csp := state.Get("csp").(gophercloud.CloudServersProvider) - instanceIp := state.Get("access_ip").(gophercloud.FloatingIp) - if s.FloatingIpPool != "" && instanceIp.Id != 0 { - if err := csp.DeleteFloatingIp(instanceIp); err != nil { - ui.Error(fmt.Sprintf("Error deleting temporary floating IP %s", instanceIp.Ip)) + instanceIp := state.Get("access_ip").(*floatingip.FloatingIP) + + // We need the v2 compute client + client, err := config.computeV2Client() + if err != nil { + ui.Error(fmt.Sprintf( + "Error deleting temporary floating IP %s", instanceIp.FixedIP)) + return + } + + if s.FloatingIpPool != "" && instanceIp.ID != "" { + if err := floatingip.Delete(client, instanceIp.ID).ExtractErr(); err != nil { + ui.Error(fmt.Sprintf( + "Error deleting temporary floating IP %s", instanceIp.FixedIP)) return } - ui.Say(fmt.Sprintf("Deleted temporary floating IP %s", instanceIp.Ip)) + + ui.Say(fmt.Sprintf("Deleted temporary floating IP %s", instanceIp.FixedIP)) } } diff --git a/builder/openstack/step_create_image.go b/builder/openstack/step_create_image.go index 52a2ec4d1..b777e8b0b 100644 --- a/builder/openstack/step_create_image.go +++ b/builder/openstack/step_create_image.go @@ -2,28 +2,36 @@ package openstack import ( "fmt" - "github.com/mitchellh/multistep" - "github.com/mitchellh/packer/packer" "log" "time" - "github.com/mitchellh/gophercloud-fork-40444fb" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/compute/v2/images" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" ) type stepCreateImage struct{} func (s *stepCreateImage) Run(state multistep.StateBag) multistep.StepAction { - csp := state.Get("csp").(gophercloud.CloudServersProvider) config := state.Get("config").(Config) - server := state.Get("server").(*gophercloud.Server) + server := state.Get("server").(*servers.Server) ui := state.Get("ui").(packer.Ui) + // We need the v2 compute client + client, err := config.computeV2Client() + if err != nil { + err = fmt.Errorf("Error initializing compute client: %s", err) + state.Put("error", err) + return multistep.ActionHalt + } + // Create the image ui.Say(fmt.Sprintf("Creating the image: %s", config.ImageName)) - createOpts := gophercloud.CreateImage{ + imageId, err := servers.CreateImage(client, server.ID, servers.CreateImageOpts{ Name: config.ImageName, - } - imageId, err := csp.CreateImage(server.Id, createOpts) + }).ExtractImageID() if err != nil { err := fmt.Errorf("Error creating image: %s", err) state.Put("error", err) @@ -32,12 +40,12 @@ func (s *stepCreateImage) Run(state multistep.StateBag) multistep.StepAction { } // Set the Image ID in the state - ui.Say(fmt.Sprintf("Image: %s", imageId)) + ui.Message(fmt.Sprintf("Image: %s", imageId)) state.Put("image", imageId) // Wait for the image to become ready ui.Say("Waiting for image to become ready...") - if err := WaitForImage(csp, imageId); err != nil { + if err := WaitForImage(client, imageId); err != nil { err := fmt.Errorf("Error waiting for image: %s", err) state.Put("error", err) ui.Error(err.Error()) @@ -52,10 +60,17 @@ func (s *stepCreateImage) Cleanup(multistep.StateBag) { } // WaitForImage waits for the given Image ID to become ready. -func WaitForImage(csp gophercloud.CloudServersProvider, imageId string) error { +func WaitForImage(client *gophercloud.ServiceClient, imageId string) error { for { - image, err := csp.ImageById(imageId) + image, err := images.Get(client, imageId).Extract() if err != nil { + errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if ok && errCode.Actual == 500 { + log.Printf("[ERROR] 500 error received, will ignore and retry: %s", err) + time.Sleep(2 * time.Second) + continue + } + return err } diff --git a/builder/openstack/step_key_pair.go b/builder/openstack/step_key_pair.go index 9c46b4377..06bcbf9ea 100644 --- a/builder/openstack/step_key_pair.go +++ b/builder/openstack/step_key_pair.go @@ -2,14 +2,13 @@ package openstack import ( "fmt" - "github.com/mitchellh/multistep" - "github.com/mitchellh/packer/common/uuid" - "github.com/mitchellh/packer/packer" - "log" "os" "runtime" - "github.com/mitchellh/gophercloud-fork-40444fb" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/common/uuid" + "github.com/mitchellh/packer/packer" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" ) type StepKeyPair struct { @@ -19,18 +18,28 @@ type StepKeyPair struct { } func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction { - csp := state.Get("csp").(gophercloud.CloudServersProvider) + config := state.Get("config").(Config) ui := state.Get("ui").(packer.Ui) + // We need the v2 compute client + computeClient, err := config.computeV2Client() + if err != nil { + err = fmt.Errorf("Error initializing compute client: %s", err) + state.Put("error", err) + return multistep.ActionHalt + } + ui.Say("Creating temporary keypair for this instance...") keyName := fmt.Sprintf("packer %s", uuid.TimeOrderedUUID()) - log.Printf("temporary keypair name: %s", keyName) - keyResp, err := csp.CreateKeyPair(gophercloud.NewKeyPair{Name: keyName}) + keypair, err := keypairs.Create(computeClient, keypairs.CreateOpts{ + Name: keyName, + }).Extract() if err != nil { state.Put("error", fmt.Errorf("Error creating temporary keypair: %s", err)) return multistep.ActionHalt } - if keyResp.PrivateKey == "" { + + if keypair.PrivateKey == "" { state.Put("error", fmt.Errorf("The temporary keypair returned was blank")) return multistep.ActionHalt } @@ -47,7 +56,7 @@ func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction { defer f.Close() // Write the key out - if _, err := f.Write([]byte(keyResp.PrivateKey)); err != nil { + if _, err := f.Write([]byte(keypair.PrivateKey)); err != nil { state.Put("error", fmt.Errorf("Error saving debug key: %s", err)) return multistep.ActionHalt } @@ -66,7 +75,7 @@ func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction { // Set some state data for use in future steps state.Put("keyPair", keyName) - state.Put("privateKey", keyResp.PrivateKey) + state.Put("privateKey", keypair.PrivateKey) return multistep.ActionContinue } @@ -77,11 +86,19 @@ func (s *StepKeyPair) Cleanup(state multistep.StateBag) { return } - csp := state.Get("csp").(gophercloud.CloudServersProvider) + config := state.Get("config").(Config) ui := state.Get("ui").(packer.Ui) + // We need the v2 compute client + computeClient, err := config.computeV2Client() + if err != nil { + ui.Error(fmt.Sprintf( + "Error cleaning up keypair. Please delete the key manually: %s", s.keyName)) + return + } + ui.Say("Deleting temporary keypair...") - err := csp.DeleteKeyPair(s.keyName) + err = keypairs.Delete(computeClient, s.keyName).ExtractErr() 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 index 19e7f024d..4432d5860 100644 --- a/builder/openstack/step_run_source_server.go +++ b/builder/openstack/step_run_source_server.go @@ -2,11 +2,12 @@ package openstack import ( "fmt" - "github.com/mitchellh/multistep" - "github.com/mitchellh/packer/packer" "log" - "github.com/mitchellh/gophercloud-fork-40444fb" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" ) type StepRunSourceServer struct { @@ -16,37 +17,38 @@ type StepRunSourceServer struct { SecurityGroups []string Networks []string - server *gophercloud.Server + server *servers.Server } func (s *StepRunSourceServer) Run(state multistep.StateBag) multistep.StepAction { - csp := state.Get("csp").(gophercloud.CloudServersProvider) + config := state.Get("config").(Config) keyName := state.Get("keyPair").(string) ui := state.Get("ui").(packer.Ui) - // XXX - validate image and flavor is available - - securityGroups := make([]map[string]interface{}, len(s.SecurityGroups)) - for i, groupName := range s.SecurityGroups { - securityGroups[i] = make(map[string]interface{}) - securityGroups[i]["name"] = groupName + // We need the v2 compute client + computeClient, err := config.computeV2Client() + if err != nil { + err = fmt.Errorf("Error initializing compute client: %s", err) + state.Put("error", err) + return multistep.ActionHalt } - networks := make([]gophercloud.NetworkConfig, len(s.Networks)) + networks := make([]servers.Network, len(s.Networks)) for i, networkUuid := range s.Networks { - networks[i].Uuid = networkUuid + networks[i].UUID = networkUuid } - server := gophercloud.NewServer{ - Name: s.Name, - ImageRef: s.SourceImage, - FlavorRef: s.Flavor, - KeyPairName: keyName, - SecurityGroup: securityGroups, - Networks: networks, - } - - serverResp, err := csp.CreateServer(server) + s.server, err = servers.Create(computeClient, keypairs.CreateOptsExt{ + CreateOptsBuilder: servers.CreateOpts{ + Name: s.Name, + ImageRef: s.SourceImage, + FlavorName: s.Flavor, + SecurityGroups: s.SecurityGroups, + Networks: networks, + }, + + KeyName: keyName, + }).Extract() if err != nil { err := fmt.Errorf("Error launching source server: %s", err) state.Put("error", err) @@ -54,25 +56,24 @@ func (s *StepRunSourceServer) Run(state multistep.StateBag) multistep.StepAction return multistep.ActionHalt } - s.server, err = csp.ServerById(serverResp.Id) - log.Printf("server id: %s", s.server.Id) + log.Printf("server id: %s", s.server.ID) - ui.Say(fmt.Sprintf("Waiting for server (%s) to become ready...", s.server.Id)) + ui.Say(fmt.Sprintf("Waiting for server (%s) to become ready...", s.server.ID)) stateChange := StateChangeConf{ Pending: []string{"BUILD"}, Target: "ACTIVE", - Refresh: ServerStateRefreshFunc(csp, s.server), + Refresh: ServerStateRefreshFunc(computeClient, 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) + err := fmt.Errorf("Error waiting for server (%s) to become ready: %s", s.server.ID, err) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt } - s.server = latestServer.(*gophercloud.Server) + s.server = latestServer.(*servers.Server) state.Put("server", s.server) return multistep.ActionContinue @@ -83,18 +84,25 @@ func (s *StepRunSourceServer) Cleanup(state multistep.StateBag) { return } - csp := state.Get("csp").(gophercloud.CloudServersProvider) + config := state.Get("config").(Config) ui := state.Get("ui").(packer.Ui) + // We need the v2 compute client + computeClient, err := config.computeV2Client() + if err != nil { + ui.Error(fmt.Sprintf("Error terminating server, may still be around: %s", err)) + return + } + ui.Say("Terminating the source server...") - if err := csp.DeleteServerById(s.server.Id); err != nil { + if err := servers.Delete(computeClient, s.server.ID).ExtractErr(); err != nil { ui.Error(fmt.Sprintf("Error terminating server, may still be around: %s", err)) return } stateChange := StateChangeConf{ Pending: []string{"ACTIVE", "BUILD", "REBUILD", "SUSPENDED"}, - Refresh: ServerStateRefreshFunc(csp, s.server), + Refresh: ServerStateRefreshFunc(computeClient, s.server), Target: "DELETED", } diff --git a/builder/openstack/step_wait_for_rackconnect.go b/builder/openstack/step_wait_for_rackconnect.go index ee6ee6138..6263bd17d 100644 --- a/builder/openstack/step_wait_for_rackconnect.go +++ b/builder/openstack/step_wait_for_rackconnect.go @@ -2,11 +2,11 @@ package openstack import ( "fmt" - "github.com/mitchellh/multistep" - "github.com/mitchellh/packer/packer" "time" - "github.com/mitchellh/gophercloud-fork-40444fb" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" ) type StepWaitForRackConnect struct { @@ -18,14 +18,22 @@ func (s *StepWaitForRackConnect) Run(state multistep.StateBag) multistep.StepAct return multistep.ActionContinue } - csp := state.Get("csp").(gophercloud.CloudServersProvider) - server := state.Get("server").(*gophercloud.Server) + config := state.Get("config").(Config) + server := state.Get("server").(*servers.Server) ui := state.Get("ui").(packer.Ui) - ui.Say(fmt.Sprintf("Waiting for server (%s) to become RackConnect ready...", server.Id)) + // We need the v2 compute client + computeClient, err := config.computeV2Client() + if err != nil { + err = fmt.Errorf("Error initializing compute client: %s", err) + state.Put("error", err) + return multistep.ActionHalt + } + ui.Say(fmt.Sprintf( + "Waiting for server (%s) to become RackConnect ready...", server.ID)) for { - server, err := csp.ServerById(server.Id) + server, err = servers.Get(computeClient, server.ID).Extract() if err != nil { return multistep.ActionHalt } From c80d1ab46b609637399a2e696f1e184812b92067 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 12 Jun 2015 00:16:56 -0400 Subject: [PATCH 06/12] remove the new plugin --- plugin/builder-openstack-new/main.go | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 plugin/builder-openstack-new/main.go diff --git a/plugin/builder-openstack-new/main.go b/plugin/builder-openstack-new/main.go deleted file mode 100644 index d8075c78d..000000000 --- a/plugin/builder-openstack-new/main.go +++ /dev/null @@ -1,15 +0,0 @@ -package main - -import ( - "github.com/mitchellh/packer/builder/openstack-new" - "github.com/mitchellh/packer/packer/plugin" -) - -func main() { - server, err := plugin.Server() - if err != nil { - panic(err) - } - server.RegisterBuilder(new(openstack.Builder)) - server.Serve() -} From 50e2eb30e6f2c0cb2313b91c1ccb9db62c7eb2e9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 12 Jun 2015 00:28:38 -0400 Subject: [PATCH 07/12] builder/openstack: modifications to work with rackspace --- builder/openstack/access_config.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/builder/openstack/access_config.go b/builder/openstack/access_config.go index e0f962c50..71679b979 100644 --- a/builder/openstack/access_config.go +++ b/builder/openstack/access_config.go @@ -42,10 +42,7 @@ func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error { } // Get as much as possible from the end - ao, err := openstack.AuthOptionsFromEnv() - if err != nil { - return []error{err} - } + ao, _ := openstack.AuthOptionsFromEnv() // Override values if we have them in our config overrides := []struct { From e724b5fe80de907da0dc8aa15b10ae0b83e0a812 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 12 Jun 2015 00:33:52 -0400 Subject: [PATCH 08/12] builder/openstack: support legacy env vars --- builder/openstack/access_config.go | 17 ++++++ .../docs/builders/openstack.html.markdown | 57 +++++-------------- 2 files changed, 30 insertions(+), 44 deletions(-) diff --git a/builder/openstack/access_config.go b/builder/openstack/access_config.go index 71679b979..ca33495a8 100644 --- a/builder/openstack/access_config.go +++ b/builder/openstack/access_config.go @@ -41,6 +41,23 @@ func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error { c.Region = os.Getenv("OS_REGION_NAME") } + // Legacy RackSpace stuff. We're keeping this around to keep things BC. + if c.APIKey == "" { + c.APIKey = os.Getenv("SDK_API_KEY") + } + if c.Password == "" { + c.Password = os.Getenv("SDK_PASSWORD") + } + if c.Region == "" { + c.Region = os.Getenv("SDK_REGION") + } + if c.TenantName == "" { + c.TenantName = os.Getenv("SDK_PROJECT") + } + if c.Username == "" { + c.Username = os.Getenv("SDK_USERNAME") + } + // Get as much as possible from the end ao, _ := openstack.AuthOptionsFromEnv() diff --git a/website/source/docs/builders/openstack.html.markdown b/website/source/docs/builders/openstack.html.markdown index d5dbbf249..10f38a445 100644 --- a/website/source/docs/builders/openstack.html.markdown +++ b/website/source/docs/builders/openstack.html.markdown @@ -34,23 +34,21 @@ each category, the available configuration keys are alphabetized. * `image_name` (string) - The name of the resulting image. -* `password` (string) - The password used to connect to the OpenStack service. - If not specified, Packer will use the environment variables - `SDK_PASSWORD` or `OS_PASSWORD` (in that order), if set. - * `source_image` (string) - The ID or full URL to the base image to use. This is the image that will be used to launch a new server and provision it. * `username` (string) - The username used to connect to the OpenStack service. + If not specified, Packer will use the environment variable + `OS_USERNAME`, if set. + +* `password` (string) - The password used to connect to the OpenStack service. If not specified, Packer will use the environment variables - `SDK_USERNAME` or `OS_USERNAME` (in that order), if set. + `OS_PASSWORD`, if set. ### Optional: * `api_key` (string) - The API key used to access OpenStack. Some OpenStack installations require this. - If not specified, Packer will use the environment variables - `SDK_API_KEY`, if set. * `floating_ip` (string) - A specific floating IP to assign to this instance. `use_floating_ip` must also be set to true for this to have an affect. @@ -65,32 +63,18 @@ each category, the available configuration keys are alphabetized. * `networks` (array of strings) - A list of networks by UUID to attach to this instance. -* `openstack_provider` (string) - A name of a provider that has a slightly - different API model. Currently supported values are "openstack" (default), - and "rackspace". - -* `project` (string) - The project name to boot the instance into. Some - OpenStack installations require this. - If not specified, Packer will use the environment variables - `SDK_PROJECT` or `OS_TENANT_NAME` (in that order), if set. - -* `provider` (string) - The provider used to connect to the OpenStack service. - If not specified, Packer will use the environment variables `SDK_PROVIDER` - or `OS_AUTH_URL` (in that order), if set. - For Rackspace this should be `rackspace-us` or `rackspace-uk`. - -* `proxy_url` (string) +* `tenant_id` or `tenant_name` (string) - The tenant ID or name to boot the + instance into. Some OpenStack installations require this. + If not specified, Packer will use the environment variable + `OS_TENANT_NAME`, if set. * `security_groups` (array of strings) - A list of security groups by name to add to this instance. * `region` (string) - The name of the region, such as "DFW", in which to launch the server to create the AMI. - If not specified, Packer will use the environment variables - `SDK_REGION` or `OS_REGION_NAME` (in that order), if set. - For a `provider` of "rackspace", it is required to specify a region, - either using this option or with an environment variable. For other - providers, including a private cloud, specifying a region is optional. + If not specified, Packer will use the environment variable + `OS_REGION_NAME`, if set. * `ssh_port` (integer) - The port that SSH will be available on. Defaults to port 22. @@ -106,9 +90,6 @@ each category, the available configuration keys are alphabetized. useful for Rackspace are "public" or "private", and the default behavior is to connect via whichever is returned first from the OpenStack API. -* `tenant_id` (string) - Tenant ID for accessing OpenStack if your - installation requires this. - * `use_floating_ip` (boolean) - Whether or not to use a floating IP for the instance. Defaults to false. @@ -124,10 +105,8 @@ Ubuntu 12.04 LTS (Precise Pangolin) on Rackspace OpenStack cloud offering. ```javascript { "type": "openstack", - "username": "", - "api_key": "", - "openstack_provider": "rackspace", - "provider": "rackspace-us", + "username": "foo", + "password": "foo", "region": "DFW", "ssh_username": "root", "image_name": "Test image", @@ -160,13 +139,3 @@ script is setting environment variables like: * `OS_TENANT_ID` * `OS_USERNAME` * `OS_PASSWORD` - -## Troubleshooting - -*I get the error "Missing or incorrect provider"* - -* Verify your "username", "password" and "provider" settings. - -*I get the error "Missing endpoint, or insufficient privileges to access endpoint"* - -* Verify your "region" setting. From 590177ea4b0b5057c5ec9a4da4b2811651d62c8b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 12 Jun 2015 00:35:54 -0400 Subject: [PATCH 09/12] builder/openstack: fix unit tests --- builder/openstack/builder_test.go | 53 ------------------------------- 1 file changed, 53 deletions(-) diff --git a/builder/openstack/builder_test.go b/builder/openstack/builder_test.go index badf9784d..ce15873eb 100644 --- a/builder/openstack/builder_test.go +++ b/builder/openstack/builder_test.go @@ -9,7 +9,6 @@ func testConfig() map[string]interface{} { return map[string]interface{}{ "username": "foo", "password": "bar", - "provider": "foo", "region": "DFW", "image_name": "foo", "source_image": "foo", @@ -40,55 +39,3 @@ func TestBuilder_Prepare_BadType(t *testing.T) { t.Fatalf("prepare should fail") } } - -func TestBuilderPrepare_ImageName(t *testing.T) { - var b Builder - config := testConfig() - - // Test good - config["image_name"] = "foo" - warns, err := b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err != nil { - t.Fatalf("should not have error: %s", err) - } - - // Test bad - config["image_name"] = "foo {{" - b = Builder{} - warns, err = b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err == nil { - t.Fatal("should have error") - } - - // Test bad - delete(config, "image_name") - b = Builder{} - warns, err = b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - 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 - warns, err := b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err == nil { - t.Fatal("should have error") - } -} From 5d32a1f6e059b107a112ec7811dd8a16c2e6101f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 12 Jun 2015 10:02:04 -0400 Subject: [PATCH 10/12] builder/openstack: use IP not FixedIP --- builder/openstack/ssh.go | 4 ++-- builder/openstack/step_allocate_ip.go | 27 ++++++++++++++------------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/builder/openstack/ssh.go b/builder/openstack/ssh.go index 7b0510f98..76c2686b1 100644 --- a/builder/openstack/ssh.go +++ b/builder/openstack/ssh.go @@ -23,8 +23,8 @@ func SSHAddress( // If we have a floating IP, use that ip := state.Get("access_ip").(*floatingip.FloatingIP) - if ip != nil && ip.FixedIP != "" { - return fmt.Sprintf("%s:%d", ip.FixedIP, port), nil + if ip != nil && ip.IP != "" { + return fmt.Sprintf("%s:%d", ip.IP, port), nil } if s.AccessIPv4 != "" { diff --git a/builder/openstack/step_allocate_ip.go b/builder/openstack/step_allocate_ip.go index 16efe8d38..0ab9b3529 100644 --- a/builder/openstack/step_allocate_ip.go +++ b/builder/openstack/step_allocate_ip.go @@ -27,13 +27,14 @@ func (s *StepAllocateIp) Run(state multistep.StateBag) multistep.StepAction { return multistep.ActionHalt } - var instanceIp *floatingip.FloatingIP + var instanceIp floatingip.FloatingIP + // This is here in case we error out before putting instanceIp into the // statebag below, because it is requested by Cleanup() - state.Put("access_ip", instanceIp) + state.Put("access_ip", &instanceIp) if s.FloatingIp != "" { - *instanceIp = floatingip.FloatingIP{FixedIP: s.FloatingIp} + instanceIp.IP = s.FloatingIp } else if s.FloatingIpPool != "" { newIp, err := floatingip.Create(client, floatingip.CreateOpts{ Pool: s.FloatingIpPool, @@ -45,26 +46,26 @@ func (s *StepAllocateIp) Run(state multistep.StateBag) multistep.StepAction { return multistep.ActionHalt } - *instanceIp = *newIp - ui.Say(fmt.Sprintf("Created temporary floating IP %s...", instanceIp.FixedIP)) + instanceIp = *newIp + ui.Say(fmt.Sprintf("Created temporary floating IP %s...", instanceIp.IP)) } - if instanceIp != nil && instanceIp.FixedIP != "" { - err := floatingip.Associate(client, server.ID, instanceIp.FixedIP).ExtractErr() + if instanceIp.IP != "" { + err := floatingip.Associate(client, server.ID, instanceIp.IP).ExtractErr() if err != nil { err := fmt.Errorf( "Error associating floating IP %s with instance.", - instanceIp.FixedIP) + instanceIp.IP) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt } ui.Say(fmt.Sprintf( - "Added floating IP %s to instance...", instanceIp.FixedIP)) + "Added floating IP %s to instance...", instanceIp.IP)) } - state.Put("access_ip", instanceIp) + state.Put("access_ip", &instanceIp) return multistep.ActionContinue } @@ -77,17 +78,17 @@ func (s *StepAllocateIp) Cleanup(state multistep.StateBag) { client, err := config.computeV2Client() if err != nil { ui.Error(fmt.Sprintf( - "Error deleting temporary floating IP %s", instanceIp.FixedIP)) + "Error deleting temporary floating IP %s", instanceIp.IP)) return } if s.FloatingIpPool != "" && instanceIp.ID != "" { if err := floatingip.Delete(client, instanceIp.ID).ExtractErr(); err != nil { ui.Error(fmt.Sprintf( - "Error deleting temporary floating IP %s", instanceIp.FixedIP)) + "Error deleting temporary floating IP %s", instanceIp.IP)) return } - ui.Say(fmt.Sprintf("Deleted temporary floating IP %s", instanceIp.FixedIP)) + ui.Say(fmt.Sprintf("Deleted temporary floating IP %s", instanceIp.IP)) } } From ad374e82afe184249687a8e0993e7376b9d92a7c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 12 Jun 2015 10:05:03 -0400 Subject: [PATCH 11/12] builder/openstack: shuffle some fields to note unused fields --- builder/openstack/run_config.go | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/builder/openstack/run_config.go b/builder/openstack/run_config.go index e5d73c9c1..de76fad20 100644 --- a/builder/openstack/run_config.go +++ b/builder/openstack/run_config.go @@ -11,19 +11,21 @@ import ( // 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"` - SSHInterface string `mapstructure:"ssh_interface"` - OpenstackProvider string `mapstructure:"openstack_provider"` - UseFloatingIp bool `mapstructure:"use_floating_ip"` - RackconnectWait bool `mapstructure:"rackconnect_wait"` - FloatingIpPool string `mapstructure:"floating_ip_pool"` - FloatingIp string `mapstructure:"floating_ip"` - SecurityGroups []string `mapstructure:"security_groups"` - Networks []string `mapstructure:"networks"` + SourceImage string `mapstructure:"source_image"` + Flavor string `mapstructure:"flavor"` + RawSSHTimeout string `mapstructure:"ssh_timeout"` + SSHUsername string `mapstructure:"ssh_username"` + SSHPort int `mapstructure:"ssh_port"` + SSHInterface string `mapstructure:"ssh_interface"` + RackconnectWait bool `mapstructure:"rackconnect_wait"` + FloatingIpPool string `mapstructure:"floating_ip_pool"` + FloatingIp string `mapstructure:"floating_ip"` + SecurityGroups []string `mapstructure:"security_groups"` + Networks []string `mapstructure:"networks"` + + // Not really used, but here for BC + OpenstackProvider string `mapstructure:"openstack_provider"` + UseFloatingIp bool `mapstructure:"use_floating_ip"` // Unexported fields that are calculated from others sshTimeout time.Duration From 92b6b5c387b4f090d55450b44ba1619d5a3eddef Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 12 Jun 2015 10:32:31 -0400 Subject: [PATCH 12/12] builder/openstack: can ref flavor by name --- builder/openstack/builder.go | 4 +- builder/openstack/step_load_flavor.go | 61 +++++++++++++++++++ builder/openstack/step_run_source_server.go | 8 ++- .../docs/builders/openstack.html.markdown | 2 +- 4 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 builder/openstack/step_load_flavor.go diff --git a/builder/openstack/builder.go b/builder/openstack/builder.go index bebb28452..d30ede8d3 100644 --- a/builder/openstack/builder.go +++ b/builder/openstack/builder.go @@ -67,13 +67,15 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe // Build the steps steps := []multistep.Step{ + &StepLoadFlavor{ + Flavor: b.config.Flavor, + }, &StepKeyPair{ Debug: b.config.PackerDebug, DebugKeyPath: fmt.Sprintf("os_%s.pem", b.config.PackerBuildName), }, &StepRunSourceServer{ Name: b.config.ImageName, - Flavor: b.config.Flavor, SourceImage: b.config.SourceImage, SecurityGroups: b.config.SecurityGroups, Networks: b.config.Networks, diff --git a/builder/openstack/step_load_flavor.go b/builder/openstack/step_load_flavor.go new file mode 100644 index 000000000..8b8cae994 --- /dev/null +++ b/builder/openstack/step_load_flavor.go @@ -0,0 +1,61 @@ +package openstack + +import ( + "fmt" + "log" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "github.com/rackspace/gophercloud/openstack/compute/v2/flavors" +) + +// StepLoadFlavor gets the FlavorRef from a Flavor. It first assumes +// that the Flavor is a ref and verifies it. Otherwise, it tries to find +// the flavor by name. +type StepLoadFlavor struct { + Flavor string +} + +func (s *StepLoadFlavor) Run(state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(Config) + ui := state.Get("ui").(packer.Ui) + + // We need the v2 compute client + client, err := config.computeV2Client() + if err != nil { + err = fmt.Errorf("Error initializing compute client: %s", err) + state.Put("error", err) + return multistep.ActionHalt + } + + ui.Say(fmt.Sprintf("Loading flavor: %s", s.Flavor)) + log.Printf("[INFO] Loading flavor by ID: %s", s.Flavor) + flavor, err := flavors.Get(client, s.Flavor).Extract() + if err != nil { + log.Printf("[ERROR] Failed to find flavor by ID: %s", err) + geterr := err + + log.Printf("[INFO] Loading flavor by name: %s", s.Flavor) + id, err := flavors.IDFromName(client, s.Flavor) + if err != nil { + log.Printf("[ERROR] Failed to find flavor by name: %s", err) + err = fmt.Errorf( + "Unable to find specified flavor by ID or name!\n\n"+ + "Error from ID lookup: %s\n\n"+ + "Error from name lookup: %s", + geterr, + err) + state.Put("error", err) + return multistep.ActionHalt + } + + flavor = &flavors.Flavor{ID: id} + } + + ui.Message(fmt.Sprintf("Verified flavor. ID: %s", flavor.ID)) + state.Put("flavor_id", flavor.ID) + return multistep.ActionContinue +} + +func (s *StepLoadFlavor) Cleanup(state multistep.StateBag) { +} diff --git a/builder/openstack/step_run_source_server.go b/builder/openstack/step_run_source_server.go index 4432d5860..89d816297 100644 --- a/builder/openstack/step_run_source_server.go +++ b/builder/openstack/step_run_source_server.go @@ -11,7 +11,6 @@ import ( ) type StepRunSourceServer struct { - Flavor string Name string SourceImage string SecurityGroups []string @@ -22,6 +21,7 @@ type StepRunSourceServer struct { func (s *StepRunSourceServer) Run(state multistep.StateBag) multistep.StepAction { config := state.Get("config").(Config) + flavor := state.Get("flavor_id").(string) keyName := state.Get("keyPair").(string) ui := state.Get("ui").(packer.Ui) @@ -38,11 +38,12 @@ func (s *StepRunSourceServer) Run(state multistep.StateBag) multistep.StepAction networks[i].UUID = networkUuid } + ui.Say("Launching server...") s.server, err = servers.Create(computeClient, keypairs.CreateOptsExt{ CreateOptsBuilder: servers.CreateOpts{ Name: s.Name, ImageRef: s.SourceImage, - FlavorName: s.Flavor, + FlavorRef: flavor, SecurityGroups: s.SecurityGroups, Networks: networks, }, @@ -56,9 +57,10 @@ func (s *StepRunSourceServer) Run(state multistep.StateBag) multistep.StepAction return multistep.ActionHalt } + ui.Message(fmt.Sprintf("Server ID: %s", s.server.ID)) log.Printf("server id: %s", s.server.ID) - ui.Say(fmt.Sprintf("Waiting for server (%s) to become ready...", s.server.ID)) + ui.Say("Waiting for server to become ready...") stateChange := StateChangeConf{ Pending: []string{"BUILD"}, Target: "ACTIVE", diff --git a/website/source/docs/builders/openstack.html.markdown b/website/source/docs/builders/openstack.html.markdown index 10f38a445..3b5bf791d 100644 --- a/website/source/docs/builders/openstack.html.markdown +++ b/website/source/docs/builders/openstack.html.markdown @@ -29,7 +29,7 @@ each category, the available configuration keys are alphabetized. ### Required: -* `flavor` (string) - The ID or full URL for the desired flavor for the +* `flavor` (string) - The ID, name, or full URL for the desired flavor for the server to be created. * `image_name` (string) - The name of the resulting image.