builder/openstack-new
This commit is contained in:
parent
17555ff21a
commit
c903579aaa
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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,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
|
||||
}
|
|
@ -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,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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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) {
|
||||
}
|
|
@ -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()
|
||||
}
|
Loading…
Reference in New Issue