Skip to content

Commit

Permalink
Add a CloudStack Builder
Browse files Browse the repository at this point in the history
  • Loading branch information
Sander van Harmelen committed Oct 16, 2016
1 parent ba00afd commit dbf3bf5
Show file tree
Hide file tree
Showing 17 changed files with 1,539 additions and 1 deletion.
64 changes: 64 additions & 0 deletions builder/cloudstack/artifact.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package cloudstack

import (
"fmt"
"log"
"strings"

"github.com/xanzy/go-cloudstack/cloudstack"
)

// Artifact represents a CloudStack template as the result of a Packer build.
type Artifact struct {
client *cloudstack.CloudStackClient
config *Config
template *cloudstack.CreateTemplateResponse
}

// BuilderId returns the builder ID.
func (a *Artifact) BuilderId() string {
return BuilderId
}

// Destroy the CloudStack template represented by the artifact.
func (a *Artifact) Destroy() error {
// Create a new parameter struct.
p := a.client.Template.NewDeleteTemplateParams(a.template.Id)

// Destroy the template.
log.Printf("Destroying template: %s", a.template.Name)
_, err := a.client.Template.DeleteTemplate(p)
if err != nil {
// This is a very poor way to be told the ID does no longer exist :(
if strings.Contains(err.Error(), fmt.Sprintf(
"Invalid parameter id value=%s due to incorrect long value format, "+
"or entity does not exist", a.template.Id)) {
return nil
}

return fmt.Errorf("Error destroying template %s: %s", a.template.Name, err)
}

return nil
}

// Files returns the files represented by the artifact.
func (a *Artifact) Files() []string {
// We have no files.
return nil
}

// Id returns CloudStack template ID.
func (a *Artifact) Id() string {
return a.template.Id
}

// String returns the string representation of the artifact.
func (a *Artifact) String() string {
return fmt.Sprintf("A template was created: %s", a.template.Name)
}

// State returns specific details from the artifact.
func (a *Artifact) State(name string) interface{} {
return nil
}
47 changes: 47 additions & 0 deletions builder/cloudstack/artifact_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package cloudstack

import (
"testing"

"github.com/mitchellh/packer/packer"
"github.com/xanzy/go-cloudstack/cloudstack"
)

const templateID = "286dd44a-ec6b-4789-b192-804f08f04b4c"

func TestArtifact_Impl(t *testing.T) {
var raw interface{} = &Artifact{}

if _, ok := raw.(packer.Artifact); !ok {
t.Fatalf("Artifact does not implement packer.Artifact")
}
}

func TestArtifactId(t *testing.T) {
a := &Artifact{
client: nil,
config: nil,
template: &cloudstack.CreateTemplateResponse{
Id: "286dd44a-ec6b-4789-b192-804f08f04b4c",
},
}

if a.Id() != templateID {
t.Fatalf("artifact ID should match: %s", templateID)
}
}

func TestArtifactString(t *testing.T) {
a := &Artifact{
client: nil,
config: nil,
template: &cloudstack.CreateTemplateResponse{
Name: "packer-foobar",
},
}
expected := "A template was created: packer-foobar"

if a.String() != expected {
t.Fatalf("artifact string should match: %s", expected)
}
}
106 changes: 106 additions & 0 deletions builder/cloudstack/builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package cloudstack

import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer"
"github.com/xanzy/go-cloudstack/cloudstack"
)

const BuilderId = "packer.cloudstack"

// Builder represents the CloudStack builder.
type Builder struct {
config *Config
runner multistep.Runner
ui packer.Ui
}

// Prepare implements the packer.Builder interface.
func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
config, errs := NewConfig(raws...)
if errs != nil {
return nil, errs
}
b.config = config

return nil, nil
}

// Run implements the packer.Builder interface.
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
b.ui = ui

// Create a CloudStack API client.
client := cloudstack.NewAsyncClient(
b.config.APIURL,
b.config.APIKey,
b.config.SecretKey,
!b.config.SSLNoVerify,
)

// Set the time to wait before timing out
client.AsyncTimeout(int64(b.config.AsyncTimeout.Seconds()))

// Some CloudStack service providers only allow HTTP GET calls.
client.HTTPGETOnly = b.config.HTTPGetOnly

// Set up the state.
state := new(multistep.BasicStateBag)
state.Put("client", client)
state.Put("config", b.config)
state.Put("hook", hook)
state.Put("ui", ui)

// Build the steps.
steps := []multistep.Step{
&stepPrepareConfig{},
&stepCreateInstance{},
&stepSetupNetworking{},
&communicator.StepConnect{
Config: &b.config.Comm,
Host: commHost,
SSHConfig: sshConfig,
},
&common.StepProvision{},
&stepShutdownInstance{},
&stepCreateTemplate{},
}

// Configure the runner.
if b.config.PackerDebug {
b.runner = &multistep.DebugRunner{
Steps: steps,
PauseFn: common.MultistepDebugFn(ui),
}
} else {
b.runner = &multistep.BasicRunner{Steps: steps}
}

// Run the steps.
b.runner.Run(state)

// If there are no templates, then just return
template, ok := state.Get("template").(*cloudstack.CreateTemplateResponse)
if !ok || template == nil {
return nil, nil
}

// Build the artifact and return it
artifact := &Artifact{
client: client,
config: b.config,
template: template,
}

return artifact, nil
}

// Cancel the step runner.
func (b *Builder) Cancel() {
if b.runner != nil {
b.ui.Say("Cancelling the step runner...")
b.runner.Cancel()
}
}
58 changes: 58 additions & 0 deletions builder/cloudstack/builder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package cloudstack

import (
"testing"

"github.com/mitchellh/packer/packer"
)

func TestBuilder_Impl(t *testing.T) {
var raw interface{} = &Builder{}

if _, ok := raw.(packer.Builder); !ok {
t.Fatalf("Builder does not implement packer.Builder")
}
}

func TestBuilder_Prepare(t *testing.T) {
cases := map[string]struct {
Config map[string]interface{}
Err bool
}{
"good": {
Config: map[string]interface{}{
"api_url": "https://cloudstack.com/client/api",
"api_key": "some-api-key",
"secret_key": "some-secret-key",
"cidr_list": []interface{}{"0.0.0.0/0"},
"disk_size": "20",
"network": "c5ed8a14-3f21-4fa9-bd74-bb887fc0ed0d",
"service_offering": "a29c52b1-a83d-4123-a57d-4548befa47a0",
"source_template": "d31e6af5-94a8-4756-abf3-6493c38db7e5",
"ssh_username": "ubuntu",
"template_os": "52d54d24-cef1-480b-b963-527703aa4ff9",
"zone": "a3b594d9-25e9-47c1-9c03-7a5fc61e3f43",
},
Err: false,
},
"bad": {
Err: true,
},
}

b := &Builder{}

for desc, tc := range cases {
_, errs := b.Prepare(tc.Config)

if tc.Err {
if errs == nil {
t.Fatalf("%s prepare should err", desc)
}
} else {
if errs != nil {
t.Fatalf("%s prepare should not fail: %s", desc, errs)
}
}
}
}
Loading

0 comments on commit dbf3bf5

Please sign in to comment.