Add a CloudStack Builder
This commit is contained in:
parent
ba00afd4f1
commit
dbf3bf56d4
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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.
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
```
|
|
@ -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">
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue