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