mirror of https://github.com/hashicorp/packer
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.
|
||||
}
|
||||
@ -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)"
|
||||
}
|
||||
```
|
||||
Loading…
Reference in new issue