Commit 68fec501 authored by Chris Bednarski's avatar Chris Bednarski

Merge branch 'master' into f-vtolstov-compress

parents 3ac74bba 65863342
## 0.8.0 (unreleased) ## 0.8.0 (unreleased)
BACKWARDS INCOMPATIBILITIES:
* builder/digitalocean: no longer supports the v1 API which has been
deprecated for some time. Most configurations should continue to
work as long as you use the `api_token` field for auth.
* builder/digitalocean: `image`, `region`, and `size` are now required.
* builder/openstack: auth parameters have been changed to better
reflect OS terminology. Existing environment variables still work.
FEATURES: FEATURES:
* **New config function: `template_dir`**: The directory to the template * **New config function: `template_dir`**: The directory to the template
...@@ -8,11 +17,19 @@ FEATURES: ...@@ -8,11 +17,19 @@ FEATURES:
IMPROVEMENTS: IMPROVEMENTS:
* core: Interrupt handling for SIGTERM signal as well. [GH-1858] * core: Interrupt handling for SIGTERM signal as well. [GH-1858]
* builder/digitalocean: Save SSH key to pwd if debug mode is on. [GH-1829]
* builder/digitalocean: User data support [GH-2113]
* builder/parallels: Support Parallels Desktop 11 [GH-2199]
* builder/openstack: Add `rackconnect_wait` for Rackspace customers to wait for * builder/openstack: Add `rackconnect_wait` for Rackspace customers to wait for
RackConnect data to appear RackConnect data to appear
* buidler/openstakc: Add `ssh_interface` option for rackconnect for users that * buidler/openstack: Add `ssh_interface` option for rackconnect for users that
have prohibitive firewalls have prohibitive firewalls
* builder/openstack: Flavor names can be used as well as refs
* builder/openstack: Add `availability_zone` [GH-2016]
* builder/virtualbox: Added option: `ssh_skip_nat_mapping` to skip the
automatic port forward for SSH and to use the guest port directly. [GH-1078]
* builder/virtualbox: Added SCSI support * builder/virtualbox: Added SCSI support
* builder/vmware: Support for additional disks [GH-1382]
* command/push: Add `-name` flag for specifying name from CLI [GH-2042] * command/push: Add `-name` flag for specifying name from CLI [GH-2042]
* command/push: Push configuration in templates supports variables [GH-1861] * command/push: Push configuration in templates supports variables [GH-1861]
* post-processor/docker-save: Can be chained [GH-2179] * post-processor/docker-save: Can be chained [GH-2179]
...@@ -22,7 +39,10 @@ IMPROVEMENTS: ...@@ -22,7 +39,10 @@ IMPROVEMENTS:
BUG FIXES: BUG FIXES:
* core: Fix potential panic for post-processor plugin exits [GH-2098] * core: Fix potential panic for post-processor plugin exits [GH-2098]
* builder/amazon: Allow spaces in AMI names when using `clean_ami_name` [GH-2182]
* builder/amazon: Remove deprecated ec2-upload-bundle paramger [GH-1931] * builder/amazon: Remove deprecated ec2-upload-bundle paramger [GH-1931]
* builder/amazon: Use IAM Profile to upload bundle if provided [GH-1985]
* builder/amazon: Use correct exit code after SSH authentication failed [GH-2004]
* builder/amazon: Retry finding created instance for eventual * builder/amazon: Retry finding created instance for eventual
consistency. [GH-2129] consistency. [GH-2129]
* builder/amazon: If no AZ is specified, use AZ chosen automatically by * builder/amazon: If no AZ is specified, use AZ chosen automatically by
...@@ -31,8 +51,13 @@ BUG FIXES: ...@@ -31,8 +51,13 @@ BUG FIXES:
is deleted on cleanup. [GH-1801] is deleted on cleanup. [GH-1801]
* builder/amazon: AMI copy won't copy to the source region [GH-2123] * builder/amazon: AMI copy won't copy to the source region [GH-2123]
* builder/amazon: Validate AMI doesn't exist with name prior to build [GH-1774] * builder/amazon: Validate AMI doesn't exist with name prior to build [GH-1774]
* builder/amazon: Improved retry logic around waiting for instances. [GH-1764]
* builder/amazon: Fix issues with creating Block Devices. [GH-2195]
* builder/amazon/chroot: Retry waiting for disk attachments [GH-2046] * builder/amazon/chroot: Retry waiting for disk attachments [GH-2046]
* builder/amazon/instance: Use `-i` in sudo commands so PATH is inherited. [GH-1930] * builder/amazon/instance: Use `-i` in sudo commands so PATH is inherited. [GH-1930]
* builder/amazon/instance: Use `--region` flag for bundle upload command. [GH-1931]
* builder/digitalocean: Wait for droplet to unlock before changing state,
should lower the "pending event" errors.
* builder/digitalocean: Ignore invalid fields from the ever-changing v2 API * builder/digitalocean: Ignore invalid fields from the ever-changing v2 API
* builder/digitalocean: Private images can be used as a source [GH-1792] * builder/digitalocean: Private images can be used as a source [GH-1792]
* builder/docker: Fixed hang on prompt while copying script * builder/docker: Fixed hang on prompt while copying script
...@@ -46,12 +71,15 @@ BUG FIXES: ...@@ -46,12 +71,15 @@ BUG FIXES:
OS installers. [GH-1709] OS installers. [GH-1709]
* builder/virtualbox: Remove the floppy controller in addition to the * builder/virtualbox: Remove the floppy controller in addition to the
floppy disk. [GH-1879] floppy disk. [GH-1879]
* builder/virtualbox: Fixed regression where downloading ISO without a
".iso" extension didn't work. [GH-1839]
* builder/vmware: Add 100ms delay between keystrokes to avoid subtle * builder/vmware: Add 100ms delay between keystrokes to avoid subtle
timing issues in most cases. [GH-1663] timing issues in most cases. [GH-1663]
* builder/vmware: Bind HTTP server to IPv4, which is more compatible with * builder/vmware: Bind HTTP server to IPv4, which is more compatible with
OS installers. [GH-1709] OS installers. [GH-1709]
* builder/vmware: Case-insensitive match of MAC address to find IP [GH-1989] * builder/vmware: Case-insensitive match of MAC address to find IP [GH-1989]
* builder/vmware: More robust IP parsing from ifconfig output [GH-1999] * builder/vmware: More robust IP parsing from ifconfig output [GH-1999]
* builder/vmware: Nested output directories for ESXi work [GH-2174]
* command/validate: don't crash for invalid builds [GH-2139] * command/validate: don't crash for invalid builds [GH-2139]
* post-processor/atlas: Find common archive prefix for Windows [GH-1874] * post-processor/atlas: Find common archive prefix for Windows [GH-1874]
* post-processor/atlas: Fix index out of range panic [GH-1959] * post-processor/atlas: Fix index out of range panic [GH-1959]
...@@ -59,6 +87,8 @@ BUG FIXES: ...@@ -59,6 +87,8 @@ BUG FIXES:
* post-processor/vagrant-cloud: Don't delete version on error [GH-2014] * post-processor/vagrant-cloud: Don't delete version on error [GH-2014]
* provisioner/puppet-masterless: Allow manifest_file to be a directory * provisioner/puppet-masterless: Allow manifest_file to be a directory
* provisioner/salt-masterless: Add `--retcode-passthrough` to salt-call * provisioner/salt-masterless: Add `--retcode-passthrough` to salt-call
* provisioner/shell: chmod executable script to 0755, not 0777 [GH-1708]
* provisioner/shell: inline commands failing will fail the provisioner [GH-2069]
## 0.7.5 (December 9, 2014) ## 0.7.5 (December 9, 2014)
......
...@@ -29,13 +29,23 @@ func buildBlockDevices(b []BlockDevice) []*ec2.BlockDeviceMapping { ...@@ -29,13 +29,23 @@ func buildBlockDevices(b []BlockDevice) []*ec2.BlockDeviceMapping {
for _, blockDevice := range b { for _, blockDevice := range b {
ebsBlockDevice := &ec2.EBSBlockDevice{ ebsBlockDevice := &ec2.EBSBlockDevice{
SnapshotID: &blockDevice.SnapshotId,
Encrypted: &blockDevice.Encrypted,
IOPS: &blockDevice.IOPS,
VolumeType: &blockDevice.VolumeType, VolumeType: &blockDevice.VolumeType,
VolumeSize: &blockDevice.VolumeSize, VolumeSize: &blockDevice.VolumeSize,
DeleteOnTermination: &blockDevice.DeleteOnTermination, DeleteOnTermination: &blockDevice.DeleteOnTermination,
} }
// IOPS is only valid for SSD Volumes
if blockDevice.VolumeType != "" && blockDevice.VolumeType != "standard" && blockDevice.VolumeType != "gp2" {
ebsBlockDevice.IOPS = &blockDevice.IOPS
}
// You cannot specify Encrypted if you specify a Snapshot ID
if blockDevice.SnapshotId != "" {
ebsBlockDevice.SnapshotID = &blockDevice.SnapshotId
} else {
ebsBlockDevice.Encrypted = &blockDevice.Encrypted
}
mapping := &ec2.BlockDeviceMapping{ mapping := &ec2.BlockDeviceMapping{
EBS: ebsBlockDevice, EBS: ebsBlockDevice,
DeviceName: &blockDevice.DeviceName, DeviceName: &blockDevice.DeviceName,
......
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"testing" "testing"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awsutil"
"github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ec2"
) )
...@@ -28,11 +29,48 @@ func TestBlockDevice(t *testing.T) { ...@@ -28,11 +29,48 @@ func TestBlockDevice(t *testing.T) {
DeviceName: aws.String("/dev/sdb"), DeviceName: aws.String("/dev/sdb"),
VirtualName: aws.String("ephemeral0"), VirtualName: aws.String("ephemeral0"),
EBS: &ec2.EBSBlockDevice{ EBS: &ec2.EBSBlockDevice{
Encrypted: aws.Boolean(false),
SnapshotID: aws.String("snap-1234"), SnapshotID: aws.String("snap-1234"),
VolumeType: aws.String("standard"), VolumeType: aws.String("standard"),
VolumeSize: aws.Long(8), VolumeSize: aws.Long(8),
DeleteOnTermination: aws.Boolean(true), DeleteOnTermination: aws.Boolean(true),
},
},
},
{
Config: &BlockDevice{
DeviceName: "/dev/sdb",
VolumeSize: 8,
},
Result: &ec2.BlockDeviceMapping{
DeviceName: aws.String("/dev/sdb"),
VirtualName: aws.String(""),
EBS: &ec2.EBSBlockDevice{
Encrypted: aws.Boolean(false),
VolumeType: aws.String(""),
VolumeSize: aws.Long(8),
DeleteOnTermination: aws.Boolean(false),
},
},
},
{
Config: &BlockDevice{
DeviceName: "/dev/sdb",
VirtualName: "ephemeral0",
VolumeType: "io1",
VolumeSize: 8,
DeleteOnTermination: true,
IOPS: 1000,
},
Result: &ec2.BlockDeviceMapping{
DeviceName: aws.String("/dev/sdb"),
VirtualName: aws.String("ephemeral0"),
EBS: &ec2.EBSBlockDevice{
Encrypted: aws.Boolean(false),
VolumeType: aws.String("io1"),
VolumeSize: aws.Long(8),
DeleteOnTermination: aws.Boolean(true),
IOPS: aws.Long(1000), IOPS: aws.Long(1000),
}, },
}, },
...@@ -48,11 +86,11 @@ func TestBlockDevice(t *testing.T) { ...@@ -48,11 +86,11 @@ func TestBlockDevice(t *testing.T) {
expected := []*ec2.BlockDeviceMapping{tc.Result} expected := []*ec2.BlockDeviceMapping{tc.Result}
got := blockDevices.BuildAMIDevices() got := blockDevices.BuildAMIDevices()
if !reflect.DeepEqual(expected, got) { if !reflect.DeepEqual(expected, got) {
t.Fatalf("bad: %#v", expected) t.Fatalf("Bad block device, \nexpected: %s\n\ngot: %s", awsutil.StringValue(expected), awsutil.StringValue(got))
} }
if !reflect.DeepEqual(expected, blockDevices.BuildLaunchDevices()) { if !reflect.DeepEqual(expected, blockDevices.BuildLaunchDevices()) {
t.Fatalf("bad: %#v", expected) t.Fatalf("Bad block device, \nexpected: %s\n\ngot: %s", awsutil.StringValue(expected), awsutil.StringValue(blockDevices.BuildLaunchDevices()))
} }
} }
} }
...@@ -67,10 +67,10 @@ func AMIStateRefreshFunc(conn *ec2.EC2, imageId string) StateRefreshFunc { ...@@ -67,10 +67,10 @@ func AMIStateRefreshFunc(conn *ec2.EC2, imageId string) StateRefreshFunc {
// InstanceStateRefreshFunc returns a StateRefreshFunc that is used to watch // InstanceStateRefreshFunc returns a StateRefreshFunc that is used to watch
// an EC2 instance. // an EC2 instance.
func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) StateRefreshFunc { func InstanceStateRefreshFunc(conn *ec2.EC2, instanceId string) StateRefreshFunc {
return func() (interface{}, string, error) { return func() (interface{}, string, error) {
resp, err := conn.DescribeInstances(&ec2.DescribeInstancesInput{ resp, err := conn.DescribeInstances(&ec2.DescribeInstancesInput{
InstanceIDs: []*string{i.InstanceID}, InstanceIDs: []*string{&instanceId},
}) })
if err != nil { if err != nil {
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidInstanceID.NotFound" { if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidInstanceID.NotFound" {
...@@ -91,7 +91,7 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) StateRefreshFunc { ...@@ -91,7 +91,7 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) StateRefreshFunc {
return nil, "", nil return nil, "", nil
} }
i = resp.Reservations[0].Instances[0] i := resp.Reservations[0].Instances[0]
return i, *i.State.Name, nil return i, *i.State.Name, nil
} }
} }
......
...@@ -223,31 +223,12 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi ...@@ -223,31 +223,12 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi
instanceId = *spotResp.SpotInstanceRequests[0].InstanceID instanceId = *spotResp.SpotInstanceRequests[0].InstanceID
} }
instanceResp, err := ec2conn.DescribeInstances(&ec2.DescribeInstancesInput{ ui.Message(fmt.Sprintf("Instance ID: %s", instanceId))
InstanceIDs: []*string{&instanceId}}) ui.Say(fmt.Sprintf("Waiting for instance (%v) to become ready...", instanceId))
for i := 0; i < 10; i++ {
if err == nil {
break
}
time.Sleep(3 * time.Second)
instanceResp, err = ec2conn.DescribeInstances(&ec2.DescribeInstancesInput{
InstanceIDs: []*string{&instanceId}})
}
if err != nil {
err := fmt.Errorf("Error finding source instance (%s): %s", instanceId, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
s.instance = instanceResp.Reservations[0].Instances[0]
ui.Message(fmt.Sprintf("Instance ID: %s", *s.instance.InstanceID))
ui.Say(fmt.Sprintf("Waiting for instance (%s) to become ready...", *s.instance.InstanceID))
stateChange := StateChangeConf{ stateChange := StateChangeConf{
Pending: []string{"pending"}, Pending: []string{"pending"},
Target: "running", Target: "running",
Refresh: InstanceStateRefreshFunc(ec2conn, s.instance), Refresh: InstanceStateRefreshFunc(ec2conn, instanceId),
StepState: state, StepState: state,
} }
latestInstance, err := WaitForState(&stateChange) latestInstance, err := WaitForState(&stateChange)
...@@ -329,7 +310,7 @@ func (s *StepRunSourceInstance) Cleanup(state multistep.StateBag) { ...@@ -329,7 +310,7 @@ func (s *StepRunSourceInstance) Cleanup(state multistep.StateBag) {
} }
stateChange := StateChangeConf{ stateChange := StateChangeConf{
Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"}, Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"},
Refresh: InstanceStateRefreshFunc(ec2conn, s.instance), Refresh: InstanceStateRefreshFunc(ec2conn, *s.instance.InstanceID),
Target: "terminated", Target: "terminated",
} }
......
...@@ -20,7 +20,7 @@ func isalphanumeric(b byte) bool { ...@@ -20,7 +20,7 @@ func isalphanumeric(b byte) bool {
// Clean up AMI name by replacing invalid characters with "-" // Clean up AMI name by replacing invalid characters with "-"
func templateCleanAMIName(s string) string { func templateCleanAMIName(s string) string {
allowed := []byte{'(', ')', ',', '/', '-', '_'} allowed := []byte{'(', ')', ',', '/', '-', '_', ' '}
b := []byte(s) b := []byte(s)
newb := make([]byte, len(b)) newb := make([]byte, len(b))
for i, c := range b { for i, c := range b {
......
...@@ -5,8 +5,8 @@ import ( ...@@ -5,8 +5,8 @@ import (
) )
func TestAMITemplatePrepare_clean(t *testing.T) { func TestAMITemplatePrepare_clean(t *testing.T) {
origName := "AMZamz09(),/-_:&^$%" origName := "AMZamz09(),/-_:&^ $%"
expected := "AMZamz09(),/-_-----" expected := "AMZamz09(),/-_--- --"
name := templateCleanAMIName(origName) name := templateCleanAMIName(origName)
......
...@@ -40,7 +40,7 @@ func (s *stepStopInstance) Run(state multistep.StateBag) multistep.StepAction { ...@@ -40,7 +40,7 @@ func (s *stepStopInstance) Run(state multistep.StateBag) multistep.StepAction {
stateChange := awscommon.StateChangeConf{ stateChange := awscommon.StateChangeConf{
Pending: []string{"running", "stopping"}, Pending: []string{"running", "stopping"},
Target: "stopped", Target: "stopped",
Refresh: awscommon.InstanceStateRefreshFunc(ec2conn, instance), Refresh: awscommon.InstanceStateRefreshFunc(ec2conn, *instance.InstanceID),
StepState: state, StepState: state,
} }
_, err = awscommon.WaitForState(&stateChange) _, err = awscommon.WaitForState(&stateChange)
......
...@@ -73,15 +73,25 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { ...@@ -73,15 +73,25 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
} }
if b.config.BundleUploadCommand == "" { if b.config.BundleUploadCommand == "" {
b.config.BundleUploadCommand = "sudo -i -n ec2-upload-bundle " + if b.config.IamInstanceProfile != "" {
"-b {{.BucketName}} " + b.config.BundleUploadCommand = "sudo -i -n ec2-upload-bundle " +
"-m {{.ManifestPath}} " + "-b {{.BucketName}} " +
"-a {{.AccessKey}} " + "-m {{.ManifestPath}} " +
"-s {{.SecretKey}} " + "-d {{.BundleDirectory}} " +
"-d {{.BundleDirectory}} " + "--batch " +
"--batch " + "--region {{.Region}} " +
"--region {{.Region}} " + "--retry"
"--retry" } else {
b.config.BundleUploadCommand = "sudo -i -n ec2-upload-bundle " +
"-b {{.BucketName}} " +
"-m {{.ManifestPath}} " +
"-a {{.AccessKey}} " +
"-s {{.SecretKey}} " +
"-d {{.BundleDirectory}} " +
"--batch " +
"--region {{.Region}} " +
"--retry"
}
} }
if b.config.BundleVolCommand == "" { if b.config.BundleVolCommand == "" {
......
// All of the methods used to communicate with the digital_ocean API
// are here. Their API is on a path to V2, so just plain JSON is used
// in place of a proper client library for now.
package digitalocean
type Region struct {
Slug string `json:"slug"`
Name string `json:"name"`
// v1 only
Id uint `json:"id,omitempty"`
// v2 only
Sizes []string `json:"sizes,omitempty"`
Available bool `json:"available,omitempty"`
Features []string `json:"features,omitempty"`
}
type RegionsResp struct {
Regions []Region
}
type Size struct {
Slug string `json:"slug"`
// v1 only
Id uint `json:"id,omitempty"`
Name string `json:"name,omitempty"`
// v2 only
Memory uint `json:"memory,omitempty"`
VCPUS uint `json:"vcpus,omitempty"`
Disk uint `json:"disk,omitempty"`
Transfer float64 `json:"transfer,omitempty"`
PriceMonthly float64 `json:"price_monthly,omitempty"`
PriceHourly float64 `json:"price_hourly,omitempty"`
}
type SizesResp struct {
Sizes []Size
}
type Image struct {
Id uint `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
Distribution string `json:"distribution"`
// v2 only
Public bool `json:"public,omitempty"`
ActionIds []string `json:"action_ids,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
}
type ImagesResp struct {
Images []Image
}
type DigitalOceanClient interface {
CreateKey(string, string) (uint, error)
DestroyKey(uint) error
CreateDroplet(string, string, string, string, uint, bool) (uint, error)
DestroyDroplet(uint) error
PowerOffDroplet(uint) error
ShutdownDroplet(uint) error
CreateSnapshot(uint, string) error
Images() ([]Image, error)
DestroyImage(uint) error
DropletStatus(uint) (string, string, error)
Image(string) (Image, error)
Regions() ([]Region, error)
Region(string) (Region, error)
Sizes() ([]Size, error)
Size(string) (Size, error)
}
// All of the methods used to communicate with the digital_ocean API
// are here. Their API is on a path to V2, so just plain JSON is used
// in place of a proper client library for now.
package digitalocean
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/mitchellh/mapstructure"
)
type DigitalOceanClientV1 struct {
// The http client for communicating
client *http.Client
// Credentials
ClientID string
APIKey string
// The base URL of the API
APIURL string
}
// Creates a new client for communicating with DO
func DigitalOceanClientNewV1(client string, key string, url string) *DigitalOceanClientV1 {
c := &DigitalOceanClientV1{
client: &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
},
},
APIURL: url,
ClientID: client,
APIKey: key,
}
return c
}
// Creates an SSH Key and returns it's id
func (d DigitalOceanClientV1) CreateKey(name string, pub string) (uint, error) {
params := url.Values{}
params.Set("name", name)
params.Set("ssh_pub_key", pub)
body, err := NewRequestV1(d, "ssh_keys/new", params)
if err != nil {
return 0, err
}
// Read the SSH key's ID we just created
key := body["ssh_key"].(map[string]interface{})
keyId := key["id"].(float64)
return uint(keyId), nil
}
// Destroys an SSH key
func (d DigitalOceanClientV1) DestroyKey(id uint) error {
path := fmt.Sprintf("ssh_keys/%v/destroy", id)
_, err := NewRequestV1(d, path, url.Values{})
return err
}
// Creates a droplet and returns it's id
func (d DigitalOceanClientV1) CreateDroplet(name string, size string, image string, region string, keyId uint, privateNetworking bool) (uint, error) {
params := url.Values{}
params.Set("name", name)
found_size, err := d.Size(size)
if err != nil {
return 0, fmt.Errorf("Invalid size or lookup failure: '%s': %s", size, err)
}
found_image, err := d.Image(image)
if err != nil {
return 0, fmt.Errorf("Invalid image or lookup failure: '%s': %s", image, err)
}
found_region, err := d.Region(region)
if err != nil {
return 0, fmt.Errorf("Invalid region or lookup failure: '%s': %s", region, err)
}
params.Set("size_slug", found_size.Slug)
params.Set("image_slug", found_image.Slug)
params.Set("region_slug", found_region.Slug)
params.Set("ssh_key_ids", fmt.Sprintf("%v", keyId))
params.Set("private_networking", fmt.Sprintf("%v", privateNetworking))
body, err := NewRequestV1(d, "droplets/new", params)
if err != nil {
return 0, err
}
// Read the Droplets ID
droplet := body["droplet"].(map[string]interface{})
dropletId := droplet["id"].(float64)
return uint(dropletId), err
}
// Destroys a droplet
func (d DigitalOceanClientV1) DestroyDroplet(id uint) error {
path := fmt.Sprintf("droplets/%v/destroy", id)
_, err := NewRequestV1(d, path, url.Values{})
return err
}
// Powers off a droplet
func (d DigitalOceanClientV1) PowerOffDroplet(id uint) error {
path := fmt.Sprintf("droplets/%v/power_off", id)
_, err := NewRequestV1(d, path, url.Values{})
return err
}
// Shutsdown a droplet. This is a "soft" shutdown.
func (d DigitalOceanClientV1) ShutdownDroplet(id uint) error {
path := fmt.Sprintf("droplets/%v/shutdown", id)
_, err := NewRequestV1(d, path, url.Values{})
return err
}
// Creates a snaphot of a droplet by it's ID
func (d DigitalOceanClientV1) CreateSnapshot(id uint, name string) error {
path := fmt.Sprintf("droplets/%v/snapshot", id)
params := url.Values{}
params.Set("name", name)
_, err := NewRequestV1(d, path, params)
return err
}
// Returns all available images.
func (d DigitalOceanClientV1) Images() ([]Image, error) {
resp, err := NewRequestV1(d, "images", url.Values{})
if err != nil {
return nil, err
}
var result ImagesResp
if err := mapstructure.Decode(resp, &result); err != nil {
return nil, err
}
return result.Images, nil
}
// Destroys an image by its ID.
func (d DigitalOceanClientV1) DestroyImage(id uint) error {
path := fmt.Sprintf("images/%d/destroy", id)
_, err := NewRequestV1(d, path, url.Values{})
return err
}
// Returns DO's string representation of status "off" "new" "active" etc.
func (d DigitalOceanClientV1) DropletStatus(id uint) (string, string, error) {
path := fmt.Sprintf("droplets/%v", id)
body, err := NewRequestV1(d, path, url.Values{})
if err != nil {
return "", "", err
}
var ip string
// Read the droplet's "status"
droplet := body["droplet"].(map[string]interface{})
status := droplet["status"].(string)
if droplet["ip_address"] != nil {
ip = droplet["ip_address"].(string)
}
return ip, status, err
}
// Sends an api request and returns a generic map[string]interface of
// the response.
func NewRequestV1(d DigitalOceanClientV1, path string, params url.Values) (map[string]interface{}, error) {
client := d.client
// Add the authentication parameters
params.Set("client_id", d.ClientID)
params.Set("api_key", d.APIKey)
url := fmt.Sprintf("%s/%s?%s", d.APIURL, path, params.Encode())
// Do some basic scrubbing so sensitive information doesn't appear in logs
scrubbedUrl := strings.Replace(url, d.ClientID, "CLIENT_ID", -1)
scrubbedUrl = strings.Replace(scrubbedUrl, d.APIKey, "API_KEY", -1)
log.Printf("sending new request to digitalocean: %s", scrubbedUrl)
var lastErr error
for attempts := 1; attempts < 10; attempts++ {
resp, err := client.Get(url)
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, err
}
log.Printf("response from digitalocean: %s", body)
var decodedResponse map[string]interface{}
err = json.Unmarshal(body, &decodedResponse)
if err != nil {
err = errors.New(fmt.Sprintf("Failed to decode JSON response (HTTP %v) from DigitalOcean: %s",
resp.StatusCode, body))
return decodedResponse, err
}
// Check for errors sent by digitalocean
status := decodedResponse["status"].(string)
if status == "OK" {
return decodedResponse, nil
}
if status == "ERROR" {
statusRaw, ok := decodedResponse["error_message"]
if ok {
status = statusRaw.(string)
} else {
status = fmt.Sprintf(
"Unknown error. Full response body: %s", body)
}
}
lastErr = errors.New(fmt.Sprintf("Received error from DigitalOcean (%d): %s",
resp.StatusCode, status))
log.Println(lastErr)
if strings.Contains(status, "a pending event") {
// Retry, DigitalOcean sends these dumb "pending event"
// errors all the time.
time.Sleep(5 * time.Second)
continue
}
// Some other kind of error. Just return.
return decodedResponse, lastErr
}
return nil, lastErr
}
func (d DigitalOceanClientV1) Image(slug_or_name_or_id string) (Image, error) {
images, err := d.Images()
if err != nil {
return Image{}, err
}
for _, image := range images {
if strings.EqualFold(image.Slug, slug_or_name_or_id) {
return image, nil
}
}
for _, image := range images {
if strings.EqualFold(image.Name, slug_or_name_or_id) {
return image, nil
}
}
for _, image := range images {
id, err := strconv.Atoi(slug_or_name_or_id)
if err == nil {
if image.Id == uint(id) {
return image, nil
}
}
}
err = errors.New(fmt.Sprintf("Unknown image '%v'", slug_or_name_or_id))
return Image{}, err
}
// Returns all available regions.
func (d DigitalOceanClientV1) Regions() ([]Region, error) {
resp, err := NewRequestV1(d, "regions", url.Values{})
if err != nil {
return nil, err
}
var result RegionsResp
if err := mapstructure.Decode(resp, &result); err != nil {
return nil, err
}
return result.Regions, nil
}
func (d DigitalOceanClientV1) Region(slug_or_name_or_id string) (Region, error) {
regions, err := d.Regions()
if err != nil {
return Region{}, err
}
for _, region := range regions {
if strings.EqualFold(region.Slug, slug_or_name_or_id) {
return region, nil
}
}
for _, region := range regions {
if strings.EqualFold(region.Name, slug_or_name_or_id) {
return region, nil
}
}
for _, region := range regions {
id, err := strconv.Atoi(slug_or_name_or_id)
if err == nil {
if region.Id == uint(id) {
return region, nil
}
}
}
err = errors.New(fmt.Sprintf("Unknown region '%v'", slug_or_name_or_id))
return Region{}, err
}
// Returns all available sizes.
func (d DigitalOceanClientV1) Sizes() ([]Size, error) {
resp, err := NewRequestV1(d, "sizes", url.Values{})
if err != nil {
return nil, err
}
var result SizesResp
if err := mapstructure.Decode(resp, &result); err != nil {
return nil, err
}
return result.Sizes, nil
}
func (d DigitalOceanClientV1) Size(slug_or_name_or_id string) (Size, error) {
sizes, err := d.Sizes()
if err != nil {
return Size{}, err
}
for _, size := range sizes {
if strings.EqualFold(size.Slug, slug_or_name_or_id) {
return size, nil
}
}
for _, size := range sizes {
if strings.EqualFold(size.Name, slug_or_name_or_id) {
return size, nil
}
}
for _, size := range sizes {
id, err := strconv.Atoi(slug_or_name_or_id)
if err == nil {
if size.Id == uint(id) {
return size, nil
}
}
}
err = errors.New(fmt.Sprintf("Unknown size '%v'", slug_or_name_or_id))
return Size{}, err
}
This diff is collapsed.
...@@ -4,6 +4,8 @@ import ( ...@@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"log" "log"
"strconv" "strconv"
"github.com/digitalocean/godo"
) )
type Artifact struct { type Artifact struct {
...@@ -11,13 +13,13 @@ type Artifact struct { ...@@ -11,13 +13,13 @@ type Artifact struct {
snapshotName string snapshotName string
// The ID of the image // The ID of the image
snapshotId uint snapshotId int
// The name of the region // The name of the region
regionName string regionName string
// The client for making API calls // The client for making API calls
client DigitalOceanClient client *godo.Client
} }
func (*Artifact) BuilderId() string { func (*Artifact) BuilderId() string {
...@@ -43,5 +45,6 @@ func (a *Artifact) State(name string) interface{} { ...@@ -43,5 +45,6 @@ func (a *Artifact) State(name string) interface{} {
func (a *Artifact) Destroy() error { func (a *Artifact) Destroy() error {
log.Printf("Destroying image: %d (%s)", a.snapshotId, a.snapshotName) log.Printf("Destroying image: %d (%s)", a.snapshotId, a.snapshotName)
return a.client.DestroyImage(a.snapshotId) _, err := a.client.Images.Delete(a.snapshotId)
return err
} }
...@@ -4,208 +4,39 @@ ...@@ -4,208 +4,39 @@
package digitalocean package digitalocean
import ( import (
"errors"
"fmt" "fmt"
"log" "log"
"os"
"time" "time"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate" "golang.org/x/oauth2"
) )
// see https://api.digitalocean.com/images/?client_id=[client_id]&api_key=[api_key]
// name="Ubuntu 12.04.4 x64", id=6374128,
const DefaultImage = "ubuntu-12-04-x64"
// see https://api.digitalocean.com/regions/?client_id=[client_id]&api_key=[api_key]
// name="New York 3", id=8
const DefaultRegion = "nyc3"
// see https://api.digitalocean.com/sizes/?client_id=[client_id]&api_key=[api_key]
// name="512MB", id=66 (the smallest droplet size)
const DefaultSize = "512mb"
// The unique id for the builder // The unique id for the builder
const BuilderId = "pearkes.digitalocean" const BuilderId = "pearkes.digitalocean"
// Configuration tells the builder the credentials
// to use while communicating with DO and describes the image
// you are creating
type Config struct {
common.PackerConfig `mapstructure:",squash"`
ClientID string `mapstructure:"client_id"`
APIKey string `mapstructure:"api_key"`
APIURL string `mapstructure:"api_url"`
APIToken string `mapstructure:"api_token"`
RegionID uint `mapstructure:"region_id"`
SizeID uint `mapstructure:"size_id"`
ImageID uint `mapstructure:"image_id"`
Region string `mapstructure:"region"`
Size string `mapstructure:"size"`
Image string `mapstructure:"image"`
PrivateNetworking bool `mapstructure:"private_networking"`
SnapshotName string `mapstructure:"snapshot_name"`
DropletName string `mapstructure:"droplet_name"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPort uint `mapstructure:"ssh_port"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
RawStateTimeout string `mapstructure:"state_timeout"`
// These are unexported since they're set by other fields
// being set.
sshTimeout time.Duration
stateTimeout time.Duration
ctx *interpolate.Context
}
type Builder struct { type Builder struct {
config Config config Config
runner multistep.Runner runner multistep.Runner
} }
func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
err := config.Decode(&b.config, &config.DecodeOpts{ c, warnings, errs := NewConfig(raws...)
Interpolate: true, if errs != nil {
}, raws...) return warnings, errs
if err != nil {
return nil, err
}
// Optional configuration with defaults
if b.config.APIKey == "" {
// Default to environment variable for api_key, if it exists
b.config.APIKey = os.Getenv("DIGITALOCEAN_API_KEY")
}
if b.config.ClientID == "" {
// Default to environment variable for client_id, if it exists
b.config.ClientID = os.Getenv("DIGITALOCEAN_CLIENT_ID")
}
if b.config.APIURL == "" {
// Default to environment variable for api_url, if it exists
b.config.APIURL = os.Getenv("DIGITALOCEAN_API_URL")
}
if b.config.APIToken == "" {
// Default to environment variable for api_token, if it exists
b.config.APIToken = os.Getenv("DIGITALOCEAN_API_TOKEN")
}
if b.config.Region == "" {
if b.config.RegionID != 0 {
b.config.Region = fmt.Sprintf("%v", b.config.RegionID)
} else {
b.config.Region = DefaultRegion
}
}
if b.config.Size == "" {
if b.config.SizeID != 0 {
b.config.Size = fmt.Sprintf("%v", b.config.SizeID)
} else {
b.config.Size = DefaultSize
}
}
if b.config.Image == "" {
if b.config.ImageID != 0 {
b.config.Image = fmt.Sprintf("%v", b.config.ImageID)
} else {
b.config.Image = DefaultImage
}
}
if b.config.SnapshotName == "" {
// Default to packer-{{ unix timestamp (utc) }}
b.config.SnapshotName = "packer-{{timestamp}}"
}
if b.config.DropletName == "" {
// Default to packer-[time-ordered-uuid]
b.config.DropletName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
} }
b.config = *c
if b.config.SSHUsername == "" {
// Default to "root". You can override this if your
// SourceImage has a different user account then the DO default
b.config.SSHUsername = "root"
}
if b.config.SSHPort == 0 {
// Default to port 22 per DO default
b.config.SSHPort = 22
}
if b.config.RawSSHTimeout == "" {
// Default to 1 minute timeouts
b.config.RawSSHTimeout = "1m"
}
if b.config.RawStateTimeout == "" {
// Default to 6 minute timeouts waiting for
// desired state. i.e waiting for droplet to become active
b.config.RawStateTimeout = "6m"
}
var errs *packer.MultiError
if b.config.APIToken == "" {
// Required configurations that will display errors if not set
if b.config.ClientID == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("a client_id for v1 auth or api_token for v2 auth must be specified"))
}
if b.config.APIKey == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("a api_key for v1 auth or api_token for v2 auth must be specified"))
}
}
if b.config.APIURL == "" {
b.config.APIURL = "https://api.digitalocean.com"
}
sshTimeout, err := time.ParseDuration(b.config.RawSSHTimeout)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
}
b.config.sshTimeout = sshTimeout
stateTimeout, err := time.ParseDuration(b.config.RawStateTimeout)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing state_timeout: %s", err))
}
b.config.stateTimeout = stateTimeout
if errs != nil && len(errs.Errors) > 0 {
return nil, errs
}
common.ScrubConfig(b.config, b.config.ClientID, b.config.APIKey)
return nil, nil return nil, nil
} }
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
var client DigitalOceanClient client := godo.NewClient(oauth2.NewClient(oauth2.NoContext, &apiTokenSource{
// Initialize the DO API client AccessToken: b.config.APIToken,
if b.config.APIToken == "" { }))
client = DigitalOceanClientNewV1(b.config.ClientID, b.config.APIKey, b.config.APIURL)
} else {
client = DigitalOceanClientNewV2(b.config.APIToken, b.config.APIURL)
}
// Set up the state // Set up the state
state := new(multistep.BasicStateBag) state := new(multistep.BasicStateBag)
...@@ -216,7 +47,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -216,7 +47,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
// Build the steps // Build the steps
steps := []multistep.Step{ steps := []multistep.Step{
new(stepCreateSSHKey), &stepCreateSSHKey{
Debug: b.config.PackerDebug,
DebugKeyPath: fmt.Sprintf("do_%s.pem", b.config.PackerBuildName),
},
new(stepCreateDroplet), new(stepCreateDroplet),
new(stepDropletInfo), new(stepDropletInfo),
&common.StepConnectSSH{ &common.StepConnectSSH{
...@@ -252,26 +86,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -252,26 +86,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
return nil, nil return nil, nil
} }
sregion := state.Get("region")
var region string
if sregion != nil {
region = sregion.(string)
} else {
region = fmt.Sprintf("%v", state.Get("region_id").(uint))
}
found_region, err := client.Region(region)
if err != nil {
return nil, err
}
artifact := &Artifact{ artifact := &Artifact{
snapshotName: state.Get("snapshot_name").(string), snapshotName: state.Get("snapshot_name").(string),
snapshotId: state.Get("snapshot_image_id").(uint), snapshotId: state.Get("snapshot_image_id").(int),
regionName: found_region.Name, regionName: state.Get("region").(string),
client: client, client: client,
} }
......
package digitalocean
import (
"os"
"testing"
builderT "github.com/mitchellh/packer/helper/builder/testing"
)
func TestBuilderAcc_basic(t *testing.T) {
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Builder: &Builder{},
Template: testBuilderAccBasic,
})
}
func testAccPreCheck(t *testing.T) {
if v := os.Getenv("DIGITALOCEAN_API_TOKEN"); v == "" {
t.Fatal("DIGITALOCEAN_API_TOKEN must be set for acceptance tests")
}
}
const testBuilderAccBasic = `
{
"builders": [{
"type": "test",
"region": "nyc2",
"size": "512mb",
"image": "ubuntu-12-04-x64"
}]
}
`
package digitalocean package digitalocean
import ( import (
"github.com/mitchellh/packer/packer"
"os"
"strconv" "strconv"
"testing" "testing"
)
func init() { "github.com/mitchellh/packer/packer"
// Clear out the credential env vars )
os.Setenv("DIGITALOCEAN_API_KEY", "")
os.Setenv("DIGITALOCEAN_CLIENT_ID", "")
}
func testConfig() map[string]interface{} { func testConfig() map[string]interface{} {
return map[string]interface{}{ return map[string]interface{}{
"client_id": "foo", "api_token": "bar",
"api_key": "bar", "region": "nyc2",
"size": "512mb",
"image": "foo",
} }
} }
...@@ -43,90 +39,6 @@ func TestBuilder_Prepare_BadType(t *testing.T) { ...@@ -43,90 +39,6 @@ func TestBuilder_Prepare_BadType(t *testing.T) {
} }
} }
func TestBuilderPrepare_APIKey(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["api_key"] = "foo"
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.APIKey != "foo" {
t.Errorf("access key invalid: %s", b.config.APIKey)
}
// Test bad
delete(config, "api_key")
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
// Test env variable
delete(config, "api_key")
os.Setenv("DIGITALOCEAN_API_KEY", "foo")
defer os.Setenv("DIGITALOCEAN_API_KEY", "")
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_ClientID(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["client_id"] = "foo"
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.ClientID != "foo" {
t.Errorf("invalid: %s", b.config.ClientID)
}
// Test bad
delete(config, "client_id")
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
// Test env variable
delete(config, "client_id")
os.Setenv("DIGITALOCEAN_CLIENT_ID", "foo")
defer os.Setenv("DIGITALOCEAN_CLIENT_ID", "")
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_InvalidKey(t *testing.T) { func TestBuilderPrepare_InvalidKey(t *testing.T) {
var b Builder var b Builder
config := testConfig() config := testConfig()
...@@ -147,22 +59,18 @@ func TestBuilderPrepare_Region(t *testing.T) { ...@@ -147,22 +59,18 @@ func TestBuilderPrepare_Region(t *testing.T) {
config := testConfig() config := testConfig()
// Test default // Test default
delete(config, "region")
warnings, err := b.Prepare(config) warnings, err := b.Prepare(config)
if len(warnings) > 0 { if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings) t.Fatalf("bad: %#v", warnings)
} }
if err != nil { if err == nil {
t.Fatalf("should not have error: %s", err) t.Fatalf("should error")
}
if b.config.Region != DefaultRegion {
t.Errorf("found %s, expected %s", b.config.Region, DefaultRegion)
} }
expected := "sfo1" expected := "sfo1"
// Test set // Test set
config["region_id"] = 0
config["region"] = expected config["region"] = expected
b = Builder{} b = Builder{}
warnings, err = b.Prepare(config) warnings, err = b.Prepare(config)
...@@ -183,22 +91,18 @@ func TestBuilderPrepare_Size(t *testing.T) { ...@@ -183,22 +91,18 @@ func TestBuilderPrepare_Size(t *testing.T) {
config := testConfig() config := testConfig()
// Test default // Test default
delete(config, "size")
warnings, err := b.Prepare(config) warnings, err := b.Prepare(config)
if len(warnings) > 0 { if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings) t.Fatalf("bad: %#v", warnings)
} }
if err != nil { if err == nil {
t.Fatalf("should not have error: %s", err) t.Fatalf("should error")
}
if b.config.Size != DefaultSize {
t.Errorf("found %s, expected %s", b.config.Size, DefaultSize)
} }
expected := "1024mb" expected := "1024mb"
// Test set // Test set
config["size_id"] = 0
config["size"] = expected config["size"] = expected
b = Builder{} b = Builder{}
warnings, err = b.Prepare(config) warnings, err = b.Prepare(config)
...@@ -219,22 +123,18 @@ func TestBuilderPrepare_Image(t *testing.T) { ...@@ -219,22 +123,18 @@ func TestBuilderPrepare_Image(t *testing.T) {
config := testConfig() config := testConfig()
// Test default // Test default
delete(config, "image")
warnings, err := b.Prepare(config) warnings, err := b.Prepare(config)
if len(warnings) > 0 { if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings) t.Fatalf("bad: %#v", warnings)
} }
if err != nil { if err == nil {
t.Fatalf("should not have error: %s", err) t.Fatal("should error")
}
if b.config.Image != DefaultImage {
t.Errorf("found %s, expected %s", b.config.Image, DefaultImage)
} }
expected := "ubuntu-14-04-x64" expected := "ubuntu-14-04-x64"
// Test set // Test set
config["image_id"] = 0
config["image"] = expected config["image"] = expected
b = Builder{} b = Builder{}
warnings, err = b.Prepare(config) warnings, err = b.Prepare(config)
......
package digitalocean
import (
"errors"
"fmt"
"os"
"time"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"
)
type Config struct {
common.PackerConfig `mapstructure:",squash"`
APIToken string `mapstructure:"api_token"`
Region string `mapstructure:"region"`
Size string `mapstructure:"size"`
Image string `mapstructure:"image"`
PrivateNetworking bool `mapstructure:"private_networking"`
SnapshotName string `mapstructure:"snapshot_name"`
DropletName string `mapstructure:"droplet_name"`
UserData string `mapstructure:"user_data"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPort uint `mapstructure:"ssh_port"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
RawStateTimeout string `mapstructure:"state_timeout"`
// These are unexported since they're set by other fields
// being set.
sshTimeout time.Duration
stateTimeout time.Duration
ctx *interpolate.Context
}
func NewConfig(raws ...interface{}) (*Config, []string, error) {
c := new(Config)
var md mapstructure.Metadata
err := config.Decode(c, &config.DecodeOpts{
Metadata: &md,
Interpolate: true,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{
"run_command",
},
},
}, raws...)
if err != nil {
return nil, nil, err
}
// Defaults
if c.APIToken == "" {
// Default to environment variable for api_token, if it exists
c.APIToken = os.Getenv("DIGITALOCEAN_API_TOKEN")
}
if c.SnapshotName == "" {
def, err := interpolate.Render("packer-{{timestamp}}", nil)
if err != nil {
panic(err)
}
// Default to packer-{{ unix timestamp (utc) }}
c.SnapshotName = def
}
if c.DropletName == "" {
// Default to packer-[time-ordered-uuid]
c.DropletName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
}
if c.SSHUsername == "" {
// Default to "root". You can override this if your
// SourceImage has a different user account then the DO default
c.SSHUsername = "root"
}
if c.SSHPort == 0 {
// Default to port 22 per DO default
c.SSHPort = 22
}
if c.RawSSHTimeout == "" {
// Default to 1 minute timeouts
c.RawSSHTimeout = "1m"
}
if c.RawStateTimeout == "" {
// Default to 6 minute timeouts waiting for
// desired state. i.e waiting for droplet to become active
c.RawStateTimeout = "6m"
}
var errs *packer.MultiError
if c.APIToken == "" {
// Required configurations that will display errors if not set
errs = packer.MultiErrorAppend(
errs, errors.New("api_token for auth must be specified"))
}
if c.Region == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("region is required"))
}
if c.Size == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("size is required"))
}
if c.Image == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("image is required"))
}
sshTimeout, err := time.ParseDuration(c.RawSSHTimeout)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
}
c.sshTimeout = sshTimeout
stateTimeout, err := time.ParseDuration(c.RawStateTimeout)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing state_timeout: %s", err))
}
c.stateTimeout = stateTimeout
if errs != nil && len(errs.Errors) > 0 {
return nil, nil, errs
}
common.ScrubConfig(c, c.APIToken)
return c, nil, nil
}
...@@ -3,25 +3,36 @@ package digitalocean ...@@ -3,25 +3,36 @@ package digitalocean
import ( import (
"fmt" "fmt"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
type stepCreateDroplet struct { type stepCreateDroplet struct {
dropletId uint dropletId int
} }
func (s *stepCreateDroplet) Run(state multistep.StateBag) multistep.StepAction { func (s *stepCreateDroplet) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(DigitalOceanClient) client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(Config) c := state.Get("config").(Config)
sshKeyId := state.Get("ssh_key_id").(uint) sshKeyId := state.Get("ssh_key_id").(int)
ui.Say("Creating droplet...")
// Create the droplet based on configuration // Create the droplet based on configuration
dropletId, err := client.CreateDroplet(c.DropletName, c.Size, c.Image, c.Region, sshKeyId, c.PrivateNetworking) ui.Say("Creating droplet...")
droplet, _, err := client.Droplets.Create(&godo.DropletCreateRequest{
Name: c.DropletName,
Region: c.Region,
Size: c.Size,
Image: godo.DropletCreateImage{
Slug: c.Image,
},
SSHKeys: []godo.DropletCreateSSHKey{
godo.DropletCreateSSHKey{ID: int(sshKeyId)},
},
PrivateNetworking: c.PrivateNetworking,
UserData: c.UserData,
})
if err != nil { if err != nil {
err := fmt.Errorf("Error creating droplet: %s", err) err := fmt.Errorf("Error creating droplet: %s", err)
state.Put("error", err) state.Put("error", err)
...@@ -30,10 +41,10 @@ func (s *stepCreateDroplet) Run(state multistep.StateBag) multistep.StepAction { ...@@ -30,10 +41,10 @@ func (s *stepCreateDroplet) Run(state multistep.StateBag) multistep.StepAction {
} }
// We use this in cleanup // We use this in cleanup
s.dropletId = dropletId s.dropletId = droplet.ID
// Store the droplet id for later // Store the droplet id for later
state.Put("droplet_id", dropletId) state.Put("droplet_id", droplet.ID)
return multistep.ActionContinue return multistep.ActionContinue
} }
...@@ -44,19 +55,14 @@ func (s *stepCreateDroplet) Cleanup(state multistep.StateBag) { ...@@ -44,19 +55,14 @@ func (s *stepCreateDroplet) Cleanup(state multistep.StateBag) {
return return
} }
client := state.Get("client").(DigitalOceanClient) client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(Config)
// Destroy the droplet we just created // Destroy the droplet we just created
ui.Say("Destroying droplet...") ui.Say("Destroying droplet...")
_, err := client.Droplets.Delete(s.dropletId)
err := client.DestroyDroplet(s.dropletId)
if err != nil { if err != nil {
curlstr := fmt.Sprintf("curl '%v/droplets/%v/destroy?client_id=%v&api_key=%v'",
c.APIURL, s.dropletId, c.ClientID, c.APIKey)
ui.Error(fmt.Sprintf( ui.Error(fmt.Sprintf(
"Error destroying droplet. Please destroy it manually: %v", curlstr)) "Error destroying droplet. Please destroy it manually: %s", err))
} }
} }
...@@ -7,19 +7,25 @@ import ( ...@@ -7,19 +7,25 @@ import (
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"log" "log"
"os"
"runtime"
"code.google.com/p/gosshold/ssh" "code.google.com/p/gosshold/ssh"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common/uuid" "github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
type stepCreateSSHKey struct { type stepCreateSSHKey struct {
keyId uint Debug bool
DebugKeyPath string
keyId int
} }
func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction { func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(DigitalOceanClient) client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
ui.Say("Creating temporary ssh key for droplet...") ui.Say("Creating temporary ssh key for droplet...")
...@@ -46,7 +52,10 @@ func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction { ...@@ -46,7 +52,10 @@ func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction {
name := fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID()) name := fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
// Create the key! // Create the key!
keyId, err := client.CreateKey(name, pub_sshformat) key, _, err := client.Keys.Create(&godo.KeyCreateRequest{
Name: name,
PublicKey: pub_sshformat,
})
if err != nil { if err != nil {
err := fmt.Errorf("Error creating temporary SSH key: %s", err) err := fmt.Errorf("Error creating temporary SSH key: %s", err)
state.Put("error", err) state.Put("error", err)
...@@ -55,12 +64,37 @@ func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction { ...@@ -55,12 +64,37 @@ func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction {
} }
// We use this to check cleanup // We use this to check cleanup
s.keyId = keyId s.keyId = key.ID
log.Printf("temporary ssh key name: %s", name) log.Printf("temporary ssh key name: %s", name)
// Remember some state for the future // Remember some state for the future
state.Put("ssh_key_id", keyId) state.Put("ssh_key_id", key.ID)
// If we're in debug mode, output the private key to the working directory.
if s.Debug {
ui.Message(fmt.Sprintf("Saving key for debug purposes: %s", s.DebugKeyPath))
f, err := os.Create(s.DebugKeyPath)
if err != nil {
state.Put("error", fmt.Errorf("Error saving debug key: %s", err))
return multistep.ActionHalt
}
defer f.Close()
// Write the key out
if _, err := f.Write(pem.EncodeToMemory(&priv_blk)); err != nil {
state.Put("error", fmt.Errorf("Error saving debug key: %s", err))
return multistep.ActionHalt
}
// Chmod it so that it is SSH ready
if runtime.GOOS != "windows" {
if err := f.Chmod(0600); err != nil {
state.Put("error", fmt.Errorf("Error setting permissions of debug key: %s", err))
return multistep.ActionHalt
}
}
}
return multistep.ActionContinue return multistep.ActionContinue
} }
...@@ -71,18 +105,14 @@ func (s *stepCreateSSHKey) Cleanup(state multistep.StateBag) { ...@@ -71,18 +105,14 @@ func (s *stepCreateSSHKey) Cleanup(state multistep.StateBag) {
return return
} }
client := state.Get("client").(DigitalOceanClient) client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(Config)
ui.Say("Deleting temporary ssh key...") ui.Say("Deleting temporary ssh key...")
err := client.DestroyKey(s.keyId) _, err := client.Keys.DeleteByID(s.keyId)
curlstr := fmt.Sprintf("curl -H 'Authorization: Bearer #TOKEN#' -X DELETE '%v/v2/account/keys/%v'", c.APIURL, s.keyId)
if err != nil { if err != nil {
log.Printf("Error cleaning up ssh key: %v", err.Error()) log.Printf("Error cleaning up ssh key: %s", err)
ui.Error(fmt.Sprintf( ui.Error(fmt.Sprintf(
"Error cleaning up ssh key. Please delete the key manually: %v", curlstr)) "Error cleaning up ssh key. Please delete the key manually: %s", err))
} }
} }
...@@ -3,6 +3,7 @@ package digitalocean ...@@ -3,6 +3,7 @@ package digitalocean
import ( import (
"fmt" "fmt"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
...@@ -10,10 +11,10 @@ import ( ...@@ -10,10 +11,10 @@ import (
type stepDropletInfo struct{} type stepDropletInfo struct{}
func (s *stepDropletInfo) Run(state multistep.StateBag) multistep.StepAction { func (s *stepDropletInfo) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(DigitalOceanClient) client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(Config) c := state.Get("config").(Config)
dropletId := state.Get("droplet_id").(uint) dropletId := state.Get("droplet_id").(int)
ui.Say("Waiting for droplet to become active...") ui.Say("Waiting for droplet to become active...")
...@@ -26,16 +27,25 @@ func (s *stepDropletInfo) Run(state multistep.StateBag) multistep.StepAction { ...@@ -26,16 +27,25 @@ func (s *stepDropletInfo) Run(state multistep.StateBag) multistep.StepAction {
} }
// Set the IP on the state for later // Set the IP on the state for later
ip, _, err := client.DropletStatus(dropletId) droplet, _, err := client.Droplets.Get(dropletId)
if err != nil { if err != nil {
err := fmt.Errorf("Error retrieving droplet ID: %s", err) err := fmt.Errorf("Error retrieving droplet: %s", err)
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
} }
state.Put("droplet_ip", ip) // Verify we have an IPv4 address
invalid := droplet.Networks == nil ||
len(droplet.Networks.V4) == 0
if invalid {
err := fmt.Errorf("IPv4 address not found for droplet!")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
state.Put("droplet_ip", droplet.Networks.V4[0].IPAddress)
return multistep.ActionContinue return multistep.ActionContinue
} }
......
...@@ -3,7 +3,9 @@ package digitalocean ...@@ -3,7 +3,9 @@ package digitalocean
import ( import (
"fmt" "fmt"
"log" "log"
"time"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
...@@ -11,12 +13,12 @@ import ( ...@@ -11,12 +13,12 @@ import (
type stepPowerOff struct{} type stepPowerOff struct{}
func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction { func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(DigitalOceanClient) client := state.Get("client").(*godo.Client)
c := state.Get("config").(Config) c := state.Get("config").(Config)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
dropletId := state.Get("droplet_id").(uint) dropletId := state.Get("droplet_id").(int)
_, status, err := client.DropletStatus(dropletId) droplet, _, err := client.Droplets.Get(dropletId)
if err != nil { if err != nil {
err := fmt.Errorf("Error checking droplet state: %s", err) err := fmt.Errorf("Error checking droplet state: %s", err)
state.Put("error", err) state.Put("error", err)
...@@ -24,14 +26,14 @@ func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction { ...@@ -24,14 +26,14 @@ func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionHalt return multistep.ActionHalt
} }
if status == "off" { if droplet.Status == "off" {
// Droplet is already off, don't do anything // Droplet is already off, don't do anything
return multistep.ActionContinue return multistep.ActionContinue
} }
// Pull the plug on the Droplet // Pull the plug on the Droplet
ui.Say("Forcefully shutting down Droplet...") ui.Say("Forcefully shutting down Droplet...")
err = client.PowerOffDroplet(dropletId) _, _, err = client.DropletActions.PowerOff(dropletId)
if err != nil { if err != nil {
err := fmt.Errorf("Error powering off droplet: %s", err) err := fmt.Errorf("Error powering off droplet: %s", err)
state.Put("error", err) state.Put("error", err)
...@@ -47,6 +49,15 @@ func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction { ...@@ -47,6 +49,15 @@ func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionHalt return multistep.ActionHalt
} }
// Wait for the droplet to become unlocked for future steps
if err := waitForDropletUnlocked(client, dropletId, 2*time.Minute); err != nil {
// If we get an error the first time, actually report it
err := fmt.Errorf("Error powering off droplet: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue return multistep.ActionContinue
} }
......
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"log" "log"
"time" "time"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
...@@ -12,16 +13,16 @@ import ( ...@@ -12,16 +13,16 @@ import (
type stepShutdown struct{} type stepShutdown struct{}
func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction { func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(DigitalOceanClient) client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
dropletId := state.Get("droplet_id").(uint) dropletId := state.Get("droplet_id").(int)
// Gracefully power off the droplet. We have to retry this a number // Gracefully power off the droplet. We have to retry this a number
// of times because sometimes it says it completed when it actually // of times because sometimes it says it completed when it actually
// did absolutely nothing (*ALAKAZAM!* magic!). We give up after // did absolutely nothing (*ALAKAZAM!* magic!). We give up after
// a pretty arbitrary amount of time. // a pretty arbitrary amount of time.
ui.Say("Gracefully shutting down droplet...") ui.Say("Gracefully shutting down droplet...")
err := client.ShutdownDroplet(dropletId) _, _, err := client.DropletActions.Shutdown(dropletId)
if err != nil { if err != nil {
// If we get an error the first time, actually report it // If we get an error the first time, actually report it
err := fmt.Errorf("Error shutting down droplet: %s", err) err := fmt.Errorf("Error shutting down droplet: %s", err)
...@@ -48,7 +49,7 @@ func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction { ...@@ -48,7 +49,7 @@ func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
for attempts := 2; attempts > 0; attempts++ { for attempts := 2; attempts > 0; attempts++ {
log.Printf("ShutdownDroplet attempt #%d...", attempts) log.Printf("ShutdownDroplet attempt #%d...", attempts)
err := client.ShutdownDroplet(dropletId) _, _, err := client.DropletActions.Shutdown(dropletId)
if err != nil { if err != nil {
log.Printf("Shutdown retry error: %s", err) log.Printf("Shutdown retry error: %s", err)
} }
...@@ -64,7 +65,19 @@ func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction { ...@@ -64,7 +65,19 @@ func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
err = waitForDropletState("off", dropletId, client, 2*time.Minute) err = waitForDropletState("off", dropletId, client, 2*time.Minute)
if err != nil { if err != nil {
log.Printf("Error waiting for graceful off: %s", err) // If we get an error the first time, actually report it
err := fmt.Errorf("Error shutting down droplet: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if err := waitForDropletUnlocked(client, dropletId, 2*time.Minute); err != nil {
// If we get an error the first time, actually report it
err := fmt.Errorf("Error shutting down droplet: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
} }
return multistep.ActionContinue return multistep.ActionContinue
......
...@@ -4,7 +4,9 @@ import ( ...@@ -4,7 +4,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"log" "log"
"time"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
...@@ -12,13 +14,13 @@ import ( ...@@ -12,13 +14,13 @@ import (
type stepSnapshot struct{} type stepSnapshot struct{}
func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction { func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(DigitalOceanClient) client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(Config) c := state.Get("config").(Config)
dropletId := state.Get("droplet_id").(uint) dropletId := state.Get("droplet_id").(int)
ui.Say(fmt.Sprintf("Creating snapshot: %v", c.SnapshotName)) ui.Say(fmt.Sprintf("Creating snapshot: %v", c.SnapshotName))
err := client.CreateSnapshot(dropletId, c.SnapshotName) _, _, err := client.DropletActions.Snapshot(dropletId, c.SnapshotName)
if err != nil { if err != nil {
err := fmt.Errorf("Error creating snapshot: %s", err) err := fmt.Errorf("Error creating snapshot: %s", err)
state.Put("error", err) state.Put("error", err)
...@@ -26,6 +28,18 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction { ...@@ -26,6 +28,18 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionHalt return multistep.ActionHalt
} }
// Wait for the droplet to become unlocked first. For snapshots
// this can end up taking quite a long time, so we hardcode this to
// 10 minutes.
if err := waitForDropletUnlocked(client, dropletId, 10*time.Minute); err != nil {
// If we get an error the first time, actually report it
err := fmt.Errorf("Error shutting down droplet: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// With the pending state over, verify that we're in the active state
ui.Say("Waiting for snapshot to complete...") ui.Say("Waiting for snapshot to complete...")
err = waitForDropletState("active", dropletId, client, c.stateTimeout) err = waitForDropletState("active", dropletId, client, c.stateTimeout)
if err != nil { if err != nil {
...@@ -36,7 +50,7 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction { ...@@ -36,7 +50,7 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
} }
log.Printf("Looking up snapshot ID for snapshot: %s", c.SnapshotName) log.Printf("Looking up snapshot ID for snapshot: %s", c.SnapshotName)
images, err := client.Images() images, _, err := client.Images.ListUser(&godo.ListOptions{PerPage: 200})
if err != nil { if err != nil {
err := fmt.Errorf("Error looking up snapshot ID: %s", err) err := fmt.Errorf("Error looking up snapshot ID: %s", err)
state.Put("error", err) state.Put("error", err)
...@@ -44,10 +58,10 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction { ...@@ -44,10 +58,10 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionHalt return multistep.ActionHalt
} }
var imageId uint var imageId int
for _, image := range images { for _, image := range images {
if image.Name == c.SnapshotName { if image.Name == c.SnapshotName {
imageId = image.Id imageId = image.ID
break break
} }
} }
...@@ -60,7 +74,6 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction { ...@@ -60,7 +74,6 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
} }
log.Printf("Snapshot image ID: %d", imageId) log.Printf("Snapshot image ID: %d", imageId)
state.Put("snapshot_image_id", imageId) state.Put("snapshot_image_id", imageId)
state.Put("snapshot_name", c.SnapshotName) state.Put("snapshot_name", c.SnapshotName)
state.Put("region", c.Region) state.Put("region", c.Region)
......
package digitalocean
import (
"golang.org/x/oauth2"
)
type apiTokenSource struct {
AccessToken string
}
func (t *apiTokenSource) Token() (*oauth2.Token, error) {
return &oauth2.Token{
AccessToken: t.AccessToken,
}, nil
}
...@@ -4,11 +4,64 @@ import ( ...@@ -4,11 +4,64 @@ import (
"fmt" "fmt"
"log" "log"
"time" "time"
"github.com/digitalocean/godo"
) )
// waitForDropletUnlocked waits for the Droplet to be unlocked to
// avoid "pending" errors when making state changes.
func waitForDropletUnlocked(
client *godo.Client, dropletId int, timeout time.Duration) error {
done := make(chan struct{})
defer close(done)
result := make(chan error, 1)
go func() {
attempts := 0
for {
attempts += 1
log.Printf("[DEBUG] Checking droplet lock state... (attempt: %d)", attempts)
droplet, _, err := client.Droplets.Get(dropletId)
if err != nil {
result <- err
return
}
if !droplet.Locked {
result <- nil
return
}
// Wait 3 seconds in between
time.Sleep(3 * time.Second)
// Verify we shouldn't exit
select {
case <-done:
// We finished, so just exit the goroutine
return
default:
// Keep going
}
}
}()
log.Printf("[DEBUG] Waiting for up to %d seconds for droplet to unlock", timeout/time.Second)
select {
case err := <-result:
return err
case <-time.After(timeout):
return fmt.Errorf(
"Timeout while waiting to for droplet to unlock")
}
}
// waitForState simply blocks until the droplet is in // waitForState simply blocks until the droplet is in
// a state we expect, while eventually timing out. // a state we expect, while eventually timing out.
func waitForDropletState(desiredState string, dropletId uint, client DigitalOceanClient, timeout time.Duration) error { func waitForDropletState(
desiredState string, dropletId int,
client *godo.Client, timeout time.Duration) error {
done := make(chan struct{}) done := make(chan struct{})
defer close(done) defer close(done)
...@@ -19,13 +72,13 @@ func waitForDropletState(desiredState string, dropletId uint, client DigitalOcea ...@@ -19,13 +72,13 @@ func waitForDropletState(desiredState string, dropletId uint, client DigitalOcea
attempts += 1 attempts += 1
log.Printf("Checking droplet status... (attempt: %d)", attempts) log.Printf("Checking droplet status... (attempt: %d)", attempts)
_, status, err := client.DropletStatus(dropletId) droplet, _, err := client.Droplets.Get(dropletId)
if err != nil { if err != nil {
result <- err result <- err
return return
} }
if status == desiredState { if droplet.Status == desiredState {
result <- nil result <- nil
return return
} }
......
...@@ -31,10 +31,10 @@ type Config struct { ...@@ -31,10 +31,10 @@ type Config struct {
} }
func NewConfig(raws ...interface{}) (*Config, []string, error) { func NewConfig(raws ...interface{}) (*Config, []string, error) {
var c Config c := new(Config)
var md mapstructure.Metadata var md mapstructure.Metadata
err := config.Decode(&c, &config.DecodeOpts{ err := config.Decode(c, &config.DecodeOpts{
Metadata: &md, Metadata: &md,
Interpolate: true, Interpolate: true,
InterpolateFilter: &interpolate.RenderFilter{ InterpolateFilter: &interpolate.RenderFilter{
...@@ -91,5 +91,5 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { ...@@ -91,5 +91,5 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
return nil, nil, errs return nil, nil, errs
} }
return &c, nil, nil return c, nil, nil
} }
...@@ -47,7 +47,7 @@ type Config struct { ...@@ -47,7 +47,7 @@ type Config struct {
func NewConfig(raws ...interface{}) (*Config, []string, error) { func NewConfig(raws ...interface{}) (*Config, []string, error) {
c := new(Config) c := new(Config)
err := config.Decode(&c, &config.DecodeOpts{ err := config.Decode(c, &config.DecodeOpts{
Interpolate: true, Interpolate: true,
InterpolateFilter: &interpolate.RenderFilter{ InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{ Exclude: []string{
......
...@@ -21,7 +21,7 @@ type Config struct { ...@@ -21,7 +21,7 @@ type Config struct {
func NewConfig(raws ...interface{}) (*Config, []string, error) { func NewConfig(raws ...interface{}) (*Config, []string, error) {
c := new(Config) c := new(Config)
err := config.Decode(&c, &config.DecodeOpts{ err := config.Decode(c, &config.DecodeOpts{
Interpolate: true, Interpolate: true,
InterpolateFilter: &interpolate.RenderFilter{ InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{ Exclude: []string{
......
...@@ -4,99 +4,120 @@ import ( ...@@ -4,99 +4,120 @@ import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"os" "os"
"strings"
"github.com/mitchellh/gophercloud-fork-40444fb"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack"
) )
// AccessConfig is for common configuration related to openstack access // AccessConfig is for common configuration related to openstack access
type AccessConfig struct { type AccessConfig struct {
Username string `mapstructure:"username"` Username string `mapstructure:"username"`
Password string `mapstructure:"password"` UserID string `mapstructure:"user_id"`
ApiKey string `mapstructure:"api_key"` Password string `mapstructure:"password"`
Project string `mapstructure:"project"` APIKey string `mapstructure:"api_key"`
Provider string `mapstructure:"provider"` IdentityEndpoint string `mapstructure:"identity_endpoint"`
RawRegion string `mapstructure:"region"` TenantID string `mapstructure:"tenant_id"`
ProxyUrl string `mapstructure:"proxy_url"` TenantName string `mapstructure:"tenant_name"`
TenantId string `mapstructure:"tenant_id"` DomainID string `mapstructure:"domain_id"`
Insecure bool `mapstructure:"insecure"` DomainName string `mapstructure:"domain_name"`
Insecure bool `mapstructure:"insecure"`
Region string `mapstructure:"region"`
EndpointType string `mapstructure:"endpoint_type"`
osClient *gophercloud.ProviderClient
} }
// Auth returns a valid Auth object for access to openstack services, or func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
// an error if the authentication couldn't be resolved. if c.EndpointType != "internal" && c.EndpointType != "internalURL" &&
func (c *AccessConfig) Auth() (gophercloud.AccessProvider, error) { c.EndpointType != "admin" && c.EndpointType != "adminURL" &&
c.Username = common.ChooseString(c.Username, os.Getenv("SDK_USERNAME"), os.Getenv("OS_USERNAME")) c.EndpointType != "public" && c.EndpointType != "publicURL" &&
c.Password = common.ChooseString(c.Password, os.Getenv("SDK_PASSWORD"), os.Getenv("OS_PASSWORD")) c.EndpointType != "" {
c.ApiKey = common.ChooseString(c.ApiKey, os.Getenv("SDK_API_KEY")) return []error{fmt.Errorf("Invalid endpoint type provided")}
c.Project = common.ChooseString(c.Project, os.Getenv("SDK_PROJECT"), os.Getenv("OS_TENANT_NAME"))
c.Provider = common.ChooseString(c.Provider, os.Getenv("SDK_PROVIDER"), os.Getenv("OS_AUTH_URL"))
c.RawRegion = common.ChooseString(c.RawRegion, os.Getenv("SDK_REGION"), os.Getenv("OS_REGION_NAME"))
c.TenantId = common.ChooseString(c.TenantId, os.Getenv("OS_TENANT_ID"))
// OpenStack's auto-generated openrc.sh files do not append the suffix
// /tokens to the authentication URL. This ensures it is present when
// specifying the URL.
if strings.Contains(c.Provider, "://") && !strings.HasSuffix(c.Provider, "/tokens") {
c.Provider += "/tokens"
} }
authoptions := gophercloud.AuthOptions{ if c.Region == "" {
AllowReauth: true, c.Region = os.Getenv("OS_REGION_NAME")
ApiKey: c.ApiKey,
TenantId: c.TenantId,
TenantName: c.Project,
Username: c.Username,
Password: c.Password,
} }
default_transport := &http.Transport{} // Legacy RackSpace stuff. We're keeping this around to keep things BC.
if c.APIKey == "" {
if c.Insecure { c.APIKey = os.Getenv("SDK_API_KEY")
cfg := new(tls.Config) }
cfg.InsecureSkipVerify = true if c.Password == "" {
default_transport.TLSClientConfig = cfg c.Password = os.Getenv("SDK_PASSWORD")
}
if c.Region == "" {
c.Region = os.Getenv("SDK_REGION")
}
if c.TenantName == "" {
c.TenantName = os.Getenv("SDK_PROJECT")
}
if c.Username == "" {
c.Username = os.Getenv("SDK_USERNAME")
} }
// For corporate networks it may be the case where we want our API calls // Get as much as possible from the end
// to be sent through a separate HTTP proxy than external traffic. ao, _ := openstack.AuthOptionsFromEnv()
if c.ProxyUrl != "" {
url, err := url.Parse(c.ProxyUrl) // Override values if we have them in our config
if err != nil { overrides := []struct {
return nil, err From, To *string
}{
{&c.Username, &ao.Username},
{&c.UserID, &ao.UserID},
{&c.Password, &ao.Password},
{&c.APIKey, &ao.APIKey},
{&c.IdentityEndpoint, &ao.IdentityEndpoint},
{&c.TenantID, &ao.TenantID},
{&c.TenantName, &ao.TenantName},
{&c.DomainID, &ao.DomainID},
{&c.DomainName, &ao.DomainName},
}
for _, s := range overrides {
if *s.From != "" {
*s.To = *s.From
} }
}
// The gophercloud.Context has a UseCustomClient method which // Build the client itself
// would allow us to override with a new instance of http.Client. client, err := openstack.NewClient(ao.IdentityEndpoint)
default_transport.Proxy = http.ProxyURL(url) if err != nil {
return []error{err}
} }
if c.Insecure || c.ProxyUrl != "" { // If we have insecure set, then create a custom HTTP client that
http.DefaultTransport = default_transport // ignores SSL errors.
if c.Insecure {
config := &tls.Config{InsecureSkipVerify: true}
transport := &http.Transport{TLSClientConfig: config}
client.HTTPClient.Transport = transport
} }
return gophercloud.Authenticate(c.Provider, authoptions) // Auth
err = openstack.Authenticate(client, ao)
if err != nil {
return []error{err}
}
c.osClient = client
return nil
} }
func (c *AccessConfig) Region() string { func (c *AccessConfig) computeV2Client() (*gophercloud.ServiceClient, error) {
return common.ChooseString(c.RawRegion, os.Getenv("SDK_REGION"), os.Getenv("OS_REGION_NAME")) return openstack.NewComputeV2(c.osClient, gophercloud.EndpointOpts{
Region: c.Region,
Availability: c.getEndpointType(),
})
} }
func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error { func (c *AccessConfig) getEndpointType() gophercloud.Availability {
errs := make([]error, 0) if c.EndpointType == "internal" || c.EndpointType == "internalURL" {
if strings.HasPrefix(c.Provider, "rackspace") { return gophercloud.AvailabilityInternal
if c.Region() == "" {
errs = append(errs, fmt.Errorf("region must be specified when using rackspace"))
}
} }
if c.EndpointType == "admin" || c.EndpointType == "adminURL" {
if len(errs) > 0 { return gophercloud.AvailabilityAdmin
return errs
} }
return gophercloud.AvailabilityPublic
return nil
} }
package openstack
import (
"os"
"testing"
)
func init() {
// Clear out the openstack env vars so they don't
// affect our tests.
os.Setenv("SDK_REGION", "")
os.Setenv("OS_REGION_NAME", "")
}
func testAccessConfig() *AccessConfig {
return &AccessConfig{}
}
func TestAccessConfigPrepare_NoRegion_Rackspace(t *testing.T) {
c := testAccessConfig()
c.Provider = "rackspace-us"
if err := c.Prepare(nil); err == nil {
t.Fatalf("shouldn't have err: %s", err)
}
}
func TestAccessConfigRegionWithEmptyEnv(t *testing.T) {
c := testAccessConfig()
c.Prepare(nil)
if c.Region() != "" {
t.Fatalf("Region should be empty")
}
}
func TestAccessConfigRegionWithSdkRegionEnv(t *testing.T) {
c := testAccessConfig()
c.Prepare(nil)
expectedRegion := "sdk_region"
os.Setenv("SDK_REGION", expectedRegion)
os.Setenv("OS_REGION_NAME", "")
if c.Region() != expectedRegion {
t.Fatalf("Region should be: %s", expectedRegion)
}
}
func TestAccessConfigRegionWithOsRegionNameEnv(t *testing.T) {
c := testAccessConfig()
c.Prepare(nil)
expectedRegion := "os_region_name"
os.Setenv("SDK_REGION", "")
os.Setenv("OS_REGION_NAME", expectedRegion)
if c.Region() != expectedRegion {
t.Fatalf("Region should be: %s", expectedRegion)
}
}
func TestAccessConfigPrepare_NoRegion_PrivateCloud(t *testing.T) {
c := testAccessConfig()
c.Provider = "http://some-keystone-server:5000/v2.0"
if err := c.Prepare(nil); err != nil {
t.Fatalf("shouldn't have err: %s", err)
}
}
func TestAccessConfigPrepare_Region(t *testing.T) {
dfw := "DFW"
c := testAccessConfig()
c.RawRegion = dfw
if err := c.Prepare(nil); err != nil {
t.Fatalf("shouldn't have err: %s", err)
}
if dfw != c.Region() {
t.Fatalf("Regions do not match: %s %s", dfw, c.Region())
}
}
...@@ -4,7 +4,8 @@ import ( ...@@ -4,7 +4,8 @@ import (
"fmt" "fmt"
"log" "log"
"github.com/mitchellh/gophercloud-fork-40444fb" "github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/compute/v2/images"
) )
// Artifact is an artifact implementation that contains built images. // Artifact is an artifact implementation that contains built images.
...@@ -16,7 +17,7 @@ type Artifact struct { ...@@ -16,7 +17,7 @@ type Artifact struct {
BuilderIdValue string BuilderIdValue string
// OpenStack connection for performing API stuff. // OpenStack connection for performing API stuff.
Conn gophercloud.CloudServersProvider Client *gophercloud.ServiceClient
} }
func (a *Artifact) BuilderId() string { func (a *Artifact) BuilderId() string {
...@@ -42,5 +43,5 @@ func (a *Artifact) State(name string) interface{} { ...@@ -42,5 +43,5 @@ func (a *Artifact) State(name string) interface{} {
func (a *Artifact) Destroy() error { func (a *Artifact) Destroy() error {
log.Printf("Destroying image: %s", a.ImageId) log.Printf("Destroying image: %s", a.ImageId)
return a.Conn.DeleteImageById(a.ImageId) return images.Delete(a.Client, a.ImageId).ExtractErr()
} }
...@@ -9,7 +9,6 @@ import ( ...@@ -9,7 +9,6 @@ import (
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"log" "log"
"github.com/mitchellh/gophercloud-fork-40444fb"
"github.com/mitchellh/packer/helper/config" "github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
...@@ -55,43 +54,32 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { ...@@ -55,43 +54,32 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
} }
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
auth, err := b.config.AccessConfig.Auth() computeClient, err := b.config.computeV2Client()
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("Error initializing compute client: %s", err)
}
//fetches the api requisites from gophercloud for the appropriate
//openstack variant
api, err := gophercloud.PopulateApi(b.config.RunConfig.OpenstackProvider)
if err != nil {
return nil, err
}
api.Region = b.config.AccessConfig.Region()
csp, err := gophercloud.ServersApi(auth, api)
if err != nil {
log.Printf("Region: %s", b.config.AccessConfig.Region())
return nil, err
} }
// Setup the state bag and initial state for the steps // Setup the state bag and initial state for the steps
state := new(multistep.BasicStateBag) state := new(multistep.BasicStateBag)
state.Put("config", b.config) state.Put("config", b.config)
state.Put("csp", csp)
state.Put("hook", hook) state.Put("hook", hook)
state.Put("ui", ui) state.Put("ui", ui)
// Build the steps // Build the steps
steps := []multistep.Step{ steps := []multistep.Step{
&StepLoadFlavor{
Flavor: b.config.Flavor,
},
&StepKeyPair{ &StepKeyPair{
Debug: b.config.PackerDebug, Debug: b.config.PackerDebug,
DebugKeyPath: fmt.Sprintf("os_%s.pem", b.config.PackerBuildName), DebugKeyPath: fmt.Sprintf("os_%s.pem", b.config.PackerBuildName),
}, },
&StepRunSourceServer{ &StepRunSourceServer{
Name: b.config.ImageName, Name: b.config.ImageName,
Flavor: b.config.Flavor, SourceImage: b.config.SourceImage,
SourceImage: b.config.SourceImage, SecurityGroups: b.config.SecurityGroups,
SecurityGroups: b.config.SecurityGroups, Networks: b.config.Networks,
Networks: b.config.Networks, AvailabilityZone: b.config.AvailabilityZone,
}, },
&StepWaitForRackConnect{ &StepWaitForRackConnect{
Wait: b.config.RackconnectWait, Wait: b.config.RackconnectWait,
...@@ -101,7 +89,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -101,7 +89,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
FloatingIp: b.config.FloatingIp, FloatingIp: b.config.FloatingIp,
}, },
&common.StepConnectSSH{ &common.StepConnectSSH{
SSHAddress: SSHAddress(csp, b.config.SSHInterface, b.config.SSHPort), SSHAddress: SSHAddress(computeClient, b.config.SSHInterface, b.config.SSHPort),
SSHConfig: SSHConfig(b.config.SSHUsername), SSHConfig: SSHConfig(b.config.SSHUsername),
SSHWaitTimeout: b.config.SSHTimeout(), SSHWaitTimeout: b.config.SSHTimeout(),
}, },
...@@ -135,7 +123,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -135,7 +123,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
artifact := &Artifact{ artifact := &Artifact{
ImageId: state.Get("image").(string), ImageId: state.Get("image").(string),
BuilderIdValue: BuilderId, BuilderIdValue: BuilderId,
Conn: csp, Client: computeClient,
} }
return artifact, nil return artifact, nil
......
...@@ -9,7 +9,6 @@ func testConfig() map[string]interface{} { ...@@ -9,7 +9,6 @@ func testConfig() map[string]interface{} {
return map[string]interface{}{ return map[string]interface{}{
"username": "foo", "username": "foo",
"password": "bar", "password": "bar",
"provider": "foo",
"region": "DFW", "region": "DFW",
"image_name": "foo", "image_name": "foo",
"source_image": "foo", "source_image": "foo",
...@@ -40,55 +39,3 @@ func TestBuilder_Prepare_BadType(t *testing.T) { ...@@ -40,55 +39,3 @@ func TestBuilder_Prepare_BadType(t *testing.T) {
t.Fatalf("prepare should fail") t.Fatalf("prepare should fail")
} }
} }
func TestBuilderPrepare_ImageName(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["image_name"] = "foo"
warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test bad
config["image_name"] = "foo {{"
b = Builder{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Test bad
delete(config, "image_name")
b = Builder{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_InvalidKey(t *testing.T) {
var b Builder
config := testConfig()
// Add a random key
config["i_should_not_be_valid"] = true
warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
}
...@@ -11,19 +11,22 @@ import ( ...@@ -11,19 +11,22 @@ import (
// RunConfig contains configuration for running an instance from a source // RunConfig contains configuration for running an instance from a source
// image and details on how to access that launched image. // image and details on how to access that launched image.
type RunConfig struct { type RunConfig struct {
SourceImage string `mapstructure:"source_image"` SourceImage string `mapstructure:"source_image"`
Flavor string `mapstructure:"flavor"` Flavor string `mapstructure:"flavor"`
RawSSHTimeout string `mapstructure:"ssh_timeout"` RawSSHTimeout string `mapstructure:"ssh_timeout"`
SSHUsername string `mapstructure:"ssh_username"` SSHUsername string `mapstructure:"ssh_username"`
SSHPort int `mapstructure:"ssh_port"` SSHPort int `mapstructure:"ssh_port"`
SSHInterface string `mapstructure:"ssh_interface"` SSHInterface string `mapstructure:"ssh_interface"`
OpenstackProvider string `mapstructure:"openstack_provider"` AvailabilityZone string `mapstructure:"availability_zone"`
UseFloatingIp bool `mapstructure:"use_floating_ip"` RackconnectWait bool `mapstructure:"rackconnect_wait"`
RackconnectWait bool `mapstructure:"rackconnect_wait"` FloatingIpPool string `mapstructure:"floating_ip_pool"`
FloatingIpPool string `mapstructure:"floating_ip_pool"` FloatingIp string `mapstructure:"floating_ip"`
FloatingIp string `mapstructure:"floating_ip"` SecurityGroups []string `mapstructure:"security_groups"`
SecurityGroups []string `mapstructure:"security_groups"` Networks []string `mapstructure:"networks"`
Networks []string `mapstructure:"networks"`
// Not really used, but here for BC
OpenstackProvider string `mapstructure:"openstack_provider"`
UseFloatingIp bool `mapstructure:"use_floating_ip"`
// Unexported fields that are calculated from others // Unexported fields that are calculated from others
sshTimeout time.Duration sshTimeout time.Duration
......
...@@ -3,12 +3,12 @@ package openstack ...@@ -3,12 +3,12 @@ package openstack
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/mitchellh/multistep"
"github.com/racker/perigee"
"log" "log"
"time" "time"
"github.com/mitchellh/gophercloud-fork-40444fb" "github.com/mitchellh/multistep"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
) )
// StateRefreshFunc is a function type used for StateChangeConf that is // StateRefreshFunc is a function type used for StateChangeConf that is
...@@ -33,21 +33,22 @@ type StateChangeConf struct { ...@@ -33,21 +33,22 @@ type StateChangeConf struct {
// ServerStateRefreshFunc returns a StateRefreshFunc that is used to watch // ServerStateRefreshFunc returns a StateRefreshFunc that is used to watch
// an openstack server. // an openstack server.
func ServerStateRefreshFunc(csp gophercloud.CloudServersProvider, s *gophercloud.Server) StateRefreshFunc { func ServerStateRefreshFunc(
client *gophercloud.ServiceClient, s *servers.Server) StateRefreshFunc {
return func() (interface{}, string, int, error) { return func() (interface{}, string, int, error) {
resp, err := csp.ServerById(s.Id) serverNew, err := servers.Get(client, s.ID).Extract()
if err != nil { if err != nil {
urce, ok := err.(*perigee.UnexpectedResponseCodeError) errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError)
if ok && (urce.Actual == 404) { if ok && errCode.Actual == 404 {
log.Printf("404 on ServerStateRefresh, returning DELETED") log.Printf("[INFO] 404 on ServerStateRefresh, returning DELETED")
return nil, "DELETED", 0, nil return nil, "DELETED", 0, nil
} else { } else {
log.Printf("Error on ServerStateRefresh: %s", err) log.Printf("[ERROR] Error on ServerStateRefresh: %s", err)
return nil, "", 0, err return nil, "", 0, err
} }
} }
return resp, resp.Status, resp.Progress, nil
return serverNew, serverNew.Status, serverNew.Progress, nil
} }
} }
......
...@@ -3,49 +3,67 @@ package openstack ...@@ -3,49 +3,67 @@ package openstack
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/mitchellh/multistep" "log"
"golang.org/x/crypto/ssh"
"time" "time"
"github.com/mitchellh/gophercloud-fork-40444fb" "github.com/mitchellh/multistep"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
"golang.org/x/crypto/ssh"
) )
// SSHAddress returns a function that can be given to the SSH communicator // SSHAddress returns a function that can be given to the SSH communicator
// for determining the SSH address based on the server AccessIPv4 setting.. // for determining the SSH address based on the server AccessIPv4 setting..
func SSHAddress(csp gophercloud.CloudServersProvider, sshinterface string, port int) func(multistep.StateBag) (string, error) { func SSHAddress(
client *gophercloud.ServiceClient,
sshinterface string, port int) func(multistep.StateBag) (string, error) {
return func(state multistep.StateBag) (string, error) { return func(state multistep.StateBag) (string, error) {
s := state.Get("server").(*gophercloud.Server) s := state.Get("server").(*servers.Server)
if ip := state.Get("access_ip").(gophercloud.FloatingIp); ip.Ip != "" { // If we have a floating IP, use that
return fmt.Sprintf("%s:%d", ip.Ip, port), nil ip := state.Get("access_ip").(*floatingip.FloatingIP)
if ip != nil && ip.IP != "" {
return fmt.Sprintf("%s:%d", ip.IP, port), nil
} }
ip_pools, err := s.AllAddressPools() if s.AccessIPv4 != "" {
if err != nil { return fmt.Sprintf("%s:%d", s.AccessIPv4, port), nil
return "", errors.New("Error parsing SSH addresses")
} }
for pool, addresses := range ip_pools {
if sshinterface != "" { // Get all the addresses associated with this server. This
if pool != sshinterface { // was taken directly from Terraform.
continue for _, networkAddresses := range s.Addresses {
} elements, ok := networkAddresses.([]interface{})
if !ok {
log.Printf(
"[ERROR] Unknown return type for address field: %#v",
networkAddresses)
continue
} }
if pool != "" {
for _, address := range addresses { for _, element := range elements {
if address.Addr != "" && address.Version == 4 { var addr string
return fmt.Sprintf("%s:%d", address.Addr, port), nil address := element.(map[string]interface{})
if address["OS-EXT-IPS:type"] == "floating" {
addr = address["addr"].(string)
} else {
if address["version"].(float64) == 4 {
addr = address["addr"].(string)
} }
} }
if addr != "" {
return fmt.Sprintf("%s:%d", addr, port), nil
}
} }
} }
serverState, err := csp.ServerById(s.Id) s, err := servers.Get(client, s.ID).Extract()
if err != nil { if err != nil {
return "", err return "", err
} }
state.Put("server", serverState) state.Put("server", s)
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
return "", errors.New("couldn't determine IP address for server") return "", errors.New("couldn't determine IP address for server")
......
...@@ -2,10 +2,11 @@ package openstack ...@@ -2,10 +2,11 @@ package openstack
import ( import (
"fmt" "fmt"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip"
"github.com/mitchellh/gophercloud-fork-40444fb" "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
) )
type StepAllocateIp struct { type StepAllocateIp struct {
...@@ -15,53 +16,83 @@ type StepAllocateIp struct { ...@@ -15,53 +16,83 @@ type StepAllocateIp struct {
func (s *StepAllocateIp) Run(state multistep.StateBag) multistep.StepAction { func (s *StepAllocateIp) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
csp := state.Get("csp").(gophercloud.CloudServersProvider) config := state.Get("config").(Config)
server := state.Get("server").(*gophercloud.Server) server := state.Get("server").(*servers.Server)
// We need the v2 compute client
client, err := config.computeV2Client()
if err != nil {
err = fmt.Errorf("Error initializing compute client: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
var instanceIp floatingip.FloatingIP
var instanceIp gophercloud.FloatingIp
// This is here in case we error out before putting instanceIp into the // This is here in case we error out before putting instanceIp into the
// statebag below, because it is requested by Cleanup() // statebag below, because it is requested by Cleanup()
state.Put("access_ip", instanceIp) state.Put("access_ip", &instanceIp)
if s.FloatingIp != "" { if s.FloatingIp != "" {
instanceIp.Ip = s.FloatingIp instanceIp.IP = s.FloatingIp
} else if s.FloatingIpPool != "" { } else if s.FloatingIpPool != "" {
newIp, err := csp.CreateFloatingIp(s.FloatingIpPool) ui.Say(fmt.Sprintf("Creating floating IP..."))
ui.Message(fmt.Sprintf("Pool: %s", s.FloatingIpPool))
newIp, err := floatingip.Create(client, floatingip.CreateOpts{
Pool: s.FloatingIpPool,
}).Extract()
if err != nil { if err != nil {
err := fmt.Errorf("Error creating floating ip from pool '%s'", s.FloatingIpPool) err := fmt.Errorf("Error creating floating ip from pool '%s'", s.FloatingIpPool)
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
} }
instanceIp = newIp
ui.Say(fmt.Sprintf("Created temporary floating IP %s...", instanceIp.Ip)) instanceIp = *newIp
ui.Message(fmt.Sprintf("Created floating IP: %s", instanceIp.IP))
} }
if instanceIp.Ip != "" { if instanceIp.IP != "" {
if err := csp.AssociateFloatingIp(server.Id, instanceIp); err != nil { ui.Say(fmt.Sprintf("Associating floating IP with server..."))
err := fmt.Errorf("Error associating floating IP %s with instance.", instanceIp.Ip) ui.Message(fmt.Sprintf("IP: %s", instanceIp.IP))
err := floatingip.Associate(client, server.ID, instanceIp.IP).ExtractErr()
if err != nil {
err := fmt.Errorf(
"Error associating floating IP %s with instance: %s",
instanceIp.IP, err)
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
} else {
ui.Say(fmt.Sprintf("Added floating IP %s to instance...", instanceIp.Ip))
} }
}
state.Put("access_ip", instanceIp) ui.Message(fmt.Sprintf(
"Added floating IP %s to instance!", instanceIp.IP))
}
state.Put("access_ip", &instanceIp)
return multistep.ActionContinue return multistep.ActionContinue
} }
func (s *StepAllocateIp) Cleanup(state multistep.StateBag) { func (s *StepAllocateIp) Cleanup(state multistep.StateBag) {
config := state.Get("config").(Config)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
csp := state.Get("csp").(gophercloud.CloudServersProvider) instanceIp := state.Get("access_ip").(*floatingip.FloatingIP)
instanceIp := state.Get("access_ip").(gophercloud.FloatingIp)
if s.FloatingIpPool != "" && instanceIp.Id != 0 { // We need the v2 compute client
if err := csp.DeleteFloatingIp(instanceIp); err != nil { client, err := config.computeV2Client()
ui.Error(fmt.Sprintf("Error deleting temporary floating IP %s", instanceIp.Ip)) if err != nil {
ui.Error(fmt.Sprintf(
"Error deleting temporary floating IP %s", instanceIp.IP))
return
}
if s.FloatingIpPool != "" && instanceIp.ID != "" {
if err := floatingip.Delete(client, instanceIp.ID).ExtractErr(); err != nil {
ui.Error(fmt.Sprintf(
"Error deleting temporary floating IP %s", instanceIp.IP))
return return
} }
ui.Say(fmt.Sprintf("Deleted temporary floating IP %s", instanceIp.Ip))
ui.Say(fmt.Sprintf("Deleted temporary floating IP %s", instanceIp.IP))
} }
} }
...@@ -2,28 +2,36 @@ package openstack ...@@ -2,28 +2,36 @@ package openstack
import ( import (
"fmt" "fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log" "log"
"time" "time"
"github.com/mitchellh/gophercloud-fork-40444fb" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/compute/v2/images"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
) )
type stepCreateImage struct{} type stepCreateImage struct{}
func (s *stepCreateImage) Run(state multistep.StateBag) multistep.StepAction { func (s *stepCreateImage) Run(state multistep.StateBag) multistep.StepAction {
csp := state.Get("csp").(gophercloud.CloudServersProvider)
config := state.Get("config").(Config) config := state.Get("config").(Config)
server := state.Get("server").(*gophercloud.Server) server := state.Get("server").(*servers.Server)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
// We need the v2 compute client
client, err := config.computeV2Client()
if err != nil {
err = fmt.Errorf("Error initializing compute client: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
// Create the image // Create the image
ui.Say(fmt.Sprintf("Creating the image: %s", config.ImageName)) ui.Say(fmt.Sprintf("Creating the image: %s", config.ImageName))
createOpts := gophercloud.CreateImage{ imageId, err := servers.CreateImage(client, server.ID, servers.CreateImageOpts{
Name: config.ImageName, Name: config.ImageName,
} }).ExtractImageID()
imageId, err := csp.CreateImage(server.Id, createOpts)
if err != nil { if err != nil {
err := fmt.Errorf("Error creating image: %s", err) err := fmt.Errorf("Error creating image: %s", err)
state.Put("error", err) state.Put("error", err)
...@@ -32,12 +40,12 @@ func (s *stepCreateImage) Run(state multistep.StateBag) multistep.StepAction { ...@@ -32,12 +40,12 @@ func (s *stepCreateImage) Run(state multistep.StateBag) multistep.StepAction {
} }
// Set the Image ID in the state // Set the Image ID in the state
ui.Say(fmt.Sprintf("Image: %s", imageId)) ui.Message(fmt.Sprintf("Image: %s", imageId))
state.Put("image", imageId) state.Put("image", imageId)
// Wait for the image to become ready // Wait for the image to become ready
ui.Say("Waiting for image to become ready...") ui.Say("Waiting for image to become ready...")
if err := WaitForImage(csp, imageId); err != nil { if err := WaitForImage(client, imageId); err != nil {
err := fmt.Errorf("Error waiting for image: %s", err) err := fmt.Errorf("Error waiting for image: %s", err)
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
...@@ -52,10 +60,17 @@ func (s *stepCreateImage) Cleanup(multistep.StateBag) { ...@@ -52,10 +60,17 @@ func (s *stepCreateImage) Cleanup(multistep.StateBag) {
} }
// WaitForImage waits for the given Image ID to become ready. // WaitForImage waits for the given Image ID to become ready.
func WaitForImage(csp gophercloud.CloudServersProvider, imageId string) error { func WaitForImage(client *gophercloud.ServiceClient, imageId string) error {
for { for {
image, err := csp.ImageById(imageId) image, err := images.Get(client, imageId).Extract()
if err != nil { if err != nil {
errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError)
if ok && errCode.Actual == 500 {
log.Printf("[ERROR] 500 error received, will ignore and retry: %s", err)
time.Sleep(2 * time.Second)
continue
}
return err return err
} }
......
...@@ -2,14 +2,13 @@ package openstack ...@@ -2,14 +2,13 @@ package openstack
import ( import (
"fmt" "fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/packer"
"log"
"os" "os"
"runtime" "runtime"
"github.com/mitchellh/gophercloud-fork-40444fb" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
) )
type StepKeyPair struct { type StepKeyPair struct {
...@@ -19,18 +18,28 @@ type StepKeyPair struct { ...@@ -19,18 +18,28 @@ type StepKeyPair struct {
} }
func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction { func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction {
csp := state.Get("csp").(gophercloud.CloudServersProvider) config := state.Get("config").(Config)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
// We need the v2 compute client
computeClient, err := config.computeV2Client()
if err != nil {
err = fmt.Errorf("Error initializing compute client: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
ui.Say("Creating temporary keypair for this instance...") ui.Say("Creating temporary keypair for this instance...")
keyName := fmt.Sprintf("packer %s", uuid.TimeOrderedUUID()) keyName := fmt.Sprintf("packer %s", uuid.TimeOrderedUUID())
log.Printf("temporary keypair name: %s", keyName) keypair, err := keypairs.Create(computeClient, keypairs.CreateOpts{
keyResp, err := csp.CreateKeyPair(gophercloud.NewKeyPair{Name: keyName}) Name: keyName,
}).Extract()
if err != nil { if err != nil {
state.Put("error", fmt.Errorf("Error creating temporary keypair: %s", err)) state.Put("error", fmt.Errorf("Error creating temporary keypair: %s", err))
return multistep.ActionHalt return multistep.ActionHalt
} }
if keyResp.PrivateKey == "" {
if keypair.PrivateKey == "" {
state.Put("error", fmt.Errorf("The temporary keypair returned was blank")) state.Put("error", fmt.Errorf("The temporary keypair returned was blank"))
return multistep.ActionHalt return multistep.ActionHalt
} }
...@@ -47,7 +56,7 @@ func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction { ...@@ -47,7 +56,7 @@ func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction {
defer f.Close() defer f.Close()
// Write the key out // Write the key out
if _, err := f.Write([]byte(keyResp.PrivateKey)); err != nil { if _, err := f.Write([]byte(keypair.PrivateKey)); err != nil {
state.Put("error", fmt.Errorf("Error saving debug key: %s", err)) state.Put("error", fmt.Errorf("Error saving debug key: %s", err))
return multistep.ActionHalt return multistep.ActionHalt
} }
...@@ -66,7 +75,7 @@ func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction { ...@@ -66,7 +75,7 @@ func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction {
// Set some state data for use in future steps // Set some state data for use in future steps
state.Put("keyPair", keyName) state.Put("keyPair", keyName)
state.Put("privateKey", keyResp.PrivateKey) state.Put("privateKey", keypair.PrivateKey)
return multistep.ActionContinue return multistep.ActionContinue
} }
...@@ -77,11 +86,19 @@ func (s *StepKeyPair) Cleanup(state multistep.StateBag) { ...@@ -77,11 +86,19 @@ func (s *StepKeyPair) Cleanup(state multistep.StateBag) {
return return
} }
csp := state.Get("csp").(gophercloud.CloudServersProvider) config := state.Get("config").(Config)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
// We need the v2 compute client
computeClient, err := config.computeV2Client()
if err != nil {
ui.Error(fmt.Sprintf(
"Error cleaning up keypair. Please delete the key manually: %s", s.keyName))
return
}
ui.Say("Deleting temporary keypair...") ui.Say("Deleting temporary keypair...")
err := csp.DeleteKeyPair(s.keyName) err = keypairs.Delete(computeClient, s.keyName).ExtractErr()
if err != nil { if err != nil {
ui.Error(fmt.Sprintf( ui.Error(fmt.Sprintf(
"Error cleaning up keypair. Please delete the key manually: %s", s.keyName)) "Error cleaning up keypair. Please delete the key manually: %s", s.keyName))
......
package openstack
import (
"fmt"
"log"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
)
// StepLoadFlavor gets the FlavorRef from a Flavor. It first assumes
// that the Flavor is a ref and verifies it. Otherwise, it tries to find
// the flavor by name.
type StepLoadFlavor struct {
Flavor string
}
func (s *StepLoadFlavor) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(Config)
ui := state.Get("ui").(packer.Ui)
// We need the v2 compute client
client, err := config.computeV2Client()
if err != nil {
err = fmt.Errorf("Error initializing compute client: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
ui.Say(fmt.Sprintf("Loading flavor: %s", s.Flavor))
log.Printf("[INFO] Loading flavor by ID: %s", s.Flavor)
flavor, err := flavors.Get(client, s.Flavor).Extract()
if err != nil {
log.Printf("[ERROR] Failed to find flavor by ID: %s", err)
geterr := err
log.Printf("[INFO] Loading flavor by name: %s", s.Flavor)
id, err := flavors.IDFromName(client, s.Flavor)
if err != nil {
log.Printf("[ERROR] Failed to find flavor by name: %s", err)
err = fmt.Errorf(
"Unable to find specified flavor by ID or name!\n\n"+
"Error from ID lookup: %s\n\n"+
"Error from name lookup: %s",
geterr,
err)
state.Put("error", err)
return multistep.ActionHalt
}
flavor = &flavors.Flavor{ID: id}
}
ui.Message(fmt.Sprintf("Verified flavor. ID: %s", flavor.ID))
state.Put("flavor_id", flavor.ID)
return multistep.ActionContinue
}
func (s *StepLoadFlavor) Cleanup(state multistep.StateBag) {
}
...@@ -2,51 +2,56 @@ package openstack ...@@ -2,51 +2,56 @@ package openstack
import ( import (
"fmt" "fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log" "log"
"github.com/mitchellh/gophercloud-fork-40444fb" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
) )
type StepRunSourceServer struct { type StepRunSourceServer struct {
Flavor string Name string
Name string SourceImage string
SourceImage string SecurityGroups []string
SecurityGroups []string Networks []string
Networks []string AvailabilityZone string
server *gophercloud.Server server *servers.Server
} }
func (s *StepRunSourceServer) Run(state multistep.StateBag) multistep.StepAction { func (s *StepRunSourceServer) Run(state multistep.StateBag) multistep.StepAction {
csp := state.Get("csp").(gophercloud.CloudServersProvider) config := state.Get("config").(Config)
flavor := state.Get("flavor_id").(string)
keyName := state.Get("keyPair").(string) keyName := state.Get("keyPair").(string)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
// XXX - validate image and flavor is available // We need the v2 compute client
computeClient, err := config.computeV2Client()
securityGroups := make([]map[string]interface{}, len(s.SecurityGroups)) if err != nil {
for i, groupName := range s.SecurityGroups { err = fmt.Errorf("Error initializing compute client: %s", err)
securityGroups[i] = make(map[string]interface{}) state.Put("error", err)
securityGroups[i]["name"] = groupName return multistep.ActionHalt
} }
networks := make([]gophercloud.NetworkConfig, len(s.Networks)) networks := make([]servers.Network, len(s.Networks))
for i, networkUuid := range s.Networks { for i, networkUuid := range s.Networks {
networks[i].Uuid = networkUuid networks[i].UUID = networkUuid
} }
server := gophercloud.NewServer{ ui.Say("Launching server...")
Name: s.Name, s.server, err = servers.Create(computeClient, keypairs.CreateOptsExt{
ImageRef: s.SourceImage, CreateOptsBuilder: servers.CreateOpts{
FlavorRef: s.Flavor, Name: s.Name,
KeyPairName: keyName, ImageRef: s.SourceImage,
SecurityGroup: securityGroups, FlavorRef: flavor,
Networks: networks, SecurityGroups: s.SecurityGroups,
} Networks: networks,
AvailabilityZone: s.AvailabilityZone,
serverResp, err := csp.CreateServer(server) },
KeyName: keyName,
}).Extract()
if err != nil { if err != nil {
err := fmt.Errorf("Error launching source server: %s", err) err := fmt.Errorf("Error launching source server: %s", err)
state.Put("error", err) state.Put("error", err)
...@@ -54,25 +59,25 @@ func (s *StepRunSourceServer) Run(state multistep.StateBag) multistep.StepAction ...@@ -54,25 +59,25 @@ func (s *StepRunSourceServer) Run(state multistep.StateBag) multistep.StepAction
return multistep.ActionHalt return multistep.ActionHalt
} }
s.server, err = csp.ServerById(serverResp.Id) ui.Message(fmt.Sprintf("Server ID: %s", s.server.ID))
log.Printf("server id: %s", s.server.Id) log.Printf("server id: %s", s.server.ID)
ui.Say(fmt.Sprintf("Waiting for server (%s) to become ready...", s.server.Id)) ui.Say("Waiting for server to become ready...")
stateChange := StateChangeConf{ stateChange := StateChangeConf{
Pending: []string{"BUILD"}, Pending: []string{"BUILD"},
Target: "ACTIVE", Target: "ACTIVE",
Refresh: ServerStateRefreshFunc(csp, s.server), Refresh: ServerStateRefreshFunc(computeClient, s.server),
StepState: state, StepState: state,
} }
latestServer, err := WaitForState(&stateChange) latestServer, err := WaitForState(&stateChange)
if err != nil { if err != nil {
err := fmt.Errorf("Error waiting for server (%s) to become ready: %s", s.server.Id, err) err := fmt.Errorf("Error waiting for server (%s) to become ready: %s", s.server.ID, err)
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
} }
s.server = latestServer.(*gophercloud.Server) s.server = latestServer.(*servers.Server)
state.Put("server", s.server) state.Put("server", s.server)
return multistep.ActionContinue return multistep.ActionContinue
...@@ -83,18 +88,25 @@ func (s *StepRunSourceServer) Cleanup(state multistep.StateBag) { ...@@ -83,18 +88,25 @@ func (s *StepRunSourceServer) Cleanup(state multistep.StateBag) {
return return
} }
csp := state.Get("csp").(gophercloud.CloudServersProvider) config := state.Get("config").(Config)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
// We need the v2 compute client
computeClient, err := config.computeV2Client()
if err != nil {
ui.Error(fmt.Sprintf("Error terminating server, may still be around: %s", err))
return
}
ui.Say("Terminating the source server...") ui.Say("Terminating the source server...")
if err := csp.DeleteServerById(s.server.Id); err != nil { if err := servers.Delete(computeClient, s.server.ID).ExtractErr(); err != nil {
ui.Error(fmt.Sprintf("Error terminating server, may still be around: %s", err)) ui.Error(fmt.Sprintf("Error terminating server, may still be around: %s", err))
return return
} }
stateChange := StateChangeConf{ stateChange := StateChangeConf{
Pending: []string{"ACTIVE", "BUILD", "REBUILD", "SUSPENDED"}, Pending: []string{"ACTIVE", "BUILD", "REBUILD", "SUSPENDED"},
Refresh: ServerStateRefreshFunc(csp, s.server), Refresh: ServerStateRefreshFunc(computeClient, s.server),
Target: "DELETED", Target: "DELETED",
} }
......
...@@ -2,11 +2,11 @@ package openstack ...@@ -2,11 +2,11 @@ package openstack
import ( import (
"fmt" "fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"time" "time"
"github.com/mitchellh/gophercloud-fork-40444fb" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
) )
type StepWaitForRackConnect struct { type StepWaitForRackConnect struct {
...@@ -18,14 +18,22 @@ func (s *StepWaitForRackConnect) Run(state multistep.StateBag) multistep.StepAct ...@@ -18,14 +18,22 @@ func (s *StepWaitForRackConnect) Run(state multistep.StateBag) multistep.StepAct
return multistep.ActionContinue return multistep.ActionContinue
} }
csp := state.Get("csp").(gophercloud.CloudServersProvider) config := state.Get("config").(Config)
server := state.Get("server").(*gophercloud.Server) server := state.Get("server").(*servers.Server)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
ui.Say(fmt.Sprintf("Waiting for server (%s) to become RackConnect ready...", server.Id)) // We need the v2 compute client
computeClient, err := config.computeV2Client()
if err != nil {
err = fmt.Errorf("Error initializing compute client: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
ui.Say(fmt.Sprintf(
"Waiting for server (%s) to become RackConnect ready...", server.ID))
for { for {
server, err := csp.ServerById(server.Id) server, err = servers.Get(computeClient, server.ID).Extract()
if err != nil { if err != nil {
return multistep.ActionHalt return multistep.ActionHalt
} }
......
...@@ -73,6 +73,12 @@ func NewDriver() (Driver, error) { ...@@ -73,6 +73,12 @@ func NewDriver() (Driver, error) {
log.Printf("prlctl path: %s", prlctlPath) log.Printf("prlctl path: %s", prlctlPath)
drivers = map[string]Driver{ drivers = map[string]Driver{
"11": &Parallels10Driver{
Parallels9Driver: Parallels9Driver{
PrlctlPath: prlctlPath,
dhcp_lease_file: dhcp_lease_file,
},
},
"10": &Parallels10Driver{ "10": &Parallels10Driver{
Parallels9Driver: Parallels9Driver{ Parallels9Driver: Parallels9Driver{
PrlctlPath: prlctlPath, PrlctlPath: prlctlPath,
......
package common package common
// Parallels10Driver are inherited from Parallels9Driver. // Parallels10Driver are inherited from Parallels9Driver.
// Used for Parallels v 10 & 11
type Parallels10Driver struct { type Parallels10Driver struct {
Parallels9Driver Parallels9Driver
} }
...@@ -33,7 +33,7 @@ type Config struct { ...@@ -33,7 +33,7 @@ type Config struct {
func NewConfig(raws ...interface{}) (*Config, []string, error) { func NewConfig(raws ...interface{}) (*Config, []string, error) {
c := new(Config) c := new(Config)
err := config.Decode(&c, &config.DecodeOpts{ err := config.Decode(c, &config.DecodeOpts{
Interpolate: true, Interpolate: true,
InterpolateFilter: &interpolate.RenderFilter{ InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{ Exclude: []string{
......
...@@ -18,6 +18,7 @@ type SSHConfig struct { ...@@ -18,6 +18,7 @@ type SSHConfig struct {
SSHPort uint `mapstructure:"ssh_port"` SSHPort uint `mapstructure:"ssh_port"`
SSHUser string `mapstructure:"ssh_username"` SSHUser string `mapstructure:"ssh_username"`
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"` RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
SSHSkipNatMapping bool `mapstructure:"ssh_skip_nat_mapping"`
SSHWaitTimeout time.Duration SSHWaitTimeout time.Duration
} }
......
...@@ -17,9 +17,10 @@ import ( ...@@ -17,9 +17,10 @@ import (
// Produces: // Produces:
// exportPath string - The path to the resulting export. // exportPath string - The path to the resulting export.
type StepExport struct { type StepExport struct {
Format string Format string
OutputDir string OutputDir string
ExportOpts []string ExportOpts []string
SkipNatMapping bool
} }
func (s *StepExport) Run(state multistep.StateBag) multistep.StepAction { func (s *StepExport) Run(state multistep.StateBag) multistep.StepAction {
...@@ -30,30 +31,31 @@ func (s *StepExport) Run(state multistep.StateBag) multistep.StepAction { ...@@ -30,30 +31,31 @@ func (s *StepExport) Run(state multistep.StateBag) multistep.StepAction {
// Wait a second to ensure VM is really shutdown. // Wait a second to ensure VM is really shutdown.
log.Println("1 second timeout to ensure VM is really shutdown") log.Println("1 second timeout to ensure VM is really shutdown")
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
ui.Say("Preparing to export machine...")
// Clear out the Packer-created forwarding rule // Clear out the Packer-created forwarding rule
ui.Say("Preparing to export machine...") if !s.SkipNatMapping {
ui.Message(fmt.Sprintf( ui.Message(fmt.Sprintf(
"Deleting forwarded port mapping for SSH (host port %d)", "Deleting forwarded port mapping for SSH (host port %d)",
state.Get("sshHostPort"))) state.Get("sshHostPort")))
command := []string{"modifyvm", vmName, "--natpf1", "delete", "packerssh"} command := []string{"modifyvm", vmName, "--natpf1", "delete", "packerssh"}
if err := driver.VBoxManage(command...); err != nil { if err := driver.VBoxManage(command...); err != nil {
err := fmt.Errorf("Error deleting port forwarding rule: %s", err) err := fmt.Errorf("Error deleting port forwarding rule: %s", err)
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
}
} }
// Export the VM to an OVF // Export the VM to an OVF
outputPath := filepath.Join(s.OutputDir, vmName+"."+s.Format) outputPath := filepath.Join(s.OutputDir, vmName+"."+s.Format)
command = []string{ command := []string{
"export", "export",
vmName, vmName,
"--output", "--output",
outputPath, outputPath,
} }
command = append(command, s.ExportOpts...) command = append(command, s.ExportOpts...)
ui.Say("Exporting virtual machine...") ui.Say("Exporting virtual machine...")
......
...@@ -19,9 +19,10 @@ import ( ...@@ -19,9 +19,10 @@ import (
// //
// Produces: // Produces:
type StepForwardSSH struct { type StepForwardSSH struct {
GuestPort uint GuestPort uint
HostPortMin uint HostPortMin uint
HostPortMax uint HostPortMax uint
SkipNatMapping bool
} }
func (s *StepForwardSSH) Run(state multistep.StateBag) multistep.StepAction { func (s *StepForwardSSH) Run(state multistep.StateBag) multistep.StepAction {
...@@ -29,39 +30,41 @@ func (s *StepForwardSSH) Run(state multistep.StateBag) multistep.StepAction { ...@@ -29,39 +30,41 @@ func (s *StepForwardSSH) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
vmName := state.Get("vmName").(string) vmName := state.Get("vmName").(string)
log.Printf("Looking for available SSH port between %d and %d", sshHostPort := s.GuestPort
s.HostPortMin, s.HostPortMax) if !s.SkipNatMapping {
var sshHostPort uint log.Printf("Looking for available SSH port between %d and %d",
var offset uint = 0 s.HostPortMin, s.HostPortMax)
var offset uint = 0
portRange := int(s.HostPortMax - s.HostPortMin) portRange := int(s.HostPortMax - s.HostPortMin)
if portRange > 0 { if portRange > 0 {
// Have to check if > 0 to avoid a panic // Have to check if > 0 to avoid a panic
offset = uint(rand.Intn(portRange)) offset = uint(rand.Intn(portRange))
} }
for { for {
sshHostPort = offset + s.HostPortMin sshHostPort = offset + s.HostPortMin
log.Printf("Trying port: %d", sshHostPort) log.Printf("Trying port: %d", sshHostPort)
l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", sshHostPort)) l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", sshHostPort))
if err == nil { if err == nil {
defer l.Close() defer l.Close()
break break
}
} }
}
// Create a forwarded port mapping to the VM // Create a forwarded port mapping to the VM
ui.Say(fmt.Sprintf("Creating forwarded port mapping for SSH (host port %d)", sshHostPort)) ui.Say(fmt.Sprintf("Creating forwarded port mapping for SSH (host port %d)", sshHostPort))
command := []string{ command := []string{
"modifyvm", vmName, "modifyvm", vmName,
"--natpf1", "--natpf1",
fmt.Sprintf("packerssh,tcp,127.0.0.1,%d,,%d", sshHostPort, s.GuestPort), fmt.Sprintf("packerssh,tcp,127.0.0.1,%d,,%d", sshHostPort, s.GuestPort),
} }
if err := driver.VBoxManage(command...); err != nil { if err := driver.VBoxManage(command...); err != nil {
err := fmt.Errorf("Error creating port forwarding rule: %s", err) err := fmt.Errorf("Error creating port forwarding rule: %s", err)
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
}
} }
// Save the port we're using so that future steps can use it // Save the port we're using so that future steps can use it
......
...@@ -230,6 +230,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -230,6 +230,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Description: "ISO", Description: "ISO",
ResultKey: "iso_path", ResultKey: "iso_path",
Url: b.config.ISOUrls, Url: b.config.ISOUrls,
Extension: "iso",
}, },
&vboxcommon.StepOutputDir{ &vboxcommon.StepOutputDir{
Force: b.config.PackerForce, Force: b.config.PackerForce,
...@@ -252,9 +253,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -252,9 +253,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}, },
new(vboxcommon.StepAttachFloppy), new(vboxcommon.StepAttachFloppy),
&vboxcommon.StepForwardSSH{ &vboxcommon.StepForwardSSH{
GuestPort: b.config.SSHPort, GuestPort: b.config.SSHPort,
HostPortMin: b.config.SSHHostPortMin, HostPortMin: b.config.SSHHostPortMin,
HostPortMax: b.config.SSHHostPortMax, HostPortMax: b.config.SSHHostPortMax,
SkipNatMapping: b.config.SSHSkipNatMapping,
}, },
&vboxcommon.StepVBoxManage{ &vboxcommon.StepVBoxManage{
Commands: b.config.VBoxManage, Commands: b.config.VBoxManage,
...@@ -293,9 +295,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -293,9 +295,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Ctx: b.config.ctx, Ctx: b.config.ctx,
}, },
&vboxcommon.StepExport{ &vboxcommon.StepExport{
Format: b.config.Format, Format: b.config.Format,
OutputDir: b.config.OutputDir, OutputDir: b.config.OutputDir,
ExportOpts: b.config.ExportOpts.ExportOpts, ExportOpts: b.config.ExportOpts.ExportOpts,
SkipNatMapping: b.config.SSHSkipNatMapping,
}, },
} }
......
...@@ -82,9 +82,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -82,9 +82,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}, },
new(vboxcommon.StepAttachFloppy), new(vboxcommon.StepAttachFloppy),
&vboxcommon.StepForwardSSH{ &vboxcommon.StepForwardSSH{
GuestPort: b.config.SSHPort, GuestPort: b.config.SSHPort,
HostPortMin: b.config.SSHHostPortMin, HostPortMin: b.config.SSHHostPortMin,
HostPortMax: b.config.SSHHostPortMax, HostPortMax: b.config.SSHHostPortMax,
SkipNatMapping: b.config.SSHSkipNatMapping,
}, },
&vboxcommon.StepVBoxManage{ &vboxcommon.StepVBoxManage{
Commands: b.config.VBoxManage, Commands: b.config.VBoxManage,
...@@ -123,9 +124,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -123,9 +124,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Ctx: b.config.ctx, Ctx: b.config.ctx,
}, },
&vboxcommon.StepExport{ &vboxcommon.StepExport{
Format: b.config.Format, Format: b.config.Format,
OutputDir: b.config.OutputDir, OutputDir: b.config.OutputDir,
ExportOpts: b.config.ExportOpts.ExportOpts, ExportOpts: b.config.ExportOpts.ExportOpts,
SkipNatMapping: b.config.SSHSkipNatMapping,
}, },
} }
......
...@@ -40,8 +40,8 @@ type Config struct { ...@@ -40,8 +40,8 @@ type Config struct {
} }
func NewConfig(raws ...interface{}) (*Config, []string, error) { func NewConfig(raws ...interface{}) (*Config, []string, error) {
var c Config c := new(Config)
err := config.Decode(&c, &config.DecodeOpts{ err := config.Decode(c, &config.DecodeOpts{
Interpolate: true, Interpolate: true,
InterpolateFilter: &interpolate.RenderFilter{ InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{ Exclude: []string{
...@@ -132,5 +132,5 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { ...@@ -132,5 +132,5 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
c.ImportFlags = append(c.ImportFlags, "--options", c.ImportOpts) c.ImportFlags = append(c.ImportFlags, "--options", c.ImportOpts)
} }
return &c, warnings, nil return c, warnings, nil
} }
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"log" "log"
"os/exec" "os/exec"
"regexp"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
...@@ -135,6 +136,18 @@ func runAndLog(cmd *exec.Cmd) (string, string, error) { ...@@ -135,6 +136,18 @@ func runAndLog(cmd *exec.Cmd) (string, string, error) {
} }
err = fmt.Errorf("VMware error: %s", message) err = fmt.Errorf("VMware error: %s", message)
// If "unknown error" is in there, add some additional notes
re := regexp.MustCompile(`(?i)unknown error`)
if re.MatchString(message) {
err = fmt.Errorf(
"%s\n\n%s", err,
"Packer detected a VMware 'Unknown Error'. Unfortunately VMware\n"+
"often has extremely vague error messages such as this and Packer\n"+
"itself can't do much about that. Please check the vmware.log files\n"+
"created by VMware when a VM is started (in the directory of the\n"+
"vmx file), which often contains more detailed error information.")
}
} }
log.Printf("stdout: %s", stdoutString) log.Printf("stdout: %s", stdoutString)
......
...@@ -36,6 +36,18 @@ func (s StepCompactDisk) Run(state multistep.StateBag) multistep.StepAction { ...@@ -36,6 +36,18 @@ func (s StepCompactDisk) Run(state multistep.StateBag) multistep.StepAction {
state.Put("error", fmt.Errorf("Error compacting disk: %s", err)) state.Put("error", fmt.Errorf("Error compacting disk: %s", err))
return multistep.ActionHalt return multistep.ActionHalt
} }
if state.Get("additional_disk_paths") != nil {
if moreDisks := state.Get("additional_disk_paths").([]string); len(moreDisks) > 0 {
for i, path := range moreDisks {
ui.Say(fmt.Sprintf("Compacting additional disk image %d",i+1))
if err := driver.CompactDisk(path); err != nil {
state.Put("error", fmt.Errorf("Error compacting additional disk %d: %s", i+1, err))
return multistep.ActionHalt
}
}
}
}
return multistep.ActionContinue return multistep.ActionContinue
} }
......
...@@ -35,19 +35,20 @@ type Config struct { ...@@ -35,19 +35,20 @@ type Config struct {
vmwcommon.ToolsConfig `mapstructure:",squash"` vmwcommon.ToolsConfig `mapstructure:",squash"`
vmwcommon.VMXConfig `mapstructure:",squash"` vmwcommon.VMXConfig `mapstructure:",squash"`
DiskName string `mapstructure:"vmdk_name"` AdditionalDiskSize []uint `mapstructure:"disk_additional_size"`
DiskSize uint `mapstructure:"disk_size"` DiskName string `mapstructure:"vmdk_name"`
DiskTypeId string `mapstructure:"disk_type_id"` DiskSize uint `mapstructure:"disk_size"`
FloppyFiles []string `mapstructure:"floppy_files"` DiskTypeId string `mapstructure:"disk_type_id"`
GuestOSType string `mapstructure:"guest_os_type"` FloppyFiles []string `mapstructure:"floppy_files"`
ISOChecksum string `mapstructure:"iso_checksum"` GuestOSType string `mapstructure:"guest_os_type"`
ISOChecksumType string `mapstructure:"iso_checksum_type"` ISOChecksum string `mapstructure:"iso_checksum"`
ISOUrls []string `mapstructure:"iso_urls"` ISOChecksumType string `mapstructure:"iso_checksum_type"`
Version string `mapstructure:"version"` ISOUrls []string `mapstructure:"iso_urls"`
VMName string `mapstructure:"vm_name"` Version string `mapstructure:"version"`
BootCommand []string `mapstructure:"boot_command"` VMName string `mapstructure:"vm_name"`
SkipCompaction bool `mapstructure:"skip_compaction"` BootCommand []string `mapstructure:"boot_command"`
VMXTemplatePath string `mapstructure:"vmx_template_path"` SkipCompaction bool `mapstructure:"skip_compaction"`
VMXTemplatePath string `mapstructure:"vmx_template_path"`
RemoteType string `mapstructure:"remote_type"` RemoteType string `mapstructure:"remote_type"`
RemoteDatastore string `mapstructure:"remote_datastore"` RemoteDatastore string `mapstructure:"remote_datastore"`
......
...@@ -311,8 +311,8 @@ func (d *ESX5Driver) String() string { ...@@ -311,8 +311,8 @@ func (d *ESX5Driver) String() string {
} }
func (d *ESX5Driver) datastorePath(path string) string { func (d *ESX5Driver) datastorePath(path string) string {
baseDir := filepath.Base(filepath.Dir(path)) dirPath := filepath.Dir(path)
return filepath.ToSlash(filepath.Join("/vmfs/volumes", d.Datastore, baseDir, filepath.Base(path))) return filepath.ToSlash(filepath.Join("/vmfs/volumes", d.Datastore, dirPath, filepath.Base(path)))
} }
func (d *ESX5Driver) cachePath(path string) string { func (d *ESX5Driver) cachePath(path string) string {
......
...@@ -35,6 +35,28 @@ func (stepCreateDisk) Run(state multistep.StateBag) multistep.StepAction { ...@@ -35,6 +35,28 @@ func (stepCreateDisk) Run(state multistep.StateBag) multistep.StepAction {
state.Put("full_disk_path", full_disk_path) state.Put("full_disk_path", full_disk_path)
if len(config.AdditionalDiskSize) > 0 {
// stash the disk paths we create
additional_paths := make([]string, len(config.AdditionalDiskSize))
ui.Say("Creating additional hard drives...")
for i, additionalsize := range config.AdditionalDiskSize {
additionalpath := filepath.Join(config.OutputDir, fmt.Sprintf("%s-%d.vmdk", config.DiskName, i+1))
size := fmt.Sprintf("%dM", uint64(additionalsize))
if err := driver.CreateDisk(additionalpath, size, config.DiskTypeId); err != nil {
err := fmt.Errorf("Error creating additional disk: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
additional_paths[i] = additionalpath
}
state.Put("additional_disk_paths", additional_paths)
}
return multistep.ActionContinue return multistep.ActionContinue
} }
......
...@@ -20,6 +20,11 @@ type vmxTemplateData struct { ...@@ -20,6 +20,11 @@ type vmxTemplateData struct {
Version string Version string
} }
type additionalDiskTemplateData struct {
DiskNumber int
DiskName string
}
// This step creates the VMX file for the VM. // This step creates the VMX file for the VM.
// //
// Uses: // Uses:
...@@ -40,15 +45,6 @@ func (s *stepCreateVMX) Run(state multistep.StateBag) multistep.StepAction { ...@@ -40,15 +45,6 @@ func (s *stepCreateVMX) Run(state multistep.StateBag) multistep.StepAction {
ui.Say("Building and writing VMX file") ui.Say("Building and writing VMX file")
ctx := config.ctx
ctx.Data = &vmxTemplateData{
Name: config.VMName,
GuestOS: config.GuestOSType,
DiskName: config.DiskName,
Version: config.Version,
ISOPath: isoPath,
}
vmxTemplate := DefaultVMXTemplate vmxTemplate := DefaultVMXTemplate
if config.VMXTemplatePath != "" { if config.VMXTemplatePath != "" {
f, err := os.Open(config.VMXTemplatePath) f, err := os.Open(config.VMXTemplatePath)
...@@ -71,6 +67,35 @@ func (s *stepCreateVMX) Run(state multistep.StateBag) multistep.StepAction { ...@@ -71,6 +67,35 @@ func (s *stepCreateVMX) Run(state multistep.StateBag) multistep.StepAction {
vmxTemplate = string(rawBytes) vmxTemplate = string(rawBytes)
} }
ctx := config.ctx
if len(config.AdditionalDiskSize) > 0 {
for i, _ := range config.AdditionalDiskSize {
ctx.Data = &additionalDiskTemplateData{
DiskNumber: i + 1,
DiskName: config.DiskName,
}
diskTemplate, err := interpolate.Render(DefaultAdditionalDiskTemplate, &ctx)
if err != nil {
err := fmt.Errorf("Error preparing VMX template for additional disk: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
vmxTemplate += diskTemplate
}
}
ctx.Data = &vmxTemplateData{
Name: config.VMName,
GuestOS: config.GuestOSType,
DiskName: config.DiskName,
Version: config.Version,
ISOPath: isoPath,
}
vmxContents, err := interpolate.Render(vmxTemplate, &ctx) vmxContents, err := interpolate.Render(vmxTemplate, &ctx)
if err != nil { if err != nil {
err := fmt.Errorf("Error procesing VMX template: %s", err) err := fmt.Errorf("Error procesing VMX template: %s", err)
...@@ -191,3 +216,9 @@ vmci0.pciSlotNumber = "35" ...@@ -191,3 +216,9 @@ vmci0.pciSlotNumber = "35"
vmci0.present = "TRUE" vmci0.present = "TRUE"
vmotion.checkpointFBSize = "65536000" vmotion.checkpointFBSize = "65536000"
` `
const DefaultAdditionalDiskTemplate = `
scsi0:{{ .DiskNumber }}.fileName = "{{ .DiskName}}-{{ .DiskNumber }}.vmdk"
scsi0:{{ .DiskNumber }}.present = "TRUE"
scsi0:{{ .DiskNumber }}.redo = ""
`
...@@ -157,7 +157,7 @@ func (c *PushCommand) Run(args []string) int { ...@@ -157,7 +157,7 @@ func (c *PushCommand) Run(args []string) int {
// Build the upload options // Build the upload options
var uploadOpts uploadOpts var uploadOpts uploadOpts
uploadOpts.Slug = push.Name uploadOpts.Slug = name
uploadOpts.Builds = make(map[string]*uploadBuildInfo) uploadOpts.Builds = make(map[string]*uploadBuildInfo)
for _, b := range tpl.Builders { for _, b := range tpl.Builders {
info := &uploadBuildInfo{Type: b.Type} info := &uploadBuildInfo{Type: b.Type}
...@@ -236,7 +236,7 @@ func (c *PushCommand) Run(args []string) int { ...@@ -236,7 +236,7 @@ func (c *PushCommand) Run(args []string) int {
return 1 return 1
} }
c.Ui.Say(fmt.Sprintf("Push successful to '%s'", push.Name)) c.Ui.Say(fmt.Sprintf("Push successful to '%s'", name))
return 0 return 0
} }
......
...@@ -3,13 +3,14 @@ package common ...@@ -3,13 +3,14 @@ package common
import ( import (
"errors" "errors"
"fmt" "fmt"
"log"
"strings"
"time"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/communicator/ssh" "github.com/mitchellh/packer/communicator/ssh"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
gossh "golang.org/x/crypto/ssh" gossh "golang.org/x/crypto/ssh"
"log"
"strings"
"time"
) )
// StepConnectSSH is a multistep Step implementation that waits for SSH // StepConnectSSH is a multistep Step implementation that waits for SSH
...@@ -64,6 +65,7 @@ WaitLoop: ...@@ -64,6 +65,7 @@ WaitLoop:
case <-waitDone: case <-waitDone:
if err != nil { if err != nil {
ui.Error(fmt.Sprintf("Error waiting for SSH: %s", err)) ui.Error(fmt.Sprintf("Error waiting for SSH: %s", err))
state.Put("error", err)
return multistep.ActionHalt return multistep.ActionHalt
} }
......
package common package common
import ( import (
"crypto/sha1"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"log" "log"
...@@ -36,6 +37,12 @@ type StepDownload struct { ...@@ -36,6 +37,12 @@ type StepDownload struct {
// A list of URLs to attempt to download this thing. // A list of URLs to attempt to download this thing.
Url []string Url []string
// Extension is the extension to force for the file that is downloaded.
// Some systems require a certain extension. If this isn't set, the
// extension on the URL is used. Otherwise, this will be forced
// on the downloaded file for every URL.
Extension string
} }
func (s *StepDownload) Run(state multistep.StateBag) multistep.StepAction { func (s *StepDownload) Run(state multistep.StateBag) multistep.StepAction {
...@@ -60,9 +67,19 @@ func (s *StepDownload) Run(state multistep.StateBag) multistep.StepAction { ...@@ -60,9 +67,19 @@ func (s *StepDownload) Run(state multistep.StateBag) multistep.StepAction {
targetPath := s.TargetPath targetPath := s.TargetPath
if targetPath == "" { if targetPath == "" {
// Determine a cache key. This is normally just the URL but
// if we force a certain extension we hash the URL and add
// the extension to force it.
cacheKey := url
if s.Extension != "" {
hash := sha1.Sum([]byte(url))
cacheKey = fmt.Sprintf(
"%s.%s", hex.EncodeToString(hash[:]), s.Extension)
}
log.Printf("Acquiring lock to download: %s", url) log.Printf("Acquiring lock to download: %s", url)
targetPath = cache.Lock(url) targetPath = cache.Lock(cacheKey)
defer cache.Unlock(url) defer cache.Unlock(cacheKey)
} }
config := &DownloadConfig{ config := &DownloadConfig{
......
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"log" "log"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"github.com/mitchellh/osext" "github.com/mitchellh/osext"
...@@ -172,6 +173,15 @@ func (c *config) discoverSingle(glob string, m *map[string]string) error { ...@@ -172,6 +173,15 @@ func (c *config) discoverSingle(glob string, m *map[string]string) error {
for _, match := range matches { for _, match := range matches {
file := filepath.Base(match) file := filepath.Base(match)
// One Windows, ignore any plugins that don't end in .exe.
// We could do a full PATHEXT parse, but this is probably good enough.
if runtime.GOOS == "windows" && strings.ToLower(filepath.Ext(file)) != ".exe" {
log.Printf(
"[DEBUG] Ignoring plugin match %s, no exe extension",
match)
continue
}
// If the filename has a ".", trim up to there // If the filename has a ".", trim up to there
if idx := strings.Index(file, "."); idx >= 0 { if idx := strings.Index(file, "."); idx >= 0 {
file = file[:idx] file = file[:idx]
......
...@@ -94,7 +94,7 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { ...@@ -94,7 +94,7 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
} }
if p.config.InlineShebang == "" { if p.config.InlineShebang == "" {
p.config.InlineShebang = "/bin/sh" p.config.InlineShebang = "/bin/sh -e"
} }
if p.config.RawStartRetryTimeout == "" { if p.config.RawStartRetryTimeout == "" {
...@@ -247,11 +247,11 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { ...@@ -247,11 +247,11 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
} }
cmd = &packer.RemoteCmd{ cmd = &packer.RemoteCmd{
Command: fmt.Sprintf("chmod 0777 %s", p.config.RemotePath), Command: fmt.Sprintf("chmod 0755 %s", p.config.RemotePath),
} }
if err := comm.Start(cmd); err != nil { if err := comm.Start(cmd); err != nil {
return fmt.Errorf( return fmt.Errorf(
"Error chmodding script file to 0777 in remote "+ "Error chmodding script file to 0755 in remote "+
"machine: %s", err) "machine: %s", err)
} }
cmd.Wait() cmd.Wait()
......
...@@ -45,7 +45,7 @@ func TestProvisionerPrepare_InlineShebang(t *testing.T) { ...@@ -45,7 +45,7 @@ func TestProvisionerPrepare_InlineShebang(t *testing.T) {
t.Fatalf("should not have error: %s", err) t.Fatalf("should not have error: %s", err)
} }
if p.config.InlineShebang != "/bin/sh" { if p.config.InlineShebang != "/bin/sh -e" {
t.Fatalf("bad value: %s", p.config.InlineShebang) t.Fatalf("bad value: %s", p.config.InlineShebang)
} }
......
...@@ -4,9 +4,9 @@ package main ...@@ -4,9 +4,9 @@ package main
var GitCommit string var GitCommit string
// The main version number that is being run at the moment. // The main version number that is being run at the moment.
const Version = "0.7.5" const Version = "0.8.0"
// A pre-release marker for the version. If this is "" (empty string) // A pre-release marker for the version. If this is "" (empty string)
// then it means that it is a final release. Otherwise, this is a pre-release // then it means that it is a final release. Otherwise, this is a pre-release
// such as "dev" (in development), "beta", "rc1", etc. // such as "dev" (in development), "beta", "rc1", etc.
const VersionPrerelease = "" const VersionPrerelease = "dev"
...@@ -79,18 +79,18 @@ GEM ...@@ -79,18 +79,18 @@ GEM
celluloid (~> 0.16.0) celluloid (~> 0.16.0)
rb-fsevent (>= 0.9.3) rb-fsevent (>= 0.9.3)
rb-inotify (>= 0.9) rb-inotify (>= 0.9)
middleman (3.3.13) middleman (3.3.12)
coffee-script (~> 2.2) coffee-script (~> 2.2)
compass (>= 1.0.0, < 2.0.0) compass (>= 1.0.0, < 2.0.0)
compass-import-once (= 1.0.5) compass-import-once (= 1.0.5)
execjs (~> 2.0) execjs (~> 2.0)
haml (>= 4.0.5) haml (>= 4.0.5)
kramdown (~> 1.2) kramdown (~> 1.2)
middleman-core (= 3.3.13) middleman-core (= 3.3.12)
middleman-sprockets (>= 3.1.2) middleman-sprockets (>= 3.1.2)
sass (>= 3.4.0, < 4.0) sass (>= 3.4.0, < 4.0)
uglifier (~> 2.5) uglifier (~> 2.5)
middleman-core (3.3.13) middleman-core (3.3.12)
activesupport (~> 4.1.0) activesupport (~> 4.1.0)
bundler (~> 1.1) bundler (~> 1.1)
erubis erubis
...@@ -175,3 +175,6 @@ PLATFORMS ...@@ -175,3 +175,6 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
middleman-hashicorp! middleman-hashicorp!
BUNDLED WITH
1.10.2
...@@ -144,7 +144,8 @@ each category, the available configuration keys are alphabetized. ...@@ -144,7 +144,8 @@ each category, the available configuration keys are alphabetized.
or "5m". The default SSH timeout is "5m", or five minutes. or "5m". The default SSH timeout is "5m", or five minutes.
* `subnet_id` (string) - If using VPC, the ID of the subnet, such as * `subnet_id` (string) - If using VPC, the ID of the subnet, such as
"subnet-12345def", where Packer will launch the EC2 instance. "subnet-12345def", where Packer will launch the EC2 instance. This field is
required if you are using an non-default VPC.
* `tags` (object of key/value strings) - Tags applied to the AMI. * `tags` (object of key/value strings) - Tags applied to the AMI.
......
...@@ -184,7 +184,8 @@ each category, the available configuration keys are alphabetized. ...@@ -184,7 +184,8 @@ each category, the available configuration keys are alphabetized.
or "5m". The default SSH timeout is "5m", or five minutes. or "5m". The default SSH timeout is "5m", or five minutes.
* `subnet_id` (string) - If using VPC, the ID of the subnet, such as * `subnet_id` (string) - If using VPC, the ID of the subnet, such as
"subnet-12345def", where Packer will launch the EC2 instance. "subnet-12345def", where Packer will launch the EC2 instance. This field is
required if you are using an non-default VPC.
* `tags` (object of key/value strings) - Tags applied to the AMI. * `tags` (object of key/value strings) - Tags applied to the AMI.
......
...@@ -24,61 +24,29 @@ There are many configuration options available for the builder. They are ...@@ -24,61 +24,29 @@ There are many configuration options available for the builder. They are
segmented below into two categories: required and optional parameters. Within segmented below into two categories: required and optional parameters. Within
each category, the available configuration keys are alphabetized. each category, the available configuration keys are alphabetized.
### Required v1 api: ### Required:
* `api_key` (string) - The API key to use to access your account. You can * `api_token` (string) - The client TOKEN to use to access your account.
retrieve this on the "API" page visible after logging into your account It can also be specified via environment variable `DIGITALOCEAN_API_TOKEN`, if set.
on DigitalOcean.
If not specified, Packer will use the environment variable
`DIGITALOCEAN_API_KEY`, if set.
* `client_id` (string) - The client ID to use to access your account. You can
find this on the "API" page visible after logging into your account on
DigitalOcean.
If not specified, Packer will use the environment variable
`DIGITALOCEAN_CLIENT_ID`, if set.
### Required v2 api:
* `api_token` (string) - The client TOKEN to use to access your account. If it
specified, then use v2 api (current), if not then used old (v1) deprecated api.
Also it can be specified via environment variable `DIGITALOCEAN_API_TOKEN`, if set.
### Optional:
* `api_url` (string) - API endpoint, by default use https://api.digitalocean.com
Also it can be specified via environment variable `DIGITALOCEAN_API_URL`, if set.
* `droplet_name` (string) - The name assigned to the droplet. DigitalOcean
sets the hostname of the machine to this value.
* `image` (string) - The name (or slug) of the base image to use. This is the * `image` (string) - The name (or slug) of the base image to use. This is the
image that will be used to launch a new droplet and provision it. This image that will be used to launch a new droplet and provision it.
defaults to 'ubuntu-12-04-x64' which is the slug for "Ubuntu 12.04.4 x64".
See https://developers.digitalocean.com/documentation/v2/#list-all-images for details on how to get a list of the the accepted image names/slugs. See https://developers.digitalocean.com/documentation/v2/#list-all-images for details on how to get a list of the the accepted image names/slugs.
* `image_id` (integer) - The ID of the base image to use. This is the image that
will be used to launch a new droplet and provision it.
This setting is deprecated. Use `image` instead.
* `private_networking` (boolean) - Set to `true` to enable private networking
for the droplet being created. This defaults to `false`, or not enabled.
* `region` (string) - The name (or slug) of the region to launch the droplet in. * `region` (string) - The name (or slug) of the region to launch the droplet in.
Consequently, this is the region where the snapshot will be available. Consequently, this is the region where the snapshot will be available.
This defaults to "nyc3", which is the slug for "New York 3".
See https://developers.digitalocean.com/documentation/v2/#list-all-regions for the accepted region names/slugs. See https://developers.digitalocean.com/documentation/v2/#list-all-regions for the accepted region names/slugs.
* `region_id` (integer) - The ID of the region to launch the droplet in. Consequently,
this is the region where the snapshot will be available.
This setting is deprecated. Use `region` instead.
* `size` (string) - The name (or slug) of the droplet size to use. * `size` (string) - The name (or slug) of the droplet size to use.
This defaults to "512mb", which is the slug for "512MB".
See https://developers.digitalocean.com/documentation/v2/#list-all-sizes for the accepted size names/slugs. See https://developers.digitalocean.com/documentation/v2/#list-all-sizes for the accepted size names/slugs.
* `size_id` (integer) - The ID of the droplet size to use. ### Optional:
This setting is deprecated. Use `size` instead.
* `droplet_name` (string) - The name assigned to the droplet. DigitalOcean
sets the hostname of the machine to this value.
* `private_networking` (boolean) - Set to `true` to enable private networking
for the droplet being created. This defaults to `false`, or not enabled.
* `snapshot_name` (string) - The name of the resulting snapshot that will * `snapshot_name` (string) - The name of the resulting snapshot that will
appear in your account. This must be unique. appear in your account. This must be unique.
...@@ -99,6 +67,8 @@ each category, the available configuration keys are alphabetized. ...@@ -99,6 +67,8 @@ each category, the available configuration keys are alphabetized.
for a droplet to enter a desired state (such as "active") before for a droplet to enter a desired state (such as "active") before
timing out. The default state timeout is "6m". timing out. The default state timeout is "6m".
* `user_data` (string) - User data to launch with the Droplet.
## Basic Example ## Basic Example
Here is a basic example. It is completely valid as soon as you enter your Here is a basic example. It is completely valid as soon as you enter your
...@@ -107,20 +77,9 @@ own access tokens: ...@@ -107,20 +77,9 @@ own access tokens:
```javascript ```javascript
{ {
"type": "digitalocean", "type": "digitalocean",
"client_id": "YOUR CLIENT ID", "api_token": "YOUR API KEY",
"api_key": "YOUR API KEY" "image": "ubuntu-12-04-x64",
"region": "nyc2",
"size": "512mb"
} }
``` ```
## Finding Image, Region, and Size IDs
Unfortunately, finding a list of available values for `image_id`, `region_id`,
and `size_id` is not easy at the moment. Basically, it has to be done through
the [DigitalOcean API](https://www.digitalocean.com/api_access) using the
`/images`, `/regions`, and `/sizes` endpoints. You can use `curl` for this
or request it in your browser.
If you're comfortable installing RubyGems, [Tugboat](https://github.com/pearkes/tugboat)
is a fantastic DigitalOcean command-line client that has commands to
find the available images, regions, and sizes. For example, to see all the
global images, you can run `tugboat images --global`.
...@@ -29,28 +29,32 @@ each category, the available configuration keys are alphabetized. ...@@ -29,28 +29,32 @@ each category, the available configuration keys are alphabetized.
### Required: ### Required:
* `flavor` (string) - The ID or full URL for the desired flavor for the * `flavor` (string) - The ID, name, or full URL for the desired flavor for the
server to be created. server to be created.
* `image_name` (string) - The name of the resulting image. * `image_name` (string) - The name of the resulting image.
* `password` (string) - The password used to connect to the OpenStack service.
If not specified, Packer will use the environment variables
`SDK_PASSWORD` or `OS_PASSWORD` (in that order), if set.
* `source_image` (string) - The ID or full URL to the base image to use. * `source_image` (string) - The ID or full URL to the base image to use.
This is the image that will be used to launch a new server and provision it. This is the image that will be used to launch a new server and provision it.
Unless you specify completely custom SSH settings, the source image must
have `cloud-init` installed so that the keypair gets assigned properly.
* `username` (string) - The username used to connect to the OpenStack service. * `username` (string) - The username used to connect to the OpenStack service.
If not specified, Packer will use the environment variable
`OS_USERNAME`, if set.
* `password` (string) - The password used to connect to the OpenStack service.
If not specified, Packer will use the environment variables If not specified, Packer will use the environment variables
`SDK_USERNAME` or `OS_USERNAME` (in that order), if set. `OS_PASSWORD`, if set.
### Optional: ### Optional:
* `api_key` (string) - The API key used to access OpenStack. Some OpenStack * `api_key` (string) - The API key used to access OpenStack. Some OpenStack
installations require this. installations require this.
If not specified, Packer will use the environment variables
`SDK_API_KEY`, if set. * `availability_zone` (string) - The availability zone to launch the
server in. If this isn't specified, the default enforced by your OpenStack
cluster will be used. This may be required for some OpenStack clusters.
* `floating_ip` (string) - A specific floating IP to assign to this instance. * `floating_ip` (string) - A specific floating IP to assign to this instance.
`use_floating_ip` must also be set to true for this to have an affect. `use_floating_ip` must also be set to true for this to have an affect.
...@@ -65,32 +69,18 @@ each category, the available configuration keys are alphabetized. ...@@ -65,32 +69,18 @@ each category, the available configuration keys are alphabetized.
* `networks` (array of strings) - A list of networks by UUID to attach * `networks` (array of strings) - A list of networks by UUID to attach
to this instance. to this instance.
* `openstack_provider` (string) - A name of a provider that has a slightly * `tenant_id` or `tenant_name` (string) - The tenant ID or name to boot the
different API model. Currently supported values are "openstack" (default), instance into. Some OpenStack installations require this.
and "rackspace". If not specified, Packer will use the environment variable
`OS_TENANT_NAME`, if set.
* `project` (string) - The project name to boot the instance into. Some
OpenStack installations require this.
If not specified, Packer will use the environment variables
`SDK_PROJECT` or `OS_TENANT_NAME` (in that order), if set.
* `provider` (string) - The provider used to connect to the OpenStack service.
If not specified, Packer will use the environment variables `SDK_PROVIDER`
or `OS_AUTH_URL` (in that order), if set.
For Rackspace this should be `rackspace-us` or `rackspace-uk`.
* `proxy_url` (string)
* `security_groups` (array of strings) - A list of security groups by name * `security_groups` (array of strings) - A list of security groups by name
to add to this instance. to add to this instance.
* `region` (string) - The name of the region, such as "DFW", in which * `region` (string) - The name of the region, such as "DFW", in which
to launch the server to create the AMI. to launch the server to create the AMI.
If not specified, Packer will use the environment variables If not specified, Packer will use the environment variable
`SDK_REGION` or `OS_REGION_NAME` (in that order), if set. `OS_REGION_NAME`, if set.
For a `provider` of "rackspace", it is required to specify a region,
either using this option or with an environment variable. For other
providers, including a private cloud, specifying a region is optional.
* `ssh_port` (integer) - The port that SSH will be available on. Defaults to port * `ssh_port` (integer) - The port that SSH will be available on. Defaults to port
22. 22.
...@@ -106,9 +96,6 @@ each category, the available configuration keys are alphabetized. ...@@ -106,9 +96,6 @@ each category, the available configuration keys are alphabetized.
useful for Rackspace are "public" or "private", and the default behavior is useful for Rackspace are "public" or "private", and the default behavior is
to connect via whichever is returned first from the OpenStack API. to connect via whichever is returned first from the OpenStack API.
* `tenant_id` (string) - Tenant ID for accessing OpenStack if your
installation requires this.
* `use_floating_ip` (boolean) - Whether or not to use a floating IP for * `use_floating_ip` (boolean) - Whether or not to use a floating IP for
the instance. Defaults to false. the instance. Defaults to false.
...@@ -124,10 +111,8 @@ Ubuntu 12.04 LTS (Precise Pangolin) on Rackspace OpenStack cloud offering. ...@@ -124,10 +111,8 @@ Ubuntu 12.04 LTS (Precise Pangolin) on Rackspace OpenStack cloud offering.
```javascript ```javascript
{ {
"type": "openstack", "type": "openstack",
"username": "", "username": "foo",
"api_key": "", "password": "foo",
"openstack_provider": "rackspace",
"provider": "rackspace-us",
"region": "DFW", "region": "DFW",
"ssh_username": "root", "ssh_username": "root",
"image_name": "Test image", "image_name": "Test image",
...@@ -160,13 +145,3 @@ script is setting environment variables like: ...@@ -160,13 +145,3 @@ script is setting environment variables like:
* `OS_TENANT_ID` * `OS_TENANT_ID`
* `OS_USERNAME` * `OS_USERNAME`
* `OS_PASSWORD` * `OS_PASSWORD`
## Troubleshooting
*I get the error "Missing or incorrect provider"*
* Verify your "username", "password" and "provider" settings.
*I get the error "Missing endpoint, or insufficient privileges to access endpoint"*
* Verify your "region" setting.
...@@ -179,9 +179,11 @@ each category, the available options are alphabetized and described. ...@@ -179,9 +179,11 @@ each category, the available options are alphabetized and described.
By default this is "output-BUILDNAME" where "BUILDNAME" is the name By default this is "output-BUILDNAME" where "BUILDNAME" is the name
of the build. of the build.
* `shutdown_command` (string) - The command to use to gracefully shut down * `shutdown_command` (string) - The command to use to gracefully shut down the machine once all
the machine once all the provisioning is done. By default this is an empty the provisioning is done. By default this is an empty string, which tells Packer to just
string, which tells Packer to just forcefully shut down the machine. forcefully shut down the machine unless a shutdown command takes place inside script so this may
safely be omitted. If one or more scripts require a reboot it is suggested to leave this blank
since reboots may fail and specify the final shutdown command in your last script.
* `shutdown_timeout` (string) - The amount of time to wait after executing * `shutdown_timeout` (string) - The amount of time to wait after executing
the `shutdown_command` for the virtual machine to actually shut down. the `shutdown_command` for the virtual machine to actually shut down.
...@@ -209,6 +211,10 @@ each category, the available options are alphabetized and described. ...@@ -209,6 +211,10 @@ each category, the available options are alphabetized and described.
available. By default this is "20m", or 20 minutes. Note that this should available. By default this is "20m", or 20 minutes. Note that this should
be quite long since the timer begins as soon as the virtual machine is booted. be quite long since the timer begins as soon as the virtual machine is booted.
* `ssh_skip_nat_mapping` (bool) - Defaults to false. When enabled, Packer does
not setup forwarded port mapping for SSH requests and uses `ssh_port` on the
host to communicate to the virtual machine
* `vboxmanage` (array of array of strings) - Custom `VBoxManage` commands to * `vboxmanage` (array of array of strings) - Custom `VBoxManage` commands to
execute in order to further customize the virtual machine being created. execute in order to further customize the virtual machine being created.
The value of this is an array of commands to execute. The commands are executed The value of this is an array of commands to execute. The commands are executed
......
...@@ -154,9 +154,11 @@ each category, the available options are alphabetized and described. ...@@ -154,9 +154,11 @@ each category, the available options are alphabetized and described.
By default this is "output-BUILDNAME" where "BUILDNAME" is the name By default this is "output-BUILDNAME" where "BUILDNAME" is the name
of the build. of the build.
* `shutdown_command` (string) - The command to use to gracefully shut down * `shutdown_command` (string) - The command to use to gracefully shut down the machine once all
the machine once all the provisioning is done. By default this is an empty the provisioning is done. By default this is an empty string, which tells Packer to just
string, which tells Packer to just forcefully shut down the machine. forcefully shut down the machine unless a shutdown command takes place inside script so this may
safely be omitted. If one or more scripts require a reboot it is suggested to leave this blank
since reboots may fail and specify the final shutdown command in your last script.
* `shutdown_timeout` (string) - The amount of time to wait after executing * `shutdown_timeout` (string) - The amount of time to wait after executing
the `shutdown_command` for the virtual machine to actually shut down. the `shutdown_command` for the virtual machine to actually shut down.
...@@ -184,6 +186,10 @@ each category, the available options are alphabetized and described. ...@@ -184,6 +186,10 @@ each category, the available options are alphabetized and described.
available. By default this is "20m", or 20 minutes. Note that this should available. By default this is "20m", or 20 minutes. Note that this should
be quite long since the timer begins as soon as the virtual machine is booted. be quite long since the timer begins as soon as the virtual machine is booted.
* `ssh_skip_nat_mapping` (bool) - Defaults to false. When enabled, Packer does
not setup forwarded port mapping for SSH requests and uses `ssh_port` on the
host to communicate to the virtual machine
* `vboxmanage` (array of array of strings) - Custom `VBoxManage` commands to * `vboxmanage` (array of array of strings) - Custom `VBoxManage` commands to
execute in order to further customize the virtual machine being created. execute in order to further customize the virtual machine being created.
The value of this is an array of commands to execute. The commands are executed The value of this is an array of commands to execute. The commands are executed
......
...@@ -72,6 +72,12 @@ each category, the available options are alphabetized and described. ...@@ -72,6 +72,12 @@ each category, the available options are alphabetized and described.
### Optional: ### Optional:
* `disk_additional_size` (array of integers) - The size(s) of any additional
hard disks for the VM in megabytes. If this is not specified then the VM will
only contain a primary hard disk. The builder uses expandable, not fixed-size
virtual hard disks, so the actual file representing the disk will not use the
full size unless it is full.
* `boot_command` (array of strings) - This is an array of commands to type * `boot_command` (array of strings) - This is an array of commands to type
when the virtual machine is first booted. The goal of these commands should when the virtual machine is first booted. The goal of these commands should
be to type just enough to initialize the operating system installer. Special be to type just enough to initialize the operating system installer. Special
......
...@@ -109,9 +109,11 @@ each category, the available options are alphabetized and described. ...@@ -109,9 +109,11 @@ each category, the available options are alphabetized and described.
By default this is "output-BUILDNAME" where "BUILDNAME" is the name By default this is "output-BUILDNAME" where "BUILDNAME" is the name
of the build. of the build.
* `shutdown_command` (string) - The command to use to gracefully shut down * `shutdown_command` (string) - The command to use to gracefully shut down the machine once all
the machine once all the provisioning is done. By default this is an empty the provisioning is done. By default this is an empty string, which tells Packer to just
string, which tells Packer to just forcefully shut down the machine. forcefully shut down the machine unless a shutdown command takes place inside script so this may
safely be omitted. If one or more scripts require a reboot it is suggested to leave this blank
since reboots may fail and specify the final shutdown command in your last script.
* `shutdown_timeout` (string) - The amount of time to wait after executing * `shutdown_timeout` (string) - The amount of time to wait after executing
the `shutdown_command` for the virtual machine to actually shut down. the `shutdown_command` for the virtual machine to actually shut down.
......
...@@ -34,6 +34,9 @@ The example below is fully functional and expects cookbooks in the ...@@ -34,6 +34,9 @@ The example below is fully functional and expects cookbooks in the
The reference of available configuration options is listed below. No The reference of available configuration options is listed below. No
configuration is actually required, but at least `run_list` is recommended. configuration is actually required, but at least `run_list` is recommended.
* `chef_environment` (string) - The name of the `chef_environment` sent to the
Chef server. By default this is empty and will not use an environment
* `config_template` (string) - Path to a template that will be used for * `config_template` (string) - Path to a template that will be used for
the Chef configuration file. By default Packer only sets configuration the Chef configuration file. By default Packer only sets configuration
it needs to match the settings set in the provisioner configuration. If it needs to match the settings set in the provisioner configuration. If
......
...@@ -66,8 +66,10 @@ Optional parameters: ...@@ -66,8 +66,10 @@ Optional parameters:
* `inline_shebang` (string) - The * `inline_shebang` (string) - The
[shebang](http://en.wikipedia.org/wiki/Shebang_%28Unix%29) value to use when [shebang](http://en.wikipedia.org/wiki/Shebang_%28Unix%29) value to use when
running commands specified by `inline`. By default, this is `/bin/sh`. running commands specified by `inline`. By default, this is `/bin/sh -e`.
If you're not using `inline`, then this configuration has no effect. If you're not using `inline`, then this configuration has no effect.
**Important:** If you customize this, be sure to include something like
the `-e` flag, otherwise individual steps failing won't fail the provisioner.
* `remote_path` (string) - The path where the script will be uploaded to * `remote_path` (string) - The path where the script will be uploaded to
in the machine. This defaults to "/tmp/script.sh". This value must be in the machine. This defaults to "/tmp/script.sh". This value must be
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment