Add a CloudStack Builder

This commit is contained in:
Sander van Harmelen 2016-01-11 12:22:41 +01:00
parent ba00afd4f1
commit dbf3bf56d4
17 changed files with 1539 additions and 1 deletions

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -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)
}
}
}
}

View File

@ -0,0 +1,176 @@
package cloudstack
import (
"errors"
"fmt"
"os"
"time"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"
)
// Config holds all the details needed to configure the builder.
type Config struct {
common.PackerConfig `mapstructure:",squash"`
Comm communicator.Config `mapstructure:",squash"`
APIURL string `mapstructure:"api_url"`
APIKey string `mapstructure:"api_key"`
SecretKey string `mapstructure:"secret_key"`
AsyncTimeout time.Duration `mapstructure:"async_timeout"`
HTTPGetOnly bool `mapstructure:"http_get_only"`
SSLNoVerify bool `mapstructure:"ssl_no_verify"`
DiskOffering string `mapstructure:"disk_offering"`
DiskSize int64 `mapstructure:"disk_size"`
CIDRList []string `mapstructure:"cidr_list"`
Hypervisor string `mapstructure:"hypervisor"`
InstanceName string `mapstructure:"instance_name"`
Keypair string `mapstructure:"keypair"`
Network string `mapstructure:"network"`
Project string `mapstructure:"project"`
PublicIPAddress string `mapstructure:"public_ip_address"`
ServiceOffering string `mapstructure:"service_offering"`
SourceTemplate string `mapstructure:"source_template"`
SourceISO string `mapstructure:"source_iso"`
UserData string `mapstructure:"user_data"`
UserDataFile string `mapstructure:"user_data_file"`
UseLocalIPAddress bool `mapstructure:"use_local_ip_address"`
Zone string `mapstructure:"zone"`
TemplateName string `mapstructure:"template_name"`
TemplateDisplayText string `mapstructure:"template_display_text"`
TemplateOS string `mapstructure:"template_os"`
TemplateFeatured bool `mapstructure:"template_featured"`
TemplatePublic bool `mapstructure:"template_public"`
TemplatePasswordEnabled bool `mapstructure:"template_password_enabled"`
TemplateRequiresHVM bool `mapstructure:"template_requires_hvm"`
TemplateScalable bool `mapstructure:"template_scalable"`
TemplateTag string `mapstructure:"template_tag"`
ctx interpolate.Context
hostAddress string // The host address used by the communicators.
instanceSource string // This can be either a template ID or an ISO ID.
}
// NewConfig parses and validates the given config.
func NewConfig(raws ...interface{}) (*Config, error) {
c := new(Config)
err := config.Decode(c, &config.DecodeOpts{
Interpolate: true,
InterpolateContext: &c.ctx,
}, raws...)
if err != nil {
return nil, err
}
var errs *packer.MultiError
// Set some defaults.
if c.AsyncTimeout == 0 {
c.AsyncTimeout = 30 * time.Minute
}
if c.Comm.SSHUsername == "" {
c.Comm.SSHUsername = "root"
}
if c.InstanceName == "" {
c.InstanceName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
}
if c.TemplateName == "" {
name, err := interpolate.Render("packer-{{timestamp}}", nil)
if err != nil {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Unable to parse template name: %s ", err))
}
c.TemplateName = name
}
if c.TemplateDisplayText == "" {
c.TemplateDisplayText = c.TemplateName
}
// Process required parameters.
if c.APIURL == "" {
errs = packer.MultiErrorAppend(errs, errors.New("a api_url must be specified"))
}
if c.APIKey == "" {
errs = packer.MultiErrorAppend(errs, errors.New("a api_key must be specified"))
}
if c.SecretKey == "" {
errs = packer.MultiErrorAppend(errs, errors.New("a secret_key must be specified"))
}
if len(c.CIDRList) == 0 && !c.UseLocalIPAddress {
errs = packer.MultiErrorAppend(errs, errors.New("a cidr_list must be specified"))
}
if c.Network == "" {
errs = packer.MultiErrorAppend(errs, errors.New("a network must be specified"))
}
if c.ServiceOffering == "" {
errs = packer.MultiErrorAppend(errs, errors.New("a service_offering must be specified"))
}
if c.SourceISO == "" && c.SourceTemplate == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("either source_iso or source_template must be specified"))
}
if c.SourceISO != "" && c.SourceTemplate != "" {
errs = packer.MultiErrorAppend(
errs, errors.New("only one of source_iso or source_template can be specified"))
}
if c.SourceISO != "" && c.DiskOffering == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("a disk_offering must be specified when using source_iso"))
}
if c.SourceISO != "" && c.Hypervisor == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("a hypervisor must be specified when using source_iso"))
}
if c.TemplateOS == "" {
errs = packer.MultiErrorAppend(errs, errors.New("a template_os must be specified"))
}
if c.UserData != "" && c.UserDataFile != "" {
errs = packer.MultiErrorAppend(
errs, errors.New("only one of user_data or user_data_file can be specified"))
}
if c.UserDataFile != "" {
if _, err := os.Stat(c.UserDataFile); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("user_data_file not found: %s", c.UserDataFile))
}
}
if c.Zone == "" {
errs = packer.MultiErrorAppend(errs, errors.New("a zone must be specified"))
}
if es := c.Comm.Prepare(&c.ctx); len(es) > 0 {
errs = packer.MultiErrorAppend(errs, es...)
}
// Check for errors and return if we have any.
if errs != nil && len(errs.Errors) > 0 {
return nil, errs
}
return c, nil
}

View File

@ -0,0 +1,166 @@
package cloudstack
import "testing"
func TestNewConfig(t *testing.T) {
cases := map[string]struct {
Config map[string]interface{}
Nullify string
Err bool
}{
"no_api_url": {
Config: map[string]interface{}{
"disk_size": "20",
"source_template": "d31e6af5-94a8-4756-abf3-6493c38db7e5",
},
Nullify: "api_url",
Err: true,
},
"no_api_key": {
Config: map[string]interface{}{
"disk_size": "20",
"source_template": "d31e6af5-94a8-4756-abf3-6493c38db7e5",
},
Nullify: "api_key",
Err: true,
},
"no_secret_key": {
Config: map[string]interface{}{
"disk_size": "20",
"source_template": "d31e6af5-94a8-4756-abf3-6493c38db7e5",
},
Nullify: "secret_key",
Err: true,
},
"no_cidr_list": {
Config: map[string]interface{}{
"disk_size": "20",
"source_template": "d31e6af5-94a8-4756-abf3-6493c38db7e5",
},
Nullify: "cidr_list",
Err: true,
},
"no_cidr_list_with_use_local_ip_address": {
Config: map[string]interface{}{
"disk_size": "20",
"source_template": "d31e6af5-94a8-4756-abf3-6493c38db7e5",
"use_local_ip_address": true,
},
Nullify: "cidr_list",
Err: false,
},
"no_network": {
Config: map[string]interface{}{
"disk_size": "20",
"source_template": "d31e6af5-94a8-4756-abf3-6493c38db7e5",
},
Nullify: "network",
Err: true,
},
"no_service_offering": {
Config: map[string]interface{}{
"disk_size": "20",
"source_template": "d31e6af5-94a8-4756-abf3-6493c38db7e5",
},
Nullify: "service_offering",
Err: true,
},
"no_template_os": {
Config: map[string]interface{}{
"disk_size": "20",
"source_template": "d31e6af5-94a8-4756-abf3-6493c38db7e5",
},
Nullify: "template_os",
Err: true,
},
"no_zone": {
Config: map[string]interface{}{
"disk_size": "20",
"source_template": "d31e6af5-94a8-4756-abf3-6493c38db7e5",
},
Nullify: "zone",
Err: true,
},
"no_source": {
Err: true,
},
"both_sources": {
Config: map[string]interface{}{
"disk_offering": "f043d193-242f-4941-a847-29408b998711",
"disk_size": "20",
"hypervisor": "KVM",
"source_iso": "fbd904dc-f46c-42e7-a467-f27480c667d5",
"source_template": "d31e6af5-94a8-4756-abf3-6493c38db7e5",
},
Err: true,
},
"source_iso_good": {
Config: map[string]interface{}{
"disk_offering": "f043d193-242f-4941-a847-29408b998711",
"hypervisor": "KVM",
"source_iso": "fbd904dc-f46c-42e7-a467-f27480c667d5",
},
Err: false,
},
"source_iso_without_disk_offering": {
Config: map[string]interface{}{
"hypervisor": "KVM",
"source_iso": "fbd904dc-f46c-42e7-a467-f27480c667d5",
},
Err: true,
},
"source_iso_without_hypervisor": {
Config: map[string]interface{}{
"disk_offering": "f043d193-242f-4941-a847-29408b998711",
"source_iso": "fbd904dc-f46c-42e7-a467-f27480c667d5",
},
Err: true,
},
"source_template_good": {
Config: map[string]interface{}{
"disk_size": "20",
"source_template": "d31e6af5-94a8-4756-abf3-6493c38db7e5",
},
Err: false,
},
}
for desc, tc := range cases {
raw := testConfig(tc.Config)
if tc.Nullify != "" {
raw[tc.Nullify] = nil
}
_, errs := NewConfig(raw)
if tc.Err {
if errs == nil {
t.Fatalf("%q should error", desc)
}
} else {
if errs != nil {
t.Fatalf("%q should not error: %s", desc, errs)
}
}
}
}
func testConfig(config map[string]interface{}) map[string]interface{} {
raw := 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"},
"network": "c5ed8a14-3f21-4fa9-bd74-bb887fc0ed0d",
"service_offering": "a29c52b1-a83d-4123-a57d-4548befa47a0",
"template_os": "52d54d24-cef1-480b-b963-527703aa4ff9",
"zone": "a3b594d9-25e9-47c1-9c03-7a5fc61e3f43",
}
for k, v := range config {
raw[k] = v
}
return raw
}

56
builder/cloudstack/ssh.go Normal file
View File

@ -0,0 +1,56 @@
package cloudstack
import (
"fmt"
"io/ioutil"
"github.com/mitchellh/multistep"
packerssh "github.com/mitchellh/packer/communicator/ssh"
"github.com/xanzy/go-cloudstack/cloudstack"
"golang.org/x/crypto/ssh"
)
func commHost(state multistep.StateBag) (string, error) {
client := state.Get("client").(*cloudstack.CloudStackClient)
config := state.Get("config").(*Config)
if config.hostAddress == "" {
ipAddr, _, err := client.Address.GetPublicIpAddressByID(config.PublicIPAddress)
if err != nil {
return "", fmt.Errorf("Failed to retrieve IP address: %s", err)
}
config.hostAddress = ipAddr.Ipaddress
}
return config.hostAddress, nil
}
func sshConfig(state multistep.StateBag) (*ssh.ClientConfig, error) {
config := state.Get("config").(*Config)
clientConfig := &ssh.ClientConfig{
User: config.Comm.SSHUsername,
Auth: []ssh.AuthMethod{
ssh.Password(config.Comm.SSHPassword),
ssh.KeyboardInteractive(
packerssh.PasswordKeyboardInteractive(config.Comm.SSHPassword)),
},
}
if config.Comm.SSHPrivateKey != "" {
privateKey, err := ioutil.ReadFile(config.Comm.SSHPrivateKey)
if err != nil {
return nil, fmt.Errorf("Error loading configured private key file: %s", err)
}
signer, err := ssh.ParsePrivateKey([]byte(privateKey))
if err != nil {
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
}
clientConfig.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)}
}
return clientConfig, nil
}

View File

@ -0,0 +1,237 @@
package cloudstack
import (
"fmt"
"math/rand"
"strings"
"time"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"github.com/xanzy/go-cloudstack/cloudstack"
)
type stepSetupNetworking struct {
privatePort int
publicPort int
}
func (s *stepSetupNetworking) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*cloudstack.CloudStackClient)
config := state.Get("config").(*Config)
ui := state.Get("ui").(packer.Ui)
ui.Say("Setup networking...")
if config.UseLocalIPAddress {
ui.Message("Using the local IP address...")
ui.Message("Networking has been setup!")
return multistep.ActionContinue
}
// Generate a random public port used to configure our port forward.
rand.Seed(time.Now().UnixNano())
s.publicPort = 50000 + rand.Intn(10000)
// Set the currently configured port to be the private port.
s.privatePort = config.Comm.Port()
// Set the SSH or WinRM port to be the randomly generated public port.
switch config.Comm.Type {
case "ssh":
config.Comm.SSHPort = s.publicPort
case "winrm":
config.Comm.WinRMPort = s.publicPort
}
// Retrieve the instance ID from the previously saved state.
instanceID, ok := state.Get("instance_id").(string)
if !ok || instanceID == "" {
ui.Error("Could not retrieve instance_id from state!")
return multistep.ActionHalt
}
network, _, err := client.Network.GetNetworkByID(
config.Network,
cloudstack.WithProject(config.Project),
)
if err != nil {
ui.Error(fmt.Sprintf("Failed to retrieve the network object: %s", err))
return multistep.ActionHalt
}
if config.PublicIPAddress == "" {
ui.Message("Associating public IP address...")
p := client.Address.NewAssociateIpAddressParams()
if config.Project != "" {
p.SetProjectid(config.Project)
}
if network.Vpcid != "" {
p.SetVpcid(network.Vpcid)
} else {
p.SetNetworkid(network.Id)
}
// Associate a new public IP address.
ipAddr, err := client.Address.AssociateIpAddress(p)
if err != nil {
ui.Error(fmt.Sprintf("Failed to associate public IP address: %s", err))
return multistep.ActionHalt
}
// Set the IP address and it's ID.
config.PublicIPAddress = ipAddr.Id
config.hostAddress = ipAddr.Ipaddress
// Store the IP address ID.
state.Put("ip_address_id", ipAddr.Id)
}
ui.Message("Creating port forward...")
p := client.Firewall.NewCreatePortForwardingRuleParams(
config.PublicIPAddress,
s.privatePort,
"TCP",
s.publicPort,
instanceID,
)
// Configure the port forward.
p.SetNetworkid(network.Id)
p.SetOpenfirewall(false)
// Create the port forward.
forward, err := client.Firewall.CreatePortForwardingRule(p)
if err != nil {
ui.Error(fmt.Sprintf("Failed to create port forward: %s", err))
}
// Store the port forward ID.
state.Put("port_forward_id", forward.Id)
if network.Vpcid != "" {
ui.Message("Creating network ACL rule...")
if network.Aclid == "" {
ui.Error("Failed to configure the firewall: no ACL connected to the VPC network")
return multistep.ActionHalt
}
// Create a new parameter struct.
p := client.NetworkACL.NewCreateNetworkACLParams("TCP")
// Configure the network ACL rule.
p.SetAclid(network.Aclid)
p.SetAction("allow")
p.SetCidrlist(config.CIDRList)
p.SetStartport(s.publicPort)
p.SetEndport(s.publicPort)
p.SetTraffictype("ingress")
// Create the network ACL rule.
aclRule, err := client.NetworkACL.CreateNetworkACL(p)
if err != nil {
ui.Error(fmt.Sprintf("Failed to create network ACL rule: %s", err))
return multistep.ActionHalt
}
// Store the network ACL rule ID.
state.Put("network_acl_rule_id", aclRule.Id)
} else {
ui.Message("Creating firewall rule...")
// Create a new parameter struct.
p := client.Firewall.NewCreateFirewallRuleParams(config.PublicIPAddress, "TCP")
// Configure the firewall rule.
p.SetCidrlist(config.CIDRList)
p.SetStartport(s.publicPort)
p.SetEndport(s.publicPort)
fwRule, err := client.Firewall.CreateFirewallRule(p)
if err != nil {
ui.Error(fmt.Sprintf("Failed to create firewall rule: %s", err))
return multistep.ActionHalt
}
// Store the firewall rule ID.
state.Put("firewall_rule_id", fwRule.Id)
}
ui.Message("Networking has been setup!")
return multistep.ActionContinue
}
// Cleanup any resources that may have been created during the Run phase.
func (s *stepSetupNetworking) Cleanup(state multistep.StateBag) {
client := state.Get("client").(*cloudstack.CloudStackClient)
ui := state.Get("ui").(packer.Ui)
ui.Say("Cleanup networking...")
if fwRuleID, ok := state.Get("firewall_rule_id").(string); ok && fwRuleID != "" {
// Create a new parameter struct.
p := client.Firewall.NewDeleteFirewallRuleParams(fwRuleID)
ui.Message("Deleting firewal rule...")
if _, err := client.Firewall.DeleteFirewallRule(p); 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", fwRuleID)) {
ui.Error(fmt.Sprintf("Error deleting firewall rule: %s", err))
}
}
}
if aclRuleID, ok := state.Get("network_acl_rule_id").(string); ok && aclRuleID != "" {
// Create a new parameter struct.
p := client.NetworkACL.NewDeleteNetworkACLParams(aclRuleID)
ui.Message("Deleting network ACL rule...")
if _, err := client.NetworkACL.DeleteNetworkACL(p); 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", aclRuleID)) {
ui.Error(fmt.Sprintf("Error deleting network ACL rule: %s", err))
}
}
}
if forwardID, ok := state.Get("port_forward_id").(string); ok && forwardID != "" {
// Create a new parameter struct.
p := client.Firewall.NewDeletePortForwardingRuleParams(forwardID)
ui.Message("Deleting port forward...")
if _, err := client.Firewall.DeletePortForwardingRule(p); 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", forwardID)) {
ui.Error(fmt.Sprintf("Error deleting port forward: %s", err))
}
}
}
if ipAddrID, ok := state.Get("ip_address_id").(string); ok && ipAddrID != "" {
// Create a new parameter struct.
p := client.Address.NewDisassociateIpAddressParams(ipAddrID)
ui.Message("Releasing public IP address...")
if _, err := client.Address.DisassociateIpAddress(p); 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", ipAddrID)) {
ui.Error(fmt.Sprintf("Error releasing public IP address: %s", err))
}
}
}
ui.Message("Networking has been cleaned!")
return
}

View File

@ -0,0 +1,157 @@
package cloudstack
import (
"encoding/base64"
"fmt"
"strings"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"github.com/xanzy/go-cloudstack/cloudstack"
)
// stepCreateInstance represents a Packer build step that creates CloudStack instances.
type stepCreateInstance struct{}
// Run executes the Packer build step that creates a CloudStack instance.
func (s *stepCreateInstance) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*cloudstack.CloudStackClient)
config := state.Get("config").(*Config)
ui := state.Get("ui").(packer.Ui)
ui.Say("Creating instance...")
// Create a new parameter struct.
p := client.VirtualMachine.NewDeployVirtualMachineParams(
config.ServiceOffering,
config.instanceSource,
config.Zone,
)
// Configure the instance.
p.SetName(config.InstanceName)
p.SetDisplayname("Created by Packer")
// If we use an ISO, configure the disk offering.
if config.SourceISO != "" {
p.SetDiskofferingid(config.DiskOffering)
p.SetHypervisor(config.Hypervisor)
}
// If we use a template, set the root disk size.
if config.SourceTemplate != "" && config.DiskSize > 0 {
p.SetRootdisksize(config.DiskSize)
}
// Retrieve the zone object.
zone, _, err := client.Zone.GetZoneByID(config.Zone)
if err != nil {
ui.Error(err.Error())
return multistep.ActionHalt
}
if zone.Networktype == "Advanced" {
// Set the network ID's.
p.SetNetworkids([]string{config.Network})
}
// If there is a project supplied, set the project id.
if config.Project != "" {
p.SetProjectid(config.Project)
}
if config.UserData != "" {
ud, err := getUserData(config.UserData, config.HTTPGetOnly)
if err != nil {
ui.Error(err.Error())
return multistep.ActionHalt
}
p.SetUserdata(ud)
}
// Create the new instance.
instance, err := client.VirtualMachine.DeployVirtualMachine(p)
if err != nil {
ui.Error(fmt.Sprintf("Error creating new instance %s: %s", config.InstanceName, err))
return multistep.ActionHalt
}
ui.Message("Instance has been created!")
// Set the auto generated password if a password was not explicitly configured.
switch config.Comm.Type {
case "ssh":
if config.Comm.SSHPassword == "" {
config.Comm.SSHPassword = instance.Password
}
case "winrm":
if config.Comm.WinRMPassword == "" {
config.Comm.WinRMPassword = instance.Password
}
}
// Set the host address when using the local IP address to connect.
if config.UseLocalIPAddress {
config.hostAddress = instance.Nic[0].Ipaddress
}
// Store the instance ID so we can remove it later.
state.Put("instance_id", instance.Id)
return multistep.ActionContinue
}
// Cleanup any resources that may have been created during the Run phase.
func (s *stepCreateInstance) Cleanup(state multistep.StateBag) {
client := state.Get("client").(*cloudstack.CloudStackClient)
ui := state.Get("ui").(packer.Ui)
instanceID, ok := state.Get("instance_id").(string)
if !ok || instanceID == "" {
return
}
// Create a new parameter struct.
p := client.VirtualMachine.NewDestroyVirtualMachineParams(instanceID)
// Set expunge so the instance is completely removed
p.SetExpunge(true)
ui.Say("Deleting instance...")
if _, err := client.VirtualMachine.DestroyVirtualMachine(p); 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", instanceID)) {
return
}
ui.Error(fmt.Sprintf("Error destroying instance: %s", err))
}
ui.Message("Instance has been deleted!")
return
}
// getUserData returns the user data as a base64 encoded string.
func getUserData(userData string, httpGETOnly bool) (string, error) {
ud := base64.StdEncoding.EncodeToString([]byte(userData))
// deployVirtualMachine uses POST by default, so max userdata is 32K
maxUD := 32768
if httpGETOnly {
// deployVirtualMachine using GET instead, so max userdata is 2K
maxUD = 2048
}
if len(ud) > maxUD {
return "", fmt.Errorf(
"The supplied user_data contains %d bytes after encoding, "+
"this exeeds the limit of %d bytes", len(ud), maxUD)
}
return ud, nil
}

View File

@ -0,0 +1,103 @@
package cloudstack
import (
"fmt"
"time"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"github.com/xanzy/go-cloudstack/cloudstack"
)
type stepCreateTemplate struct{}
func (s *stepCreateTemplate) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*cloudstack.CloudStackClient)
config := state.Get("config").(*Config)
ui := state.Get("ui").(packer.Ui)
ui.Say(fmt.Sprintf("Creating template: %s", config.TemplateName))
// Retrieve the instance ID from the previously saved state.
instanceID, ok := state.Get("instance_id").(string)
if !ok || instanceID == "" {
ui.Error("Could not retrieve instance_id from state!")
return multistep.ActionHalt
}
// Create a new parameter struct.
p := client.Template.NewCreateTemplateParams(
config.TemplateDisplayText,
config.TemplateName,
config.TemplateOS,
)
// Configure the template according to the supplied config.
p.SetIsfeatured(config.TemplateFeatured)
p.SetIspublic(config.TemplatePublic)
p.SetIsdynamicallyscalable(config.TemplateScalable)
p.SetPasswordenabled(config.TemplatePasswordEnabled)
p.SetRequireshvm(config.TemplateRequiresHVM)
if config.Project != "" {
p.SetProjectid(config.Project)
}
if config.TemplateTag != "" {
p.SetTemplatetag(config.TemplateTag)
}
ui.Message("Retrieving the ROOT volume ID...")
volumeID, err := getRootVolumeID(client, instanceID)
if err != nil {
ui.Error(err.Error())
return multistep.ActionHalt
}
// Set the volume ID from which to create the template.
p.SetVolumeid(volumeID)
ui.Message("Creating the new template...")
template, err := client.Template.CreateTemplate(p)
if err != nil {
ui.Error(fmt.Sprintf("Error creating the new template %s: %s", config.TemplateName, err))
return multistep.ActionHalt
}
// This is kind of nasty, but it appears to be needed to prevent corrupt templates.
// When CloudStack says the template creation is done and you then delete the source
// volume shortly after, it seems to corrupt the newly created template. Giving it an
// additional 30 seconds to really finish up, seem to prevent that from happening.
time.Sleep(30 * time.Second)
ui.Message("Template has been created!")
// Store the template ID.
state.Put("template", template)
return multistep.ActionContinue
}
// Cleanup any resources that may have been created during the Run phase.
func (s *stepCreateTemplate) Cleanup(state multistep.StateBag) {
// Nothing to cleanup for this step.
}
func getRootVolumeID(client *cloudstack.CloudStackClient, instanceID string) (string, error) {
// Retrieve the virtual machine object.
p := client.Volume.NewListVolumesParams()
// Set the type and virtual machine ID
p.SetType("ROOT")
p.SetVirtualmachineid(instanceID)
volumes, err := client.Volume.ListVolumes(p)
if err != nil {
return "", fmt.Errorf("Failed to retrieve ROOT volume: %s", err)
}
if volumes.Count != 1 {
return "", fmt.Errorf("Could not find ROOT disk of instance %s", instanceID)
}
return volumes.Volumes[0].Id, nil
}

View File

@ -0,0 +1,166 @@
package cloudstack
import (
"fmt"
"io/ioutil"
"regexp"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"github.com/xanzy/go-cloudstack/cloudstack"
)
type stepPrepareConfig struct{}
func (s *stepPrepareConfig) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*cloudstack.CloudStackClient)
config := state.Get("config").(*Config)
ui := state.Get("ui").(packer.Ui)
ui.Say("Preparing config...")
var err error
var errs *packer.MultiError
// First get the project and zone UUID's so we can use them in other calls when needed.
if config.Project != "" && !isUUID(config.Project) {
config.Project, _, err = client.Project.GetProjectID(config.Project)
if err != nil {
errs = packer.MultiErrorAppend(errs, &retrieveErr{"project", config.Project, err})
}
}
if config.UserDataFile != "" {
userdata, err := ioutil.ReadFile(config.UserDataFile)
if err != nil {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("problem reading user data file: %s", err))
}
config.UserData = string(userdata)
}
if !isUUID(config.Zone) {
config.Zone, _, err = client.Zone.GetZoneID(config.Zone)
if err != nil {
errs = packer.MultiErrorAppend(errs, &retrieveErr{"zone", config.Zone, err})
}
}
// Then try to get the remaining UUID's.
if config.DiskOffering != "" && !isUUID(config.DiskOffering) {
config.DiskOffering, _, err = client.DiskOffering.GetDiskOfferingID(config.DiskOffering)
if err != nil {
errs = packer.MultiErrorAppend(errs, &retrieveErr{"disk offering", config.DiskOffering, err})
}
}
if config.PublicIPAddress != "" && !isUUID(config.PublicIPAddress) {
// Save the public IP address before replacing it with it's UUID.
config.hostAddress = config.PublicIPAddress
p := client.Address.NewListPublicIpAddressesParams()
p.SetIpaddress(config.PublicIPAddress)
if config.Project != "" {
p.SetProjectid(config.Project)
}
ipAddrs, err := client.Address.ListPublicIpAddresses(p)
if err != nil {
errs = packer.MultiErrorAppend(errs, &retrieveErr{"IP address", config.PublicIPAddress, err})
}
if err == nil && ipAddrs.Count != 1 {
errs = packer.MultiErrorAppend(errs, &retrieveErr{"IP address", config.PublicIPAddress, ipAddrs})
}
if err == nil && ipAddrs.Count == 1 {
config.PublicIPAddress = ipAddrs.PublicIpAddresses[0].Id
}
}
if !isUUID(config.Network) {
config.Network, _, err = client.Network.GetNetworkID(config.Network, cloudstack.WithProject(config.Project))
if err != nil {
errs = packer.MultiErrorAppend(errs, &retrieveErr{"network", config.Network, err})
}
}
if !isUUID(config.ServiceOffering) {
config.ServiceOffering, _, err = client.ServiceOffering.GetServiceOfferingID(config.ServiceOffering)
if err != nil {
errs = packer.MultiErrorAppend(errs, &retrieveErr{"service offering", config.ServiceOffering, err})
}
}
if config.SourceISO != "" {
if isUUID(config.SourceISO) {
config.instanceSource = config.SourceISO
} else {
config.instanceSource, _, err = client.ISO.GetIsoID(config.SourceISO, "executable", config.Zone)
if err != nil {
errs = packer.MultiErrorAppend(errs, &retrieveErr{"ISO", config.SourceISO, err})
}
}
}
if config.SourceTemplate != "" {
if isUUID(config.SourceTemplate) {
config.instanceSource = config.SourceTemplate
} else {
config.instanceSource, _, err = client.Template.GetTemplateID(config.SourceTemplate, "executable", config.Zone)
if err != nil {
errs = packer.MultiErrorAppend(errs, &retrieveErr{"template", config.SourceTemplate, err})
}
}
}
if !isUUID(config.TemplateOS) {
p := client.GuestOS.NewListOsTypesParams()
p.SetDescription(config.TemplateOS)
types, err := client.GuestOS.ListOsTypes(p)
if err != nil {
errs = packer.MultiErrorAppend(errs, &retrieveErr{"OS type", config.TemplateOS, err})
}
if err == nil && types.Count != 1 {
errs = packer.MultiErrorAppend(errs, &retrieveErr{"OS type", config.TemplateOS, types})
}
if err == nil && types.Count == 1 {
config.TemplateOS = types.OsTypes[0].Id
}
}
// This is needed because nil is not always nil. When returning *packer.MultiError(nil)
// as an error interface, that interface will no longer be equal to nil but it will be
// an interface with type *packer.MultiError and value nil which is different then a
// nil interface.
if errs != nil && len(errs.Errors) > 0 {
ui.Error(errs.Error())
return multistep.ActionHalt
}
ui.Message("Config has been prepared!")
return multistep.ActionContinue
}
func (s *stepPrepareConfig) Cleanup(state multistep.StateBag) {
// Nothing to cleanup for this step.
}
type retrieveErr struct {
name string
value string
result interface{}
}
func (e *retrieveErr) Error() string {
if err, ok := e.result.(error); ok {
e.result = err.Error()
}
return fmt.Sprintf("Error retrieving UUID of %s %s: %v", e.name, e.value, e.result)
}
var uuidRegex = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
func isUUID(uuid string) bool {
return uuidRegex.MatchString(uuid)
}

View File

@ -0,0 +1,45 @@
package cloudstack
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"github.com/xanzy/go-cloudstack/cloudstack"
)
type stepShutdownInstance struct{}
func (s *stepShutdownInstance) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*cloudstack.CloudStackClient)
config := state.Get("config").(*Config)
ui := state.Get("ui").(packer.Ui)
ui.Say("Shutting down instance...")
// Retrieve the instance ID from the previously saved state.
instanceID, ok := state.Get("instance_id").(string)
if !ok || instanceID == "" {
ui.Error("Could not retrieve instance_id from state!")
return multistep.ActionHalt
}
// Create a new parameter struct.
p := client.VirtualMachine.NewStopVirtualMachineParams(instanceID)
// Shutdown the virtual machine.
_, err := client.VirtualMachine.StopVirtualMachine(p)
if err != nil {
ui.Error(fmt.Sprintf("Error shutting down instance %s: %s", config.InstanceName, err))
return multistep.ActionHalt
}
ui.Message("Instance has been shutdown!")
return multistep.ActionContinue
}
// Cleanup any resources that may have been created during the Run phase.
func (s *stepShutdownInstance) Cleanup(state multistep.StateBag) {
// Nothing to cleanup for this step.
}

View File

@ -17,6 +17,7 @@ import (
amazonebsbuilder "github.com/mitchellh/packer/builder/amazon/ebs"
amazoninstancebuilder "github.com/mitchellh/packer/builder/amazon/instance"
azurearmbuilder "github.com/mitchellh/packer/builder/azure/arm"
cloudstackbuilder "github.com/mitchellh/packer/builder/cloudstack"
digitaloceanbuilder "github.com/mitchellh/packer/builder/digitalocean"
dockerbuilder "github.com/mitchellh/packer/builder/docker"
filebuilder "github.com/mitchellh/packer/builder/file"
@ -69,6 +70,7 @@ var Builders = map[string]packer.Builder{
"amazon-ebs": new(amazonebsbuilder.Builder),
"amazon-instance": new(amazoninstancebuilder.Builder),
"azure-arm": new(azurearmbuilder.Builder),
"cloudstack": new(cloudstackbuilder.Builder),
"digitalocean": new(digitaloceanbuilder.Builder),
"docker": new(dockerbuilder.Builder),
"file": new(filebuilder.Builder),

View File

@ -0,0 +1,151 @@
---
description: |
The `cloudstack` Packer builder is able to create new templates for use with
CloudStack. The builder takes either an ISO or an existing template as it's
source, runs any provisioning necessary on the instance after launching it
and then creates a new template from that instance.
layout: docs
page_title: CloudStack Builder
...
# CloudStack Builder
Type: `cloudstack`
The `cloudstack` Packer builder is able to create new templates for use with
[CloudStack](https://cloudstack.apache.org/). The builder takes either an ISO
or an existing template as it's source, runs any provisioning necessary on the
instance after launching it and then creates a new template from that instance.
The builder does *not* manage templates. Once a template is created, it is up
to you to use it or delete it.
## Configuration Reference
There are many configuration options available for the builder. They are
segmented below into two categories: required and optional parameters. Within
each category, the available configuration keys are alphabetized.
In addition to the options listed here, a
[communicator](/docs/templates/communicator.html) can be configured for this
builder.
### Required:
- `api_url` (string) - The CloudStack API endpoint we will connect to.
- `api_key` (string) - The API key used to sign all API requests.
- `cidr_list` (array) - List of CIDR's that will have access to the new
instance. This is needed in order for any provisioners to be able to
connect to the instance. Usually this will be the NAT address of your
current location. Only required when `use_local_ip_address` is `false`.
- `instance_name` (string) - The name of the instance. Defaults to
"packer-UUID" where UUID is dynamically generated.
- `network` (string) - The name or ID of the network to connect the instance
to.
- `secret_key` (string) - The secret key used to sign all API requests.
- `service_offering` (string) - The name or ID of the service offering used
for the instance.
- `soure_iso` (string) - The name or ID of an ISO that will be mounted before
booting the instance. This option is mutual exclusive with `source_template`.
- `source_template` (string) - The name or ID of the template used as base
template for the instance. This option is mutual explusive with `source_iso`.
- `template_name` (string) - The name of the new template. Defaults to
"packer-{{timestamp}}" where timestamp will be the current time.
- `template_display_text` (string) - The display text of the new template.
Defaults to the `template_name`.
- `template_os` (string) - The name or ID of the template OS for the new
template that will be created.
- `zone` (string) - The name or ID of the zone where the instance will be
created.
### Optional:
- `async_timeout` (int) - The time duration to wait for async calls to
finish. Defaults to 30m.
- `disk_offering` (string) - The name or ID of the disk offering used for the
instance. This option is only available (and also required) when using
`source_iso`.
- `disk_size` (int) - The size (in GB) of the root disk of the new instance.
This option is only available when using `source_template`.
- `http_get_only` (boolean) - Some cloud providers only allow HTTP GET calls to
their CloudStack API. If using such a provider, you need to set this to `true`
in order for the provider to only make GET calls and no POST calls.
- `hypervisor` (string) - The target hypervisor (e.g. `XenServer`, `KVM`) for
the new template. This option is required when using `source_iso`.
- `keypair` (string) - The name of the SSH key pair that will be used to
access the instance. The SSH key pair is assumed to be already available
within CloudStack.
- `project` (string) - The name or ID of the project to deploy the instance to.
- `public_ip_address` (string) - The public IP address or it's ID used for
connecting any provisioners to. If not provided, a temporary public IP
address will be associated and released during the Packer run.
- `ssl_no_verify` (boolean) - Set to `true` to skip SSL verification. Defaults
to `false`.
- `template_featured` (boolean) - Set to `true` to indicate that the template
is featured. Defaults to `false`.
- `template_public` (boolean) - Set to `true` to indicate that the template is
available for all accounts. Defaults to `false`.
- `template_password_enabled` (boolean) - Set to `true` to indicate the template
should be password enabled. Defaults to `false`.
- `template_requires_hvm` (boolean) - Set to `true` to indicate the template
requires hardware-assisted virtualization. Defaults to `false`.
- `template_scalable` (boolean) - Set to `true` to indicate that the template
contains tools to support dynamic scaling of VM cpu/memory. Defaults to `false`.
- `user_data` (string) - User data to launch with the instance.
- `use_local_ip_address` (boolean) - Set to `true` to indicate that the
provisioners should connect to the local IP address of the instance.
## Basic Example
Here is a basic example.
``` {.javascript}
{
"type": "cloudstack",
"api_url": "https://cloudstack.company.com/client/api",
"api_key": "YOUR_API_KEY",
"secret_key": "YOUR_SECRET_KEY",
"disk_offering": "Small - 20GB",
"cidr_list": ["0.0.0.0/0"]
"hypervisor": "KVM",
"network": "management",
"service_offering": "small",
"source_iso": "CentOS-7.0-1406-x86_64-Minimal",
"zone": "NL1",
"template_name": "Centos7-x86_64-KVM-Packer",
"template_display_text": "Centos7-x86_64 KVM Packer",
"template_featured": true,
"template_password_enabled": true,
"template_scalable": true,
"template_os": "Other PV (64-bit)"
}
```

View File

@ -45,7 +45,7 @@ description: Packer is a free and open source tool for creating golden images
<div class="col-md-6">
<h2 class="text-green text-center">Works Great With</h2>
<p>
Out of the box Packer comes with support to build images for Amazon EC2, DigitalOcean, Docker, Google Compute Engine, QEMU, VirtualBox, VMware, Microsoft Azure, and more. Support for more platforms is on the way, and anyone can add new platforms via plugins.
Out of the box Packer comes with support to build images for Amazon EC2, CloudStack, DigitalOcean, Docker, Google Compute Engine, Microsoft Azure, QEMU, VirtualBox, VMware, and more. Support for more platforms is on the way, and anyone can add new platforms via plugins.
</p>
</div>
<div class="col-md-6">

View File

@ -37,6 +37,9 @@ on supported configuration parameters and usage, please see the appropriate
[EC2](https://aws.amazon.com/ec2/), optionally distributed to
multiple regions.
- ***CloudStack***. Images for [CloudStack](https://cloudstack.apache.org/)
that can be used to start pre-configured CloudStack servers.
- ***DigitalOcean***. Snapshots for
[DigitalOcean](https://www.digitalocean.com/) that can be used to start a
pre-configured DigitalOcean instance of any size.

View File

@ -37,6 +37,7 @@
</li>
<li><a href="/docs/builders/amazon.html">Amazon EC2 (AMI)</a></li>
<li><a href="/docs/builders/azure.html">Azure Resource Manager</a></li>
<li><a href="/docs/builders/cloudstack.html">CloudStack</a></li>
<li><a href="/docs/builders/digitalocean.html">DigitalOcean</a></li>
<li><a href="/docs/builders/docker.html">Docker</a></li>
<li><a href="/docs/builders/file.html">File</a></li>