Merge pull request #1 from mitchellh/master

update to most recent
This commit is contained in:
marc-ta 2015-06-15 12:23:29 -07:00
commit 899cd7b845
190 changed files with 4108 additions and 2815 deletions

View File

@ -1,48 +1,131 @@
## 0.8.0 (unreleased)
BACKWARDS INCOMPATIBILITIES:
* core: SSH connection will no longer request a PTY by default. This
can be enabled per builder.
* 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:
* **WinRM:** You can now connect via WinRM with almost every builder.
See the docs for more info. [GH-2239]
* **Windows AWS Support:** Windows AMIs can now be built without any
external plugins: Packer will start a Windows instance, get the
admin password, and can use WinRM (above) to connect through. [GH-2240]
* **Disable SSH:** Set `communicator` to "none" in any builder to disable SSH
connections. Note that provisioners won't work if this is done. [GH-1591]
* **SSH Agent Forwarding:** SSH Agent Forwarding will now be enabled
to allow access to remote servers such as private git repos. [GH-1066]
* **Docker builder supports SSH**: The Docker builder now supports containers
with SSH, just set `communicator` to "ssh" [GH-2244]
* **New config function: `build_name`**: The name of the currently running
build. [GH-2232]
* **New config function: `build_type`**: The type of the currently running
builder. This is useful for provisioners. [GH-2232]
* **New config function: `template_dir`**: The directory to the template
being built. This should be used for template-relative paths. [GH-54]
IMPROVEMENTS:
* core: Interrupt handling for SIGTERM signal as well. [GH-1858]
* builder/*: Add `ssh_handshake_attempts` to configure the number of
handshake attempts done before failure [GH-2237]
* builder/amazon: Add `force_deregister` option for automatic AMI
deregistration [GH-2221]
* builder/amazon: Now applies tags to EBS snapshots [GH-2212]
* 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
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
* builder/openstack: Flavor names can be used as well as refs
* builder/openstack: Add `availability_zone` [GH-2016]
* builder/openstack: Machine will be stopped prior to imaging if the
cluster supports the `startstop` extension. [GH-2223]
* builder/openstack: Support for user data [GH-2224]
* 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/vmware: Support for additional disks [GH-1382]
* command/fix: After fixing, the template is validated [GH-2228]
* command/push: Add `-name` flag for specifying name from CLI [GH-2042]
* command/push: Push configuration in templates supports variables [GH-1861]
* post-processor/docker-save: Can be chained [GH-2179]
* post-processor/docker-tag: Support `force` option [GH-2055]
* post-processor/docker-tag: Can be chained [GH-2179]
BUG FIXES:
* core: Fix potential panic for post-processor plugin exits [GH-2098]
* core: `PACKER_CONFIG` may point to a non-existent file [GH-2226]
* 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: 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
consistency. [GH-2129]
* builder/amazon: If no AZ is specified, use AZ chosen automatically by
AWS for spot instance. [GH-2017]
* builder/amazon: Private key file (only available in debug mode)
is deleted on cleanup. [GH-1801]
* 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: 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/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: Private images can be used as a source [GH-1792]
* builder/docker: Fixed hang on prompt while copying script
* builder/docker: Use `docker exec` for newer versions of Docker for
running scripts [GH-1993]
* builder/docker: Fix crash that could occur at certain timed ctrl-c [GH-1838]
* builder/docker: validate that `export_path` is not a directory [GH-2105]
* builder/google: `ssh_timeout` is respected [GH-1781]
* builder/openstack: `ssh_interface` can be used to specify the interface
to retrieve the SSH IP from. [GH-2220]
* builder/qemu: Add `disk_discard` option [GH-2120]
* builder/virtualbox: Added SCSI support
* builder/qemu: Use proper SSH port, not hardcoded to 22. [GH-2236]
* builder/virtualbox: Bind HTTP server to IPv4, which is more compatible with
OS installers. [GH-1709]
* builder/virtualbox: Remove the floppy controller in addition to the
floppy disk. [GH-1879]
* builder/virtualbox: Fixed regression where downloading ISO without a
".iso" extension didn't work. [GH-1839]
* builder/virtualbox: Output dir is verified at runtime, not template
validation time. [GH-2233]
* builder/vmware: Add 100ms delay between keystrokes to avoid subtle
timing issues in most cases. [GH-1663]
* builder/vmware: Bind HTTP server to IPv4, which is more compatible with
OS installers. [GH-1709]
* 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: Nested output directories for ESXi work [GH-2174]
* builder/vmware: Output dir is verified at runtime, not template
validation time. [GH-2233]
* command/fix: For the `virtualbox` to `virtualbox-iso` builder rename,
provisioner overrides are now also fixed [GH-2231]
* command/validate: don't crash for invalid builds [GH-2139]
* post-processor/atlas: Find common archive prefix for Windows [GH-1874]
* post-processor/atlas: Fix index out of range panic [GH-1959]
* post-processor/vagrant-cloud: Fixed failing on response
* post-processor/vagrant-cloud: Don't delete version on error [GH-2014]
* provisioner/puppet-masterless: Allow manifest_file to be a directory
* 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]
* provisioner/shell: single quotes in env vars are escaped [GH-2229]
## 0.7.5 (December 9, 2014)

View File

@ -31,7 +31,7 @@ testrace:
go test -race $(TEST) $(TESTARGS)
updatedeps:
go get -d -v -p 2 ./...
go get -u -d -v -p 2 ./...
vet:
@go tool vet 2>/dev/null ; if [ $$? -eq 3 ]; then \

View File

@ -147,6 +147,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
// Build the steps
steps := []multistep.Step{
&awscommon.StepPreValidate{
DestAmiName: b.config.AMIName,
ForceDeregister: b.config.AMIForceDeregister,
},
&StepInstanceInfo{},
&awscommon.StepSourceAMIInfo{
SourceAmi: b.config.SourceAmi,
@ -164,9 +168,15 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&StepChrootProvision{},
&StepEarlyCleanup{},
&StepSnapshot{},
&awscommon.StepDeregisterAMI{
ForceDeregister: b.config.AMIForceDeregister,
AMIName: b.config.AMIName,
},
&StepRegisterAMI{},
&awscommon.StepAMIRegionCopy{
AccessConfig: &b.config.AccessConfig,
Regions: b.config.AMIRegions,
Name: b.config.AMIName,
},
&awscommon.StepModifyAMIAttributes{
Description: b.config.AMIDescription,

View File

@ -17,6 +17,7 @@ type AMIConfig struct {
AMIRegions []string `mapstructure:"ami_regions"`
AMITags map[string]string `mapstructure:"tags"`
AMIEnhancedNetworking bool `mapstructure:"enhanced_networking"`
AMIForceDeregister bool `mapstructure:"force_deregister"`
}
func (c *AMIConfig) Prepare(ctx *interpolate.Context) []error {

View File

@ -29,13 +29,23 @@ func buildBlockDevices(b []BlockDevice) []*ec2.BlockDeviceMapping {
for _, blockDevice := range b {
ebsBlockDevice := &ec2.EBSBlockDevice{
SnapshotID: &blockDevice.SnapshotId,
Encrypted: &blockDevice.Encrypted,
IOPS: &blockDevice.IOPS,
VolumeType: &blockDevice.VolumeType,
VolumeSize: &blockDevice.VolumeSize,
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{
EBS: ebsBlockDevice,
DeviceName: &blockDevice.DeviceName,

View File

@ -5,6 +5,7 @@ import (
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awsutil"
"github.com/aws/aws-sdk-go/service/ec2"
)
@ -28,11 +29,48 @@ func TestBlockDevice(t *testing.T) {
DeviceName: aws.String("/dev/sdb"),
VirtualName: aws.String("ephemeral0"),
EBS: &ec2.EBSBlockDevice{
Encrypted: aws.Boolean(false),
SnapshotID: aws.String("snap-1234"),
VolumeType: aws.String("standard"),
VolumeSize: aws.Long(8),
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),
},
},
@ -48,11 +86,11 @@ func TestBlockDevice(t *testing.T) {
expected := []*ec2.BlockDeviceMapping{tc.Result}
got := blockDevices.BuildAMIDevices()
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()) {
t.Fatalf("bad: %#v", expected)
t.Fatalf("Bad block device, \nexpected: %s\n\ngot: %s", awsutil.StringValue(expected), awsutil.StringValue(blockDevices.BuildLaunchDevices()))
}
}
}

View File

@ -7,6 +7,7 @@ import (
"time"
"github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/template/interpolate"
)
@ -21,40 +22,32 @@ type RunConfig struct {
SourceAmi string `mapstructure:"source_ami"`
SpotPrice string `mapstructure:"spot_price"`
SpotPriceAutoProduct string `mapstructure:"spot_price_auto_product"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPrivateKeyFile string `mapstructure:"ssh_private_key_file"`
SSHPrivateIp bool `mapstructure:"ssh_private_ip"`
SSHPort int `mapstructure:"ssh_port"`
SecurityGroupId string `mapstructure:"security_group_id"`
SecurityGroupIds []string `mapstructure:"security_group_ids"`
SubnetId string `mapstructure:"subnet_id"`
TemporaryKeyPairName string `mapstructure:"temporary_key_pair_name"`
UserData string `mapstructure:"user_data"`
UserDataFile string `mapstructure:"user_data_file"`
WindowsPasswordTimeout time.Duration `mapstructure:"windows_password_timeout"`
VpcId string `mapstructure:"vpc_id"`
// Unexported fields that are calculated from others
sshTimeout time.Duration
// Communicator settings
Comm communicator.Config `mapstructure:",squash"`
SSHPrivateIp bool `mapstructure:"ssh_private_ip"`
}
func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
// Defaults
if c.SSHPort == 0 {
c.SSHPort = 22
}
if c.RawSSHTimeout == "" {
c.RawSSHTimeout = "5m"
}
if c.TemporaryKeyPairName == "" {
c.TemporaryKeyPairName = fmt.Sprintf(
"packer %s", uuid.TimeOrderedUUID())
}
if c.WindowsPasswordTimeout == 0 {
c.WindowsPasswordTimeout = 10 * time.Minute
}
// Validation
var errs []error
errs := c.Comm.Prepare(ctx)
if c.SourceAmi == "" {
errs = append(errs, errors.New("A source_ami must be specified"))
}
@ -70,10 +63,6 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
}
}
if c.SSHUsername == "" {
errs = append(errs, errors.New("An ssh_username must be specified"))
}
if c.UserData != "" && c.UserDataFile != "" {
errs = append(errs, fmt.Errorf("Only one of user_data or user_data_file can be specified."))
} else if c.UserDataFile != "" {
@ -91,15 +80,5 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
}
}
var err error
c.sshTimeout, err = time.ParseDuration(c.RawSSHTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
}
return errs
}
func (c *RunConfig) SSHTimeout() time.Duration {
return c.sshTimeout
}

View File

@ -4,6 +4,8 @@ import (
"io/ioutil"
"os"
"testing"
"github.com/mitchellh/packer/helper/communicator"
)
func init() {
@ -19,7 +21,10 @@ func testConfig() *RunConfig {
return &RunConfig{
SourceAmi: "abcd",
InstanceType: "m1.small",
SSHUsername: "root",
Comm: communicator.Config{
SSHUsername: "foo",
},
}
}
@ -62,41 +67,28 @@ func TestRunConfigPrepare_SpotAuto(t *testing.T) {
func TestRunConfigPrepare_SSHPort(t *testing.T) {
c := testConfig()
c.SSHPort = 0
c.Comm.SSHPort = 0
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
if c.SSHPort != 22 {
t.Fatalf("invalid value: %d", c.SSHPort)
if c.Comm.SSHPort != 22 {
t.Fatalf("invalid value: %d", c.Comm.SSHPort)
}
c.SSHPort = 44
c.Comm.SSHPort = 44
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
if c.SSHPort != 44 {
t.Fatalf("invalid value: %d", c.SSHPort)
}
}
func TestRunConfigPrepare_SSHTimeout(t *testing.T) {
c := testConfig()
c.RawSSHTimeout = ""
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
c.RawSSHTimeout = "bad"
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
if c.Comm.SSHPort != 44 {
t.Fatalf("invalid value: %d", c.Comm.SSHPort)
}
}
func TestRunConfigPrepare_SSHUsername(t *testing.T) {
c := testConfig()
c.SSHUsername = ""
c.Comm.SSHUsername = ""
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
}

View File

@ -10,9 +10,9 @@ import (
"golang.org/x/crypto/ssh"
)
// SSHAddress returns a function that can be given to the SSH communicator
// SSHHost returns a function that can be given to the SSH communicator
// for determining the SSH address based on the instance DNS name.
func SSHAddress(e *ec2.EC2, port int, private bool) func(multistep.StateBag) (string, error) {
func SSHHost(e *ec2.EC2, private bool) func(multistep.StateBag) (string, error) {
return func(state multistep.StateBag) (string, error) {
for j := 0; j < 2; j++ {
var host string
@ -28,7 +28,7 @@ func SSHAddress(e *ec2.EC2, port int, private bool) func(multistep.StateBag) (st
}
if host != "" {
return fmt.Sprintf("%s:%d", host, port), nil
return host, nil
}
r, err := e.DescribeInstances(&ec2.DescribeInstancesInput{

View File

@ -67,10 +67,10 @@ func AMIStateRefreshFunc(conn *ec2.EC2, imageId string) StateRefreshFunc {
// InstanceStateRefreshFunc returns a StateRefreshFunc that is used to watch
// 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) {
resp, err := conn.DescribeInstances(&ec2.DescribeInstancesInput{
InstanceIDs: []*string{i.InstanceID},
InstanceIDs: []*string{&instanceId},
})
if err != nil {
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidInstanceID.NotFound" {
@ -91,7 +91,7 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) StateRefreshFunc {
return nil, "", nil
}
i = resp.Reservations[0].Instances[0]
i := resp.Reservations[0].Instances[0]
return i, *i.State.Name, nil
}
}

View File

@ -14,6 +14,7 @@ import (
type StepAMIRegionCopy struct {
AccessConfig *AccessConfig
Regions []string
Name string
}
func (s *StepAMIRegionCopy) Run(state multistep.StateBag) multistep.StepAction {
@ -32,12 +33,18 @@ func (s *StepAMIRegionCopy) Run(state multistep.StateBag) multistep.StepAction {
var wg sync.WaitGroup
errs := new(packer.MultiError)
for _, region := range s.Regions {
if region == ec2conn.Config.Region {
ui.Message(fmt.Sprintf(
"Avoiding copying AMI to duplicate region %s", region))
continue
}
wg.Add(1)
ui.Message(fmt.Sprintf("Copying to: %s", region))
go func(region string) {
defer wg.Done()
id, err := amiRegionCopy(state, s.AccessConfig, ami, region, ec2conn.Config.Region)
id, err := amiRegionCopy(state, s.AccessConfig, s.Name, ami, region, ec2conn.Config.Region)
lock.Lock()
defer lock.Unlock()
@ -69,7 +76,7 @@ func (s *StepAMIRegionCopy) Cleanup(state multistep.StateBag) {
// amiRegionCopy does a copy for the given AMI to the target region and
// returns the resulting ID or error.
func amiRegionCopy(state multistep.StateBag, config *AccessConfig, imageId string,
func amiRegionCopy(state multistep.StateBag, config *AccessConfig, name string, imageId string,
target string, source string) (string, error) {
// Connect to the region where the AMI will be copied to
@ -83,6 +90,7 @@ func amiRegionCopy(state multistep.StateBag, config *AccessConfig, imageId strin
resp, err := regionconn.CopyImage(&ec2.CopyImageInput{
SourceRegion: &source,
SourceImageID: &imageId,
Name: &name,
})
if err != nil {

View File

@ -25,19 +25,56 @@ func (s *StepCreateTags) Run(state multistep.StateBag) multistep.StepAction {
var ec2Tags []*ec2.Tag
for key, value := range s.Tags {
ui.Message(fmt.Sprintf("Adding tag: \"%s\": \"%s\"", key, value))
ec2Tags = append(ec2Tags, &ec2.Tag{Key: &key, Value: &value})
ec2Tags = append(ec2Tags, &ec2.Tag{
Key: aws.String(key),
Value: aws.String(value),
})
}
// Declare list of resources to tag
resourceIds := []*string{&ami}
// Retrieve image list for given AMI
imageResp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{
ImageIDs: resourceIds,
})
if err != nil {
err := fmt.Errorf("Error retrieving details for AMI (%s): %s", ami, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if len(imageResp.Images) == 0 {
err := fmt.Errorf("Error retrieving details for AMI (%s), no images found", ami)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
image := imageResp.Images[0]
// Add only those with a Snapshot ID, i.e. not Ephemeral
for _, device := range image.BlockDeviceMappings {
if device.EBS != nil && device.EBS.SnapshotID != nil {
ui.Say(fmt.Sprintf("Tagging snapshot: %s", *device.EBS.SnapshotID))
resourceIds = append(resourceIds, device.EBS.SnapshotID)
}
}
regionconn := ec2.New(&aws.Config{
Credentials: ec2conn.Config.Credentials,
Region: region,
})
_, err := regionconn.CreateTags(&ec2.CreateTagsInput{
Resources: []*string{&ami},
_, err = regionconn.CreateTags(&ec2.CreateTagsInput{
Resources: resourceIds,
Tags: ec2Tags,
})
if err != nil {
err := fmt.Errorf("Error adding tags to AMI (%s): %s", ami, err)
err := fmt.Errorf("Error adding tags to Resources (%#v): %s", resourceIds, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt

View File

@ -0,0 +1,56 @@
package common
import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
type StepDeregisterAMI struct {
ForceDeregister bool
AMIName string
}
func (s *StepDeregisterAMI) Run(state multistep.StateBag) multistep.StepAction {
ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui)
// check for force deregister
if s.ForceDeregister {
resp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{
Filters: []*ec2.Filter{&ec2.Filter{
Name: aws.String("name"),
Values: []*string{aws.String(s.AMIName)},
}}})
if err != nil {
err := fmt.Errorf("Error creating AMI: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// deregister image(s) by that name
for _, i := range resp.Images {
_, err := ec2conn.DeregisterImage(&ec2.DeregisterImageInput{
ImageID: i.ImageID,
})
if err != nil {
err := fmt.Errorf("Error deregistering existing AMI: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say(fmt.Sprintf("Deregistered AMI %s, id: %s", s.AMIName, *i.ImageID))
}
}
return multistep.ActionContinue
}
func (s *StepDeregisterAMI) Cleanup(state multistep.StateBag) {
}

View File

@ -0,0 +1,160 @@
package common
import (
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"log"
"time"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer"
)
// StepGetPassword reads the password from a Windows server and sets it
// on the WinRM config.
type StepGetPassword struct {
Comm *communicator.Config
Timeout time.Duration
}
func (s *StepGetPassword) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
image := state.Get("source_image").(*ec2.Image)
// Skip if we're not Windows...
if image.Platform == nil || *image.Platform != "windows" {
log.Printf("[INFO] Not Windows, skipping get password...")
return multistep.ActionContinue
}
// If we already have a password, skip it
if s.Comm.WinRMPassword != "" {
ui.Say("Skipping waiting for password since WinRM password set...")
return multistep.ActionContinue
}
// Get the password
var password string
var err error
cancel := make(chan struct{})
waitDone := make(chan bool, 1)
go func() {
ui.Say("Waiting for auto-generated password for instance...")
ui.Message(
"It is normal for this process to take up to 15 minutes,\n" +
"but it usually takes around 5. Please wait.")
password, err = s.waitForPassword(state, cancel)
waitDone <- true
}()
timeout := time.After(s.Timeout)
WaitLoop:
for {
// Wait for either SSH to become available, a timeout to occur,
// or an interrupt to come through.
select {
case <-waitDone:
if err != nil {
ui.Error(fmt.Sprintf("Error waiting for password: %s", err))
state.Put("error", err)
return multistep.ActionHalt
}
ui.Message(fmt.Sprintf(" \nPassword retrieved!"))
s.Comm.WinRMPassword = password
break WaitLoop
case <-timeout:
err := fmt.Errorf("Timeout waiting for password.")
state.Put("error", err)
ui.Error(err.Error())
close(cancel)
return multistep.ActionHalt
case <-time.After(1 * time.Second):
if _, ok := state.GetOk(multistep.StateCancelled); ok {
// The step sequence was cancelled, so cancel waiting for password
// and just start the halting process.
close(cancel)
log.Println("[WARN] Interrupt detected, quitting waiting for password.")
return multistep.ActionHalt
}
}
}
return multistep.ActionContinue
}
func (s *StepGetPassword) Cleanup(multistep.StateBag) {}
func (s *StepGetPassword) waitForPassword(state multistep.StateBag, cancel <-chan struct{}) (string, error) {
ec2conn := state.Get("ec2").(*ec2.EC2)
instance := state.Get("instance").(*ec2.Instance)
privateKey := state.Get("privateKey").(string)
for {
select {
case <-cancel:
log.Println("[INFO] Retrieve password wait cancelled. Exiting loop.")
return "", errors.New("Retrieve password wait cancelled")
case <-time.After(5 * time.Second):
}
resp, err := ec2conn.GetPasswordData(&ec2.GetPasswordDataInput{
InstanceID: instance.InstanceID,
})
if err != nil {
err := fmt.Errorf("Error retrieving auto-generated instance password: %s", err)
return "", err
}
if resp.PasswordData != nil && *resp.PasswordData != "" {
decryptedPassword, err := decryptPasswordDataWithPrivateKey(
*resp.PasswordData, []byte(privateKey))
if err != nil {
err := fmt.Errorf("Error decrypting auto-generated instance password: %s", err)
return "", err
}
return decryptedPassword, nil
}
log.Printf("[DEBUG] Password is blank, will retry...")
}
}
func decryptPasswordDataWithPrivateKey(passwordData string, pemBytes []byte) (string, error) {
encryptedPasswd, err := base64.StdEncoding.DecodeString(passwordData)
if err != nil {
return "", err
}
block, _ := pem.Decode(pemBytes)
var asn1Bytes []byte
if _, ok := block.Headers["DEK-Info"]; ok {
return "", errors.New("encrypted private key isn't yet supported")
/*
asn1Bytes, err = x509.DecryptPEMBlock(block, password)
if err != nil {
return "", err
}
*/
} else {
asn1Bytes = block.Bytes
}
key, err := x509.ParsePKCS1PrivateKey(asn1Bytes)
if err != nil {
return "", err
}
out, err := rsa.DecryptPKCS1v15(nil, key, encryptedPasswd)
if err != nil {
return "", err
}
return string(out), nil
}

View File

@ -0,0 +1,53 @@
package common
import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
// StepPreValidate provides an opportunity to pre-validate any configuration for
// the build before actually doing any time consuming work
//
type StepPreValidate struct {
DestAmiName string
ForceDeregister bool
}
func (s *StepPreValidate) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
if s.ForceDeregister {
ui.Say("Force Deregister flag found, skipping prevalidating AMI Name")
return multistep.ActionContinue
}
ec2conn := state.Get("ec2").(*ec2.EC2)
ui.Say("Prevalidating AMI Name...")
resp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{
Filters: []*ec2.Filter{&ec2.Filter{
Name: aws.String("name"),
Values: []*string{aws.String(s.DestAmiName)},
}}})
if err != nil {
err := fmt.Errorf("Error querying AMI: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if len(resp.Images) > 0 {
err := fmt.Errorf("Error: name conflicts with an existing AMI: %s", *resp.Images[0].ImageID)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *StepPreValidate) Cleanup(multistep.StateBag) {}

View File

@ -1,6 +1,7 @@
package common
import (
"encoding/base64"
"fmt"
"io/ioutil"
"log"
@ -53,7 +54,14 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi
return multistep.ActionHalt
}
// Test if it is encoded already, and if not, encode it
if _, err := base64.StdEncoding.DecodeString(string(contents)); err != nil {
log.Printf("[DEBUG] base64 encoding user data...")
contents = []byte(base64.StdEncoding.EncodeToString(contents))
}
userData = string(contents)
}
ui.Say("Launching a source AWS instance...")
@ -174,11 +182,15 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi
ImageID: &s.SourceAMI,
InstanceType: &s.InstanceType,
UserData: &userData,
SecurityGroupIDs: securityGroupIds,
IAMInstanceProfile: &ec2.IAMInstanceProfileSpecification{Name: &s.IamInstanceProfile},
SubnetID: &s.SubnetId,
NetworkInterfaces: []*ec2.InstanceNetworkInterfaceSpecification{
&ec2.InstanceNetworkInterfaceSpecification{AssociatePublicIPAddress: &s.AssociatePublicIpAddress},
&ec2.InstanceNetworkInterfaceSpecification{
DeviceIndex: aws.Long(0),
AssociatePublicIPAddress: &s.AssociatePublicIpAddress,
SubnetID: &s.SubnetId,
Groups: securityGroupIds,
DeleteOnTermination: aws.Boolean(true),
},
},
Placement: &ec2.SpotPlacement{
AvailabilityZone: &availabilityZone,
@ -223,36 +235,17 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi
instanceId = *spotResp.SpotInstanceRequests[0].InstanceID
}
instanceResp, err := ec2conn.DescribeInstances(&ec2.DescribeInstancesInput{
InstanceIDs: []*string{&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))
ui.Message(fmt.Sprintf("Instance ID: %s", instanceId))
ui.Say(fmt.Sprintf("Waiting for instance (%v) to become ready...", instanceId))
stateChange := StateChangeConf{
Pending: []string{"pending"},
Target: "running",
Refresh: InstanceStateRefreshFunc(ec2conn, s.instance),
Refresh: InstanceStateRefreshFunc(ec2conn, instanceId),
StepState: state,
}
latestInstance, err := WaitForState(&stateChange)
if err != nil {
err := fmt.Errorf("Error waiting for instance (%s) to become ready: %s", *s.instance.InstanceID, err)
err := fmt.Errorf("Error waiting for instance (%s) to become ready: %s", instanceId, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
@ -329,7 +322,7 @@ func (s *StepRunSourceInstance) Cleanup(state multistep.StateBag) {
}
stateChange := StateChangeConf{
Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"},
Refresh: InstanceStateRefreshFunc(ec2conn, s.instance),
Refresh: InstanceStateRefreshFunc(ec2conn, *s.instance.InstanceID),
Target: "terminated",
}

View File

@ -9,12 +9,13 @@ import (
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer"
)
type StepSecurityGroup struct {
CommConfig *communicator.Config
SecurityGroupIds []string
SSHPort int
VpcId string
createdGroupId string
@ -30,8 +31,9 @@ func (s *StepSecurityGroup) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionContinue
}
if s.SSHPort == 0 {
panic("SSHPort must be set to a non-zero value.")
port := s.CommConfig.Port()
if port == 0 {
panic("port must be set to a non-zero value.")
}
// Create the group
@ -57,15 +59,17 @@ func (s *StepSecurityGroup) Run(state multistep.StateBag) multistep.StepAction {
req := &ec2.AuthorizeSecurityGroupIngressInput{
GroupID: groupResp.GroupID,
IPProtocol: aws.String("tcp"),
FromPort: aws.Long(int64(s.SSHPort)),
ToPort: aws.Long(int64(s.SSHPort)),
FromPort: aws.Long(int64(port)),
ToPort: aws.Long(int64(port)),
CIDRIP: aws.String("0.0.0.0/0"),
}
// We loop and retry this a few times because sometimes the security
// group isn't available immediately because AWS resources are eventaully
// consistent.
ui.Say("Authorizing SSH access on the temporary security group...")
ui.Say(fmt.Sprintf(
"Authorizing access to port %d the temporary security group...",
port))
for i := 0; i < 5; i++ {
_, err = ec2conn.AuthorizeSecurityGroupIngress(req)
if err == nil {

View File

@ -20,7 +20,7 @@ func isalphanumeric(b byte) bool {
// Clean up AMI name by replacing invalid characters with "-"
func templateCleanAMIName(s string) string {
allowed := []byte{'(', ')', ',', '/', '-', '_'}
allowed := []byte{'(', ')', ',', '/', '-', '_', ' '}
b := []byte(s)
newb := make([]byte, len(b))
for i, c := range b {

View File

@ -13,6 +13,7 @@ import (
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"
@ -78,6 +79,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
// Build the steps
steps := []multistep.Step{
&awscommon.StepPreValidate{
DestAmiName: b.config.AMIName,
ForceDeregister: b.config.AMIForceDeregister,
},
&awscommon.StepSourceAMIInfo{
SourceAmi: b.config.SourceAmi,
EnhancedNetworking: b.config.AMIEnhancedNetworking,
@ -86,11 +91,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Debug: b.config.PackerDebug,
DebugKeyPath: fmt.Sprintf("ec2_%s.pem", b.config.PackerBuildName),
KeyPairName: b.config.TemporaryKeyPairName,
PrivateKeyFile: b.config.SSHPrivateKeyFile,
PrivateKeyFile: b.config.RunConfig.Comm.SSHPrivateKey,
},
&awscommon.StepSecurityGroup{
SecurityGroupIds: b.config.SecurityGroupIds,
SSHPort: b.config.SSHPort,
CommConfig: &b.config.RunConfig.Comm,
VpcId: b.config.VpcId,
},
&awscommon.StepRunSourceInstance{
@ -109,20 +114,31 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
BlockDevices: b.config.BlockDevices,
Tags: b.config.RunTags,
},
&common.StepConnectSSH{
SSHAddress: awscommon.SSHAddress(
ec2conn, b.config.SSHPort, b.config.SSHPrivateIp),
SSHConfig: awscommon.SSHConfig(b.config.SSHUsername),
SSHWaitTimeout: b.config.SSHTimeout(),
&awscommon.StepGetPassword{
Comm: &b.config.RunConfig.Comm,
Timeout: b.config.WindowsPasswordTimeout,
},
&communicator.StepConnect{
Config: &b.config.RunConfig.Comm,
Host: awscommon.SSHHost(
ec2conn,
b.config.SSHPrivateIp),
SSHConfig: awscommon.SSHConfig(
b.config.RunConfig.Comm.SSHUsername),
},
&common.StepProvision{},
&stepStopInstance{SpotPrice: b.config.SpotPrice},
// TODO(mitchellh): verify works with spots
&stepModifyInstance{},
&awscommon.StepDeregisterAMI{
ForceDeregister: b.config.AMIForceDeregister,
AMIName: b.config.AMIName,
},
&stepCreateAMI{},
&awscommon.StepAMIRegionCopy{
AccessConfig: &b.config.AccessConfig,
Regions: b.config.AMIRegions,
Name: b.config.AMIName,
},
&awscommon.StepModifyAMIAttributes{
Description: b.config.AMIDescription,

View File

@ -1,10 +1,14 @@
package ebs
import (
"fmt"
"os"
"testing"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/mitchellh/packer/builder/amazon/common"
builderT "github.com/mitchellh/packer/helper/builder/testing"
"github.com/mitchellh/packer/packer"
)
func TestBuilderAcc_basic(t *testing.T) {
@ -15,6 +19,64 @@ func TestBuilderAcc_basic(t *testing.T) {
})
}
func TestBuilderAcc_regionCopy(t *testing.T) {
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Builder: &Builder{},
Template: testBuilderAccRegionCopy,
Check: checkRegionCopy([]string{"us-east-1", "us-west-2"}),
})
}
func TestBuilderAcc_forceDeregister(t *testing.T) {
// Build the same AMI name twice, with force_deregister on the second run
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Builder: &Builder{},
Template: buildForceDeregisterConfig("false", "dereg"),
SkipArtifactTeardown: true,
})
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Builder: &Builder{},
Template: buildForceDeregisterConfig("true", "dereg"),
})
}
func checkRegionCopy(regions []string) builderT.TestCheckFunc {
return func(artifacts []packer.Artifact) error {
if len(artifacts) > 1 {
return fmt.Errorf("more than 1 artifact")
}
// Get the actual *Artifact pointer so we can access the AMIs directly
artifactRaw := artifacts[0]
artifact, ok := artifactRaw.(*common.Artifact)
if !ok {
return fmt.Errorf("unknown artifact: %#v", artifactRaw)
}
// Verify that we copied to only the regions given
regionSet := make(map[string]struct{})
for _, r := range regions {
regionSet[r] = struct{}{}
}
for r, _ := range artifact.Amis {
if _, ok := regionSet[r]; !ok {
return fmt.Errorf("unknown region: %s", r)
}
delete(regionSet, r)
}
if len(regionSet) > 0 {
return fmt.Errorf("didn't copy to: %#v", regionSet)
}
return nil
}
}
func testAccPreCheck(t *testing.T) {
if v := os.Getenv("AWS_ACCESS_KEY_ID"); v == "" {
t.Fatal("AWS_ACCESS_KEY_ID must be set for acceptance tests")
@ -25,6 +87,16 @@ func testAccPreCheck(t *testing.T) {
}
}
func testEC2Conn() (*ec2.EC2, error) {
access := &common.AccessConfig{RawRegion: "us-east-1"}
config, err := access.Config()
if err != nil {
return nil, err
}
return ec2.New(config), nil
}
const testBuilderAccBasic = `
{
"builders": [{
@ -37,3 +109,35 @@ const testBuilderAccBasic = `
}]
}
`
const testBuilderAccRegionCopy = `
{
"builders": [{
"type": "test",
"region": "us-east-1",
"instance_type": "m3.medium",
"source_ami": "ami-76b2a71e",
"ssh_username": "ubuntu",
"ami_name": "packer-test {{timestamp}}",
"ami_regions": ["us-east-1", "us-west-2"]
}]
}
`
const testBuilderAccForceDeregister = `
{
"builders": [{
"type": "test",
"region": "us-east-1",
"instance_type": "m3.medium",
"source_ami": "ami-76b2a71e",
"ssh_username": "ubuntu",
"force_deregister": "%s",
"ami_name": "packer-test-%s"
}]
}
`
func buildForceDeregisterConfig(name, flag string) string {
return fmt.Sprintf(testBuilderAccForceDeregister, name, flag)
}

View File

@ -40,7 +40,7 @@ func (s *stepStopInstance) Run(state multistep.StateBag) multistep.StepAction {
stateChange := awscommon.StateChangeConf{
Pending: []string{"running", "stopping"},
Target: "stopped",
Refresh: awscommon.InstanceStateRefreshFunc(ec2conn, instance),
Refresh: awscommon.InstanceStateRefreshFunc(ec2conn, *instance.InstanceID),
StepState: state,
}
_, err = awscommon.WaitForState(&stateChange)

View File

@ -0,0 +1,114 @@
package ebs
import (
"fmt"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/mitchellh/packer/builder/amazon/common"
builderT "github.com/mitchellh/packer/helper/builder/testing"
"github.com/mitchellh/packer/packer"
)
func TestBuilderTagsAcc_basic(t *testing.T) {
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Builder: &Builder{},
Template: testBuilderTagsAccBasic,
Check: checkTags(),
})
}
func checkTags() builderT.TestCheckFunc {
return func(artifacts []packer.Artifact) error {
if len(artifacts) > 1 {
return fmt.Errorf("more than 1 artifact")
}
tags := make(map[string]string)
tags["OS_Version"] = "Ubuntu"
tags["Release"] = "Latest"
// Get the actual *Artifact pointer so we can access the AMIs directly
artifactRaw := artifacts[0]
artifact, ok := artifactRaw.(*common.Artifact)
if !ok {
return fmt.Errorf("unknown artifact: %#v", artifactRaw)
}
// describe the image, get block devices with a snapshot
ec2conn, _ := testEC2Conn()
imageResp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{
ImageIDs: []*string{aws.String(artifact.Amis["us-east-1"])},
})
if err != nil {
return fmt.Errorf("Error retrieving details for AMI Artifcat (%#v) in Tags Test: %s", artifact, err)
}
if len(imageResp.Images) == 0 {
return fmt.Errorf("No images found for AMI Artifcat (%#v) in Tags Test: %s", artifact, err)
}
image := imageResp.Images[0]
// Check only those with a Snapshot ID, i.e. not Ephemeral
var snapshots []*string
for _, device := range image.BlockDeviceMappings {
if device.EBS != nil && device.EBS.SnapshotID != nil {
snapshots = append(snapshots, device.EBS.SnapshotID)
}
}
// grab matching snapshot info
resp, err := ec2conn.DescribeSnapshots(&ec2.DescribeSnapshotsInput{
SnapshotIDs: snapshots,
})
if err != nil {
return fmt.Errorf("Error retreiving Snapshots for AMI Artifcat (%#v) in Tags Test: %s", artifact, err)
}
if len(resp.Snapshots) == 0 {
return fmt.Errorf("No Snapshots found for AMI Artifcat (%#v) in Tags Test", artifact)
}
// grab the snapshots, check the tags
for _, s := range resp.Snapshots {
expected := len(tags)
for _, t := range s.Tags {
for key, value := range tags {
if key == *t.Key && value == *t.Value {
expected--
}
}
}
if expected > 0 {
return fmt.Errorf("Not all tags found")
}
}
return nil
}
}
const testBuilderTagsAccBasic = `
{
"builders": [
{
"type": "test",
"region": "us-east-1",
"source_ami": "ami-9eaa1cf6",
"instance_type": "t2.micro",
"ssh_username": "ubuntu",
"ami_name": "packer-tags-testing-{{timestamp}}",
"tags": {
"OS_Version": "Ubuntu",
"Release": "Latest"
}
}
]
}
`

View File

@ -13,6 +13,7 @@ import (
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"
@ -73,6 +74,15 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
}
if b.config.BundleUploadCommand == "" {
if b.config.IamInstanceProfile != "" {
b.config.BundleUploadCommand = "sudo -i -n ec2-upload-bundle " +
"-b {{.BucketName}} " +
"-m {{.ManifestPath}} " +
"-d {{.BundleDirectory}} " +
"--batch " +
"--region {{.Region}} " +
"--retry"
} else {
b.config.BundleUploadCommand = "sudo -i -n ec2-upload-bundle " +
"-b {{.BucketName}} " +
"-m {{.ManifestPath}} " +
@ -80,9 +90,10 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
"-s {{.SecretKey}} " +
"-d {{.BundleDirectory}} " +
"--batch " +
"--location {{.Region}} " +
"--region {{.Region}} " +
"--retry"
}
}
if b.config.BundleVolCommand == "" {
b.config.BundleVolCommand = "sudo -i -n ec2-bundle-vol " +
@ -157,6 +168,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
// Build the steps
steps := []multistep.Step{
&awscommon.StepPreValidate{
DestAmiName: b.config.AMIName,
ForceDeregister: b.config.AMIForceDeregister,
},
&awscommon.StepSourceAMIInfo{
SourceAmi: b.config.SourceAmi,
EnhancedNetworking: b.config.AMIEnhancedNetworking,
@ -165,11 +180,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Debug: b.config.PackerDebug,
DebugKeyPath: fmt.Sprintf("ec2_%s.pem", b.config.PackerBuildName),
KeyPairName: b.config.TemporaryKeyPairName,
PrivateKeyFile: b.config.SSHPrivateKeyFile,
PrivateKeyFile: b.config.RunConfig.Comm.SSHPrivateKey,
},
&awscommon.StepSecurityGroup{
CommConfig: &b.config.RunConfig.Comm,
SecurityGroupIds: b.config.SecurityGroupIds,
SSHPort: b.config.SSHPort,
VpcId: b.config.VpcId,
},
&awscommon.StepRunSourceInstance{
@ -187,11 +202,17 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
BlockDevices: b.config.BlockDevices,
Tags: b.config.RunTags,
},
&common.StepConnectSSH{
SSHAddress: awscommon.SSHAddress(
ec2conn, b.config.SSHPort, b.config.SSHPrivateIp),
SSHConfig: awscommon.SSHConfig(b.config.SSHUsername),
SSHWaitTimeout: b.config.SSHTimeout(),
&awscommon.StepGetPassword{
Comm: &b.config.RunConfig.Comm,
Timeout: b.config.WindowsPasswordTimeout,
},
&communicator.StepConnect{
Config: &b.config.RunConfig.Comm,
Host: awscommon.SSHHost(
ec2conn,
b.config.SSHPrivateIp),
SSHConfig: awscommon.SSHConfig(
b.config.RunConfig.Comm.SSHUsername),
},
&common.StepProvision{},
&StepUploadX509Cert{},
@ -201,10 +222,15 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&StepUploadBundle{
Debug: b.config.PackerDebug,
},
&awscommon.StepDeregisterAMI{
ForceDeregister: b.config.AMIForceDeregister,
AMIName: b.config.AMIName,
},
&StepRegisterAMI{},
&awscommon.StepAMIRegionCopy{
AccessConfig: &b.config.AccessConfig,
Regions: b.config.AMIRegions,
Name: b.config.AMIName,
},
&awscommon.StepModifyAMIAttributes{
Description: b.config.AMIDescription,

View File

@ -1,76 +0,0 @@
// 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)
}

View File

@ -1,382 +0,0 @@
// 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
}

View File

@ -1,457 +0,0 @@
// 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 (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"strconv"
"strings"
)
type DigitalOceanClientV2 struct {
// The http client for communicating
client *http.Client
// Credentials
APIToken string
// The base URL of the API
APIURL string
}
// Creates a new client for communicating with DO
func DigitalOceanClientNewV2(token string, url string) *DigitalOceanClientV2 {
c := &DigitalOceanClientV2{
client: &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
},
},
APIURL: url,
APIToken: token,
}
return c
}
// Creates an SSH Key and returns it's id
func (d DigitalOceanClientV2) CreateKey(name string, pub string) (uint, error) {
type KeyReq struct {
Name string `json:"name"`
PublicKey string `json:"public_key"`
}
type KeyRes struct {
SSHKey struct {
Id uint
Name string
Fingerprint string
PublicKey string `json:"public_key"`
} `json:"ssh_key"`
}
req := &KeyReq{Name: name, PublicKey: pub}
res := KeyRes{}
err := NewRequestV2(d, "v2/account/keys", "POST", req, &res)
if err != nil {
return 0, err
}
return res.SSHKey.Id, err
}
// Destroys an SSH key
func (d DigitalOceanClientV2) DestroyKey(id uint) error {
path := fmt.Sprintf("v2/account/keys/%v", id)
return NewRequestV2(d, path, "DELETE", nil, nil)
}
// Creates a droplet and returns it's id
func (d DigitalOceanClientV2) CreateDroplet(name string, size string, image string, region string, keyId uint, privateNetworking bool) (uint, error) {
type DropletReq struct {
Name string `json:"name"`
Region string `json:"region"`
Size string `json:"size"`
Image string `json:"image"`
SSHKeys []string `json:"ssh_keys,omitempty"`
Backups bool `json:"backups,omitempty"`
IPv6 bool `json:"ipv6,omitempty"`
PrivateNetworking bool `json:"private_networking,omitempty"`
}
type DropletRes struct {
Droplet struct {
Id uint
Name string
Memory uint
VCPUS uint `json:"vcpus"`
Disk uint
Region Region
Image Image
Size Size
Locked bool
CreateAt string `json:"created_at"`
Status string
Networks struct {
V4 []struct {
IPAddr string `json:"ip_address"`
Netmask string
Gateway string
Type string
} `json:"v4,omitempty"`
V6 []struct {
IPAddr string `json:"ip_address"`
CIDR uint `json:"cidr"`
Gateway string
Type string
} `json:"v6,omitempty"`
}
Kernel struct {
Id uint
Name string
Version string
}
BackupIds []uint
SnapshotIds []uint
ActionIds []uint
Features []string `json:"features,omitempty"`
}
}
req := &DropletReq{Name: name}
res := DropletRes{}
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)
}
req.Size = found_size.Slug
req.Image = found_image.Slug
req.Region = found_region.Slug
req.SSHKeys = []string{fmt.Sprintf("%v", keyId)}
req.PrivateNetworking = privateNetworking
err = NewRequestV2(d, "v2/droplets", "POST", req, &res)
if err != nil {
return 0, err
}
return res.Droplet.Id, err
}
// Destroys a droplet
func (d DigitalOceanClientV2) DestroyDroplet(id uint) error {
path := fmt.Sprintf("v2/droplets/%v", id)
return NewRequestV2(d, path, "DELETE", nil, nil)
}
// Powers off a droplet
func (d DigitalOceanClientV2) PowerOffDroplet(id uint) error {
type ActionReq struct {
Type string `json:"type"`
}
type ActionRes struct {
}
req := &ActionReq{Type: "power_off"}
path := fmt.Sprintf("v2/droplets/%v/actions", id)
return NewRequestV2(d, path, "POST", req, nil)
}
// Shutsdown a droplet. This is a "soft" shutdown.
func (d DigitalOceanClientV2) ShutdownDroplet(id uint) error {
type ActionReq struct {
Type string `json:"type"`
}
type ActionRes struct {
}
req := &ActionReq{Type: "shutdown"}
path := fmt.Sprintf("v2/droplets/%v/actions", id)
return NewRequestV2(d, path, "POST", req, nil)
}
// Creates a snaphot of a droplet by it's ID
func (d DigitalOceanClientV2) CreateSnapshot(id uint, name string) error {
type ActionReq struct {
Type string `json:"type"`
Name string `json:"name"`
}
type ActionRes struct {
}
req := &ActionReq{Type: "snapshot", Name: name}
path := fmt.Sprintf("v2/droplets/%v/actions", id)
return NewRequestV2(d, path, "POST", req, nil)
}
// Returns all available images.
func (d DigitalOceanClientV2) Images() ([]Image, error) {
res := ImagesResp{}
err := NewRequestV2(d, "v2/images?per_page=200", "GET", nil, &res)
if err != nil {
return nil, err
}
return res.Images, nil
}
// Destroys an image by its ID.
func (d DigitalOceanClientV2) DestroyImage(id uint) error {
path := fmt.Sprintf("v2/images/%d", id)
return NewRequestV2(d, path, "DELETE", nil, nil)
}
// Returns DO's string representation of status "off" "new" "active" etc.
func (d DigitalOceanClientV2) DropletStatus(id uint) (string, string, error) {
path := fmt.Sprintf("v2/droplets/%v", id)
type DropletRes struct {
Droplet struct {
Id uint
Name string
Memory uint
VCPUS uint `json:"vcpus"`
Disk uint
Region Region
Image Image
Size Size
Locked bool
CreateAt string `json:"created_at"`
Status string
Networks struct {
V4 []struct {
IPAddr string `json:"ip_address"`
Netmask string
Gateway string
Type string
} `json:"v4,omitempty"`
V6 []struct {
IPAddr string `json:"ip_address"`
CIDR uint `json:"cidr"`
Gateway string
Type string
} `json:"v6,omitempty"`
}
Kernel struct {
Id uint
Name string
Version string
}
BackupIds []uint
SnapshotIds []uint
ActionIds []uint
Features []string `json:"features,omitempty"`
}
}
res := DropletRes{}
err := NewRequestV2(d, path, "GET", nil, &res)
if err != nil {
return "", "", err
}
var ip string
for _, n := range res.Droplet.Networks.V4 {
if n.Type == "public" {
ip = n.IPAddr
}
}
return ip, res.Droplet.Status, err
}
// Sends an api request and returns a generic map[string]interface of
// the response.
func NewRequestV2(d DigitalOceanClientV2, path string, method string, req interface{}, res interface{}) error {
var err error
var request *http.Request
client := d.client
buf := new(bytes.Buffer)
// Add the authentication parameters
url := fmt.Sprintf("%s/%s", d.APIURL, path)
if req != nil {
enc := json.NewEncoder(buf)
enc.Encode(req)
defer buf.Reset()
request, err = http.NewRequest(method, url, buf)
request.Header.Add("Content-Type", "application/json")
} else {
request, err = http.NewRequest(method, url, nil)
}
if err != nil {
return err
}
// Add the authentication parameters
request.Header.Add("Authorization", "Bearer "+d.APIToken)
if buf != nil {
log.Printf("sending new request to digitalocean: %s buffer: %s", url, buf)
} else {
log.Printf("sending new request to digitalocean: %s", url)
}
resp, err := client.Do(request)
if err != nil {
return err
}
if method == "DELETE" && resp.StatusCode == 204 {
if resp.Body != nil {
resp.Body.Close()
}
return nil
}
if resp.Body == nil {
return errors.New("Request returned empty body")
}
body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return err
}
log.Printf("response from digitalocean: %s", body)
err = json.Unmarshal(body, &res)
if err != nil {
return errors.New(fmt.Sprintf("Failed to decode JSON response %s (HTTP %v) from DigitalOcean: %s", err.Error(),
resp.StatusCode, body))
}
switch resp.StatusCode {
case 403, 401, 429, 422, 404, 503, 500:
return errors.New(fmt.Sprintf("digitalocean request error: %+v", res))
}
return nil
}
func (d DigitalOceanClientV2) 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 DigitalOceanClientV2) Regions() ([]Region, error) {
res := RegionsResp{}
err := NewRequestV2(d, "v2/regions?per_page=200", "GET", nil, &res)
if err != nil {
return nil, err
}
return res.Regions, nil
}
func (d DigitalOceanClientV2) 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 DigitalOceanClientV2) Sizes() ([]Size, error) {
res := SizesResp{}
err := NewRequestV2(d, "v2/sizes?per_page=200", "GET", nil, &res)
if err != nil {
return nil, err
}
return res.Sizes, nil
}
func (d DigitalOceanClientV2) 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
}

View File

@ -4,6 +4,8 @@ import (
"fmt"
"log"
"strconv"
"github.com/digitalocean/godo"
)
type Artifact struct {
@ -11,13 +13,13 @@ type Artifact struct {
snapshotName string
// The ID of the image
snapshotId uint
snapshotId int
// The name of the region
regionName string
// The client for making API calls
client DigitalOceanClient
client *godo.Client
}
func (*Artifact) BuilderId() string {
@ -43,5 +45,6 @@ func (a *Artifact) State(name string) interface{} {
func (a *Artifact) Destroy() error {
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
}

View File

@ -4,208 +4,39 @@
package digitalocean
import (
"errors"
"fmt"
"log"
"os"
"time"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/helper/communicator"
"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
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 {
config Config
runner multistep.Runner
}
func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
err := config.Decode(&b.config, &config.DecodeOpts{
Interpolate: true,
}, raws...)
if err != nil {
return nil, err
c, warnings, errs := NewConfig(raws...)
if errs != nil {
return warnings, errs
}
b.config = *c
// 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())
}
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
}
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
var client DigitalOceanClient
// Initialize the DO API client
if b.config.APIToken == "" {
client = DigitalOceanClientNewV1(b.config.ClientID, b.config.APIKey, b.config.APIURL)
} else {
client = DigitalOceanClientNewV2(b.config.APIToken, b.config.APIURL)
}
client := godo.NewClient(oauth2.NewClient(oauth2.NoContext, &apiTokenSource{
AccessToken: b.config.APIToken,
}))
// Set up the state
state := new(multistep.BasicStateBag)
@ -216,13 +47,16 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
// Build the steps
steps := []multistep.Step{
new(stepCreateSSHKey),
&stepCreateSSHKey{
Debug: b.config.PackerDebug,
DebugKeyPath: fmt.Sprintf("do_%s.pem", b.config.PackerBuildName),
},
new(stepCreateDroplet),
new(stepDropletInfo),
&common.StepConnectSSH{
SSHAddress: sshAddress,
&communicator.StepConnect{
Config: &b.config.Comm,
Host: commHost,
SSHConfig: sshConfig,
SSHWaitTimeout: 5 * time.Minute,
},
new(common.StepProvision),
new(stepShutdown),
@ -252,26 +86,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
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{
snapshotName: state.Get("snapshot_name").(string),
snapshotId: state.Get("snapshot_image_id").(uint),
regionName: found_region.Name,
snapshotId: state.Get("snapshot_image_id").(int),
regionName: state.Get("region").(string),
client: client,
}

View File

@ -0,0 +1,33 @@
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"
}]
}
`

View File

@ -1,22 +1,19 @@
package digitalocean
import (
"github.com/mitchellh/packer/packer"
"os"
"strconv"
"testing"
)
"time"
func init() {
// Clear out the credential env vars
os.Setenv("DIGITALOCEAN_API_KEY", "")
os.Setenv("DIGITALOCEAN_CLIENT_ID", "")
}
"github.com/mitchellh/packer/packer"
)
func testConfig() map[string]interface{} {
return map[string]interface{}{
"client_id": "foo",
"api_key": "bar",
"api_token": "bar",
"region": "nyc2",
"size": "512mb",
"image": "foo",
}
}
@ -43,90 +40,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) {
var b Builder
config := testConfig()
@ -147,22 +60,18 @@ func TestBuilderPrepare_Region(t *testing.T) {
config := testConfig()
// Test default
delete(config, "region")
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.Region != DefaultRegion {
t.Errorf("found %s, expected %s", b.config.Region, DefaultRegion)
if err == nil {
t.Fatalf("should error")
}
expected := "sfo1"
// Test set
config["region_id"] = 0
config["region"] = expected
b = Builder{}
warnings, err = b.Prepare(config)
@ -183,22 +92,18 @@ func TestBuilderPrepare_Size(t *testing.T) {
config := testConfig()
// Test default
delete(config, "size")
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.Size != DefaultSize {
t.Errorf("found %s, expected %s", b.config.Size, DefaultSize)
if err == nil {
t.Fatalf("should error")
}
expected := "1024mb"
// Test set
config["size_id"] = 0
config["size"] = expected
b = Builder{}
warnings, err = b.Prepare(config)
@ -219,22 +124,18 @@ func TestBuilderPrepare_Image(t *testing.T) {
config := testConfig()
// Test default
delete(config, "image")
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.Image != DefaultImage {
t.Errorf("found %s, expected %s", b.config.Image, DefaultImage)
if err == nil {
t.Fatal("should error")
}
expected := "ubuntu-14-04-x64"
// Test set
config["image_id"] = 0
config["image"] = expected
b = Builder{}
warnings, err = b.Prepare(config)
@ -263,8 +164,8 @@ func TestBuilderPrepare_SSHUsername(t *testing.T) {
t.Fatalf("should not have error: %s", err)
}
if b.config.SSHUsername != "root" {
t.Errorf("invalid: %s", b.config.SSHUsername)
if b.config.Comm.SSHUsername != "root" {
t.Errorf("invalid: %s", b.config.Comm.SSHUsername)
}
// Test set
@ -278,52 +179,11 @@ func TestBuilderPrepare_SSHUsername(t *testing.T) {
t.Fatalf("should not have error: %s", err)
}
if b.config.SSHUsername != "foo" {
t.Errorf("invalid: %s", b.config.SSHUsername)
if b.config.Comm.SSHUsername != "foo" {
t.Errorf("invalid: %s", b.config.Comm.SSHUsername)
}
}
func TestBuilderPrepare_SSHTimeout(t *testing.T) {
var b Builder
config := testConfig()
// Test default
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.RawSSHTimeout != "1m" {
t.Errorf("invalid: %s", b.config.RawSSHTimeout)
}
// Test set
config["ssh_timeout"] = "30s"
b = Builder{}
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)
}
// Test bad
config["ssh_timeout"] = "tubes"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_StateTimeout(t *testing.T) {
var b Builder
config := testConfig()
@ -337,8 +197,8 @@ func TestBuilderPrepare_StateTimeout(t *testing.T) {
t.Fatalf("should not have error: %s", err)
}
if b.config.RawStateTimeout != "6m" {
t.Errorf("invalid: %s", b.config.RawStateTimeout)
if b.config.StateTimeout != 6*time.Minute {
t.Errorf("invalid: %s", b.config.StateTimeout)
}
// Test set

View File

@ -0,0 +1,118 @@
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/communicator"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"
)
type Config struct {
common.PackerConfig `mapstructure:",squash"`
Comm communicator.Config `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"`
StateTimeout time.Duration `mapstructure:"state_timeout"`
DropletName string `mapstructure:"droplet_name"`
UserData string `mapstructure:"user_data"`
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.Comm.SSHUsername == "" {
// Default to "root". You can override this if your
// SourceImage has a different user account then the DO default
c.Comm.SSHUsername = "root"
}
if c.StateTimeout == 0 {
// Default to 6 minute timeouts waiting for
// desired state. i.e waiting for droplet to become active
c.StateTimeout = 6 * time.Minute
}
var errs *packer.MultiError
if es := c.Comm.Prepare(c.ctx); len(es) > 0 {
errs = packer.MultiErrorAppend(errs, es...)
}
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"))
}
if errs != nil && len(errs.Errors) > 0 {
return nil, nil, errs
}
common.ScrubConfig(c, c.APIToken)
return c, nil, nil
}

View File

@ -1,15 +1,15 @@
package digitalocean
import (
"golang.org/x/crypto/ssh"
"fmt"
"golang.org/x/crypto/ssh"
"github.com/mitchellh/multistep"
)
func sshAddress(state multistep.StateBag) (string, error) {
config := state.Get("config").(Config)
func commHost(state multistep.StateBag) (string, error) {
ipAddress := state.Get("droplet_ip").(string)
return fmt.Sprintf("%s:%d", ipAddress, config.SSHPort), nil
return ipAddress, nil
}
func sshConfig(state multistep.StateBag) (*ssh.ClientConfig, error) {
@ -22,7 +22,7 @@ func sshConfig(state multistep.StateBag) (*ssh.ClientConfig, error) {
}
return &ssh.ClientConfig{
User: config.SSHUsername,
User: config.Comm.SSHUsername,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},

View File

@ -3,25 +3,36 @@ package digitalocean
import (
"fmt"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
type stepCreateDroplet struct {
dropletId uint
dropletId int
}
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)
c := state.Get("config").(Config)
sshKeyId := state.Get("ssh_key_id").(uint)
ui.Say("Creating droplet...")
sshKeyId := state.Get("ssh_key_id").(int)
// 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 {
err := fmt.Errorf("Error creating droplet: %s", err)
state.Put("error", err)
@ -30,10 +41,10 @@ func (s *stepCreateDroplet) Run(state multistep.StateBag) multistep.StepAction {
}
// We use this in cleanup
s.dropletId = dropletId
s.dropletId = droplet.ID
// Store the droplet id for later
state.Put("droplet_id", dropletId)
state.Put("droplet_id", droplet.ID)
return multistep.ActionContinue
}
@ -44,19 +55,14 @@ func (s *stepCreateDroplet) Cleanup(state multistep.StateBag) {
return
}
client := state.Get("client").(DigitalOceanClient)
client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(Config)
// Destroy the droplet we just created
ui.Say("Destroying droplet...")
err := client.DestroyDroplet(s.dropletId)
_, err := client.Droplets.Delete(s.dropletId)
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(
"Error destroying droplet. Please destroy it manually: %v", curlstr))
"Error destroying droplet. Please destroy it manually: %s", err))
}
}

View File

@ -7,19 +7,25 @@ import (
"encoding/pem"
"fmt"
"log"
"os"
"runtime"
"code.google.com/p/gosshold/ssh"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/packer"
)
type stepCreateSSHKey struct {
keyId uint
Debug bool
DebugKeyPath string
keyId int
}
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.Say("Creating temporary ssh key for droplet...")
@ -46,7 +52,10 @@ func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction {
name := fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
// 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 {
err := fmt.Errorf("Error creating temporary SSH key: %s", err)
state.Put("error", err)
@ -55,12 +64,37 @@ func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction {
}
// We use this to check cleanup
s.keyId = keyId
s.keyId = key.ID
log.Printf("temporary ssh key name: %s", name)
// 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
}
@ -71,18 +105,14 @@ func (s *stepCreateSSHKey) Cleanup(state multistep.StateBag) {
return
}
client := state.Get("client").(DigitalOceanClient)
client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(Config)
ui.Say("Deleting temporary ssh key...")
err := client.DestroyKey(s.keyId)
curlstr := fmt.Sprintf("curl -H 'Authorization: Bearer #TOKEN#' -X DELETE '%v/v2/account/keys/%v'", c.APIURL, s.keyId)
_, err := client.Keys.DeleteByID(s.keyId)
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(
"Error cleaning up ssh key. Please delete the key manually: %v", curlstr))
"Error cleaning up ssh key. Please delete the key manually: %s", err))
}
}

View File

@ -3,6 +3,7 @@ package digitalocean
import (
"fmt"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
@ -10,14 +11,14 @@ import (
type stepDropletInfo struct{}
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)
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...")
err := waitForDropletState("active", dropletId, client, c.stateTimeout)
err := waitForDropletState("active", dropletId, client, c.StateTimeout)
if err != nil {
err := fmt.Errorf("Error waiting for droplet to become active: %s", err)
state.Put("error", err)
@ -26,16 +27,25 @@ func (s *stepDropletInfo) Run(state multistep.StateBag) multistep.StepAction {
}
// Set the IP on the state for later
ip, _, err := client.DropletStatus(dropletId)
droplet, _, err := client.Droplets.Get(dropletId)
if err != nil {
err := fmt.Errorf("Error retrieving droplet ID: %s", err)
err := fmt.Errorf("Error retrieving droplet: %s", err)
state.Put("error", err)
ui.Error(err.Error())
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
}

View File

@ -3,7 +3,9 @@ package digitalocean
import (
"fmt"
"log"
"time"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
@ -11,12 +13,12 @@ import (
type stepPowerOff struct{}
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)
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 {
err := fmt.Errorf("Error checking droplet state: %s", err)
state.Put("error", err)
@ -24,14 +26,14 @@ func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionHalt
}
if status == "off" {
if droplet.Status == "off" {
// Droplet is already off, don't do anything
return multistep.ActionContinue
}
// Pull the plug on the Droplet
ui.Say("Forcefully shutting down Droplet...")
err = client.PowerOffDroplet(dropletId)
_, _, err = client.DropletActions.PowerOff(dropletId)
if err != nil {
err := fmt.Errorf("Error powering off droplet: %s", err)
state.Put("error", err)
@ -40,13 +42,22 @@ func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
}
log.Println("Waiting for poweroff event to complete...")
err = waitForDropletState("off", dropletId, client, c.stateTimeout)
err = waitForDropletState("off", dropletId, client, c.StateTimeout)
if err != nil {
state.Put("error", err)
ui.Error(err.Error())
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
}

View File

@ -5,6 +5,7 @@ import (
"log"
"time"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
@ -12,16 +13,16 @@ import (
type stepShutdown struct{}
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)
dropletId := state.Get("droplet_id").(uint)
dropletId := state.Get("droplet_id").(int)
// Gracefully power off the droplet. We have to retry this a number
// of times because sometimes it says it completed when it actually
// did absolutely nothing (*ALAKAZAM!* magic!). We give up after
// a pretty arbitrary amount of time.
ui.Say("Gracefully shutting down droplet...")
err := client.ShutdownDroplet(dropletId)
_, _, err := client.DropletActions.Shutdown(dropletId)
if err != nil {
// If we get an error the first time, actually report it
err := fmt.Errorf("Error shutting down droplet: %s", err)
@ -48,7 +49,7 @@ func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
for attempts := 2; attempts > 0; attempts++ {
log.Printf("ShutdownDroplet attempt #%d...", attempts)
err := client.ShutdownDroplet(dropletId)
_, _, err := client.DropletActions.Shutdown(dropletId)
if err != nil {
log.Printf("Shutdown retry error: %s", err)
}
@ -64,7 +65,19 @@ func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
err = waitForDropletState("off", dropletId, client, 2*time.Minute)
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

View File

@ -4,7 +4,9 @@ import (
"errors"
"fmt"
"log"
"time"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
@ -12,13 +14,13 @@ import (
type stepSnapshot struct{}
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)
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))
err := client.CreateSnapshot(dropletId, c.SnapshotName)
_, _, err := client.DropletActions.Snapshot(dropletId, c.SnapshotName)
if err != nil {
err := fmt.Errorf("Error creating snapshot: %s", err)
state.Put("error", err)
@ -26,8 +28,20 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
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...")
err = waitForDropletState("active", dropletId, client, c.stateTimeout)
err = waitForDropletState("active", dropletId, client, c.StateTimeout)
if err != nil {
err := fmt.Errorf("Error waiting for snapshot to complete: %s", err)
state.Put("error", err)
@ -36,7 +50,7 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
}
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 {
err := fmt.Errorf("Error looking up snapshot ID: %s", err)
state.Put("error", err)
@ -44,10 +58,10 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionHalt
}
var imageId uint
var imageId int
for _, image := range images {
if image.Name == c.SnapshotName {
imageId = image.Id
imageId = image.ID
break
}
}
@ -60,7 +74,6 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
}
log.Printf("Snapshot image ID: %d", imageId)
state.Put("snapshot_image_id", imageId)
state.Put("snapshot_name", c.SnapshotName)
state.Put("region", c.Region)

View File

@ -0,0 +1,15 @@
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
}

View File

@ -4,11 +4,64 @@ import (
"fmt"
"log"
"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
// 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{})
defer close(done)
@ -19,13 +72,13 @@ func waitForDropletState(desiredState string, dropletId uint, client DigitalOcea
attempts += 1
log.Printf("Checking droplet status... (attempt: %d)", attempts)
_, status, err := client.DropletStatus(dropletId)
droplet, _, err := client.Droplets.Get(dropletId)
if err != nil {
result <- err
return
}
if status == desiredState {
if droplet.Status == desiredState {
result <- nil
return
}

View File

@ -5,6 +5,7 @@ import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer"
)
@ -42,7 +43,15 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&StepTempDir{},
&StepPull{},
&StepRun{},
&StepProvision{},
&communicator.StepConnect{
Config: &b.config.Comm,
Host: commHost,
SSHConfig: sshConfig(&b.config.Comm),
CustomConnect: map[string]multistep.Step{
"docker": &StepConnectDocker{},
},
},
&common.StepProvision{},
}
if b.config.Commit {

52
builder/docker/comm.go Normal file
View File

@ -0,0 +1,52 @@
package docker
import (
"fmt"
"io/ioutil"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/communicator/ssh"
"github.com/mitchellh/packer/helper/communicator"
gossh "golang.org/x/crypto/ssh"
)
func commHost(state multistep.StateBag) (string, error) {
containerId := state.Get("container_id").(string)
driver := state.Get("driver").(Driver)
return driver.IPAddress(containerId)
}
func sshConfig(comm *communicator.Config) func(state multistep.StateBag) (*gossh.ClientConfig, error) {
return func(state multistep.StateBag) (*gossh.ClientConfig, error) {
if comm.SSHPrivateKey != "" {
// key based auth
bytes, err := ioutil.ReadFile(comm.SSHPrivateKey)
if err != nil {
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
}
privateKey := string(bytes)
signer, err := gossh.ParsePrivateKey([]byte(privateKey))
if err != nil {
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
}
return &gossh.ClientConfig{
User: comm.SSHUsername,
Auth: []gossh.AuthMethod{
gossh.PublicKeys(signer),
},
}, nil
} else {
// password based auth
return &gossh.ClientConfig{
User: comm.SSHUsername,
Auth: []gossh.AuthMethod{
gossh.Password(comm.SSHPassword),
gossh.KeyboardInteractive(
ssh.PasswordKeyboardInteractive(comm.SSHPassword)),
},
}, nil
}
}
}

View File

@ -6,6 +6,7 @@ import (
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"
@ -13,6 +14,7 @@ import (
type Config struct {
common.PackerConfig `mapstructure:",squash"`
Comm communicator.Config `mapstructure:",squash"`
Commit bool
ExportPath string `mapstructure:"export_path"`
@ -31,10 +33,10 @@ type Config struct {
}
func NewConfig(raws ...interface{}) (*Config, []string, error) {
var c Config
c := new(Config)
var md mapstructure.Metadata
err := config.Decode(&c, &config.DecodeOpts{
err := config.Decode(c, &config.DecodeOpts{
Metadata: &md,
Interpolate: true,
InterpolateFilter: &interpolate.RenderFilter{
@ -69,7 +71,15 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
c.Pull = true
}
// Default to the normal Docker type
if c.Comm.Type == "" {
c.Comm.Type = "docker"
}
var errs *packer.MultiError
if es := c.Comm.Prepare(&c.ctx); len(es) > 0 {
errs = packer.MultiErrorAppend(errs, es...)
}
if c.Image == "" {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("image must be specified"))
@ -91,5 +101,5 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
return nil, nil, errs
}
return &c, nil, nil
return c, nil, nil
}

View File

@ -22,6 +22,10 @@ type Driver interface {
// Import imports a container from a tar file
Import(path, repo string) (string, error)
// IPAddress returns the address of the container that can be used
// for external access.
IPAddress(id string) (string, error)
// Login. This will lock the driver from performing another Login
// until Logout is called. Therefore, any users MUST call Logout.
Login(repo, email, username, password string) error

View File

@ -116,6 +116,23 @@ func (d *DockerDriver) Import(path string, repo string) (string, error) {
return strings.TrimSpace(stdout.String()), nil
}
func (d *DockerDriver) IPAddress(id string) (string, error) {
var stderr, stdout bytes.Buffer
cmd := exec.Command(
"docker",
"inspect",
"--format",
"{{ .NetworkSettings.IPAddress }}",
id)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("Error: %s\n\nStderr: %s", err, stderr.String())
}
return strings.TrimSpace(stdout.String()), nil
}
func (d *DockerDriver) Login(repo, email, user, pass string) error {
d.l.Lock()

View File

@ -23,6 +23,11 @@ type MockDriver struct {
ImportId string
ImportErr error
IPAddressCalled bool
IPAddressID string
IPAddressResult string
IPAddressErr error
LoginCalled bool
LoginEmail string
LoginUsername string
@ -104,6 +109,12 @@ func (d *MockDriver) Import(path, repo string) (string, error) {
return d.ImportId, d.ImportErr
}
func (d *MockDriver) IPAddress(id string) (string, error) {
d.IPAddressCalled = true
d.IPAddressID = id
return d.IPAddressResult, d.IPAddressErr
}
func (d *MockDriver) Login(r, e, u, p string) error {
d.LoginCalled = true
d.LoginRepo = r

View File

@ -2,12 +2,11 @@ package docker
import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
)
type StepProvision struct{}
type StepConnectDocker struct{}
func (s *StepProvision) Run(state multistep.StateBag) multistep.StepAction {
func (s *StepConnectDocker) Run(state multistep.StateBag) multistep.StepAction {
containerId := state.Get("container_id").(string)
driver := state.Get("driver").(Driver)
tempDir := state.Get("temp_dir").(string)
@ -28,8 +27,8 @@ func (s *StepProvision) Run(state multistep.StateBag) multistep.StepAction {
Version: version,
}
prov := common.StepProvision{Comm: comm}
return prov.Run(state)
state.Put("communicator", comm)
return multistep.ActionContinue
}
func (s *StepProvision) Cleanup(state multistep.StateBag) {}
func (s *StepConnectDocker) Cleanup(state multistep.StateBag) {}

View File

@ -4,11 +4,12 @@ package googlecompute
import (
"fmt"
"log"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer"
"log"
"time"
)
// The unique ID for this builder.
@ -60,10 +61,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&StepInstanceInfo{
Debug: b.config.PackerDebug,
},
&common.StepConnectSSH{
SSHAddress: sshAddress,
&communicator.StepConnect{
Config: &b.config.Comm,
Host: commHost,
SSHConfig: sshConfig,
SSHWaitTimeout: 5 * time.Minute,
},
new(common.StepProvision),
new(StepTeardownInstance),

View File

@ -7,6 +7,7 @@ import (
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"
@ -17,6 +18,7 @@ import (
// state of the config object.
type Config struct {
common.PackerConfig `mapstructure:",squash"`
Comm communicator.Config `mapstructure:",squash"`
AccountFile string `mapstructure:"account_file"`
ProjectId string `mapstructure:"project_id"`
@ -31,23 +33,19 @@ type Config struct {
Network string `mapstructure:"network"`
SourceImage string `mapstructure:"source_image"`
SourceImageProjectId string `mapstructure:"source_image_project_id"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPort uint `mapstructure:"ssh_port"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
RawStateTimeout string `mapstructure:"state_timeout"`
Tags []string `mapstructure:"tags"`
Zone string `mapstructure:"zone"`
account accountFile
privateKeyBytes []byte
sshTimeout time.Duration
stateTimeout time.Duration
ctx *interpolate.Context
}
func NewConfig(raws ...interface{}) (*Config, []string, error) {
c := new(Config)
err := config.Decode(&c, &config.DecodeOpts{
err := config.Decode(c, &config.DecodeOpts{
Interpolate: true,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{
@ -88,20 +86,12 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
c.MachineType = "n1-standard-1"
}
if c.RawSSHTimeout == "" {
c.RawSSHTimeout = "5m"
}
if c.RawStateTimeout == "" {
c.RawStateTimeout = "5m"
}
if c.SSHUsername == "" {
c.SSHUsername = "root"
}
if c.SSHPort == 0 {
c.SSHPort = 22
if c.Comm.SSHUsername == "" {
c.Comm.SSHUsername = "root"
}
var errs *packer.MultiError
@ -122,14 +112,6 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
errs, errors.New("a zone must be specified"))
}
// Process timeout settings.
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(

View File

@ -1,16 +1,15 @@
package googlecompute
import (
"golang.org/x/crypto/ssh"
"fmt"
"github.com/mitchellh/multistep"
"golang.org/x/crypto/ssh"
)
// sshAddress returns the ssh address.
func sshAddress(state multistep.StateBag) (string, error) {
config := state.Get("config").(*Config)
func commHost(state multistep.StateBag) (string, error) {
ipAddress := state.Get("instance_ip").(string)
return fmt.Sprintf("%s:%d", ipAddress, config.SSHPort), nil
return ipAddress, nil
}
// sshConfig returns the ssh configuration.
@ -24,7 +23,7 @@ func sshConfig(state multistep.StateBag) (*ssh.ClientConfig, error) {
}
return &ssh.ClientConfig{
User: config.SSHUsername,
User: config.Comm.SSHUsername,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},

View File

@ -32,7 +32,7 @@ func (config *Config) getInstanceMetadata(sshPublicKey string) map[string]string
// Merge any existing ssh keys with our public key
sshMetaKey := "sshKeys"
sshKeys := fmt.Sprintf("%s:%s", config.SSHUsername, sshPublicKey)
sshKeys := fmt.Sprintf("%s:%s", config.Comm.SSHUsername, sshPublicKey)
if confSshKeys, exists := instanceMetadata[sshMetaKey]; exists {
sshKeys = fmt.Sprintf("%s\n%s", sshKeys, confSshKeys)
}

View File

@ -1,11 +1,12 @@
package null
import (
"log"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer"
"log"
"time"
)
const BuilderId = "fnoeding.null"
@ -27,10 +28,13 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
steps := []multistep.Step{
&common.StepConnectSSH{
SSHAddress: SSHAddress(b.config.Host, b.config.Port),
SSHConfig: SSHConfig(b.config.SSHUsername, b.config.SSHPassword, b.config.SSHPrivateKeyFile),
SSHWaitTimeout: 1 * time.Minute,
&communicator.StepConnect{
Config: &b.config.CommConfig,
Host: CommHost(b.config.CommConfig.SSHHost),
SSHConfig: SSHConfig(
b.config.CommConfig.SSHUsername,
b.config.CommConfig.SSHPassword,
b.config.CommConfig.SSHPrivateKey),
},
&common.StepProvision{},
}

View File

@ -2,7 +2,9 @@ package null
import (
"fmt"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"
@ -11,49 +13,40 @@ import (
type Config struct {
common.PackerConfig `mapstructure:",squash"`
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPassword string `mapstructure:"ssh_password"`
SSHPrivateKeyFile string `mapstructure:"ssh_private_key_file"`
CommConfig communicator.Config `mapstructure:",squash"`
}
func NewConfig(raws ...interface{}) (*Config, []string, error) {
c := new(Config)
var c Config
err := config.Decode(&c, &config.DecodeOpts{
Interpolate: true,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{
"run_command",
},
},
InterpolateFilter: &interpolate.RenderFilter{},
}, raws...)
if err != nil {
return nil, nil, err
}
if c.Port == 0 {
c.Port = 22
}
var errs *packer.MultiError
if c.Host == "" {
if es := c.CommConfig.Prepare(nil); len(es) > 0 {
errs = packer.MultiErrorAppend(errs, es...)
}
if c.CommConfig.SSHHost == "" {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("host must be specified"))
}
if c.SSHUsername == "" {
if c.CommConfig.SSHUsername == "" {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("ssh_username must be specified"))
}
if c.SSHPassword == "" && c.SSHPrivateKeyFile == "" {
if c.CommConfig.SSHPassword == "" && c.CommConfig.SSHPrivateKey == "" {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("one of ssh_password and ssh_private_key_file must be specified"))
}
if c.SSHPassword != "" && c.SSHPrivateKeyFile != "" {
if c.CommConfig.SSHPassword != "" && c.CommConfig.SSHPrivateKey != "" {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("only one of ssh_password and ssh_private_key_file must be specified"))
}
@ -62,5 +55,5 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
return nil, nil, errs
}
return c, nil, nil
return &c, nil, nil
}

View File

@ -1,12 +1,15 @@
package null
import (
"os"
"testing"
"github.com/mitchellh/packer/helper/communicator"
)
func testConfig() map[string]interface{} {
return map[string]interface{}{
"host": "foo",
"ssh_host": "foo",
"ssh_username": "bar",
"ssh_password": "baz",
}
@ -48,8 +51,8 @@ func TestConfigPrepare_port(t *testing.T) {
// default port should be 22
delete(raw, "port")
c, warns, errs := NewConfig(raw)
if c.Port != 22 {
t.Fatalf("bad: port should default to 22, not %d", c.Port)
if c.CommConfig.SSHPort != 22 {
t.Fatalf("bad: port should default to 22, not %d", c.CommConfig.SSHPort)
}
testConfigOk(t, warns, errs)
}
@ -58,12 +61,12 @@ func TestConfigPrepare_host(t *testing.T) {
raw := testConfig()
// No host
delete(raw, "host")
delete(raw, "ssh_host")
_, warns, errs := NewConfig(raw)
testConfigErr(t, warns, errs)
// Good host
raw["host"] = "good"
raw["ssh_host"] = "good"
_, warns, errs = NewConfig(raw)
testConfigOk(t, warns, errs)
}
@ -97,7 +100,9 @@ func TestConfigPrepare_sshCredential(t *testing.T) {
testConfigOk(t, warns, errs)
// only ssh_private_key_file
raw["ssh_private_key_file"] = "good"
testFile := communicator.TestPEM(t)
defer os.Remove(testFile)
raw["ssh_private_key_file"] = testFile
delete(raw, "ssh_password")
_, warns, errs = NewConfig(raw)
testConfigOk(t, warns, errs)

View File

@ -1,18 +1,16 @@
package null
import (
gossh "golang.org/x/crypto/ssh"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/communicator/ssh"
gossh "golang.org/x/crypto/ssh"
"io/ioutil"
)
// SSHAddress returns a function that can be given to the SSH communicator
// for determining the SSH address
func SSHAddress(host string, port int) func(multistep.StateBag) (string, error) {
func CommHost(host string) func(multistep.StateBag) (string, error) {
return func(state multistep.StateBag) (string, error) {
return fmt.Sprintf("%s:%d", host, port), nil
return host, nil
}
}

View File

@ -4,99 +4,120 @@ import (
"crypto/tls"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"github.com/mitchellh/gophercloud-fork-40444fb"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/template/interpolate"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack"
)
// AccessConfig is for common configuration related to openstack access
type AccessConfig struct {
Username string `mapstructure:"username"`
UserID string `mapstructure:"user_id"`
Password string `mapstructure:"password"`
ApiKey string `mapstructure:"api_key"`
Project string `mapstructure:"project"`
Provider string `mapstructure:"provider"`
RawRegion string `mapstructure:"region"`
ProxyUrl string `mapstructure:"proxy_url"`
TenantId string `mapstructure:"tenant_id"`
APIKey string `mapstructure:"api_key"`
IdentityEndpoint string `mapstructure:"identity_endpoint"`
TenantID string `mapstructure:"tenant_id"`
TenantName string `mapstructure:"tenant_name"`
DomainID string `mapstructure:"domain_id"`
DomainName string `mapstructure:"domain_name"`
Insecure bool `mapstructure:"insecure"`
}
Region string `mapstructure:"region"`
EndpointType string `mapstructure:"endpoint_type"`
// Auth returns a valid Auth object for access to openstack services, or
// an error if the authentication couldn't be resolved.
func (c *AccessConfig) Auth() (gophercloud.AccessProvider, error) {
c.Username = common.ChooseString(c.Username, os.Getenv("SDK_USERNAME"), os.Getenv("OS_USERNAME"))
c.Password = common.ChooseString(c.Password, os.Getenv("SDK_PASSWORD"), os.Getenv("OS_PASSWORD"))
c.ApiKey = common.ChooseString(c.ApiKey, os.Getenv("SDK_API_KEY"))
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{
AllowReauth: true,
ApiKey: c.ApiKey,
TenantId: c.TenantId,
TenantName: c.Project,
Username: c.Username,
Password: c.Password,
}
default_transport := &http.Transport{}
if c.Insecure {
cfg := new(tls.Config)
cfg.InsecureSkipVerify = true
default_transport.TLSClientConfig = cfg
}
// For corporate networks it may be the case where we want our API calls
// to be sent through a separate HTTP proxy than external traffic.
if c.ProxyUrl != "" {
url, err := url.Parse(c.ProxyUrl)
if err != nil {
return nil, err
}
// The gophercloud.Context has a UseCustomClient method which
// would allow us to override with a new instance of http.Client.
default_transport.Proxy = http.ProxyURL(url)
}
if c.Insecure || c.ProxyUrl != "" {
http.DefaultTransport = default_transport
}
return gophercloud.Authenticate(c.Provider, authoptions)
}
func (c *AccessConfig) Region() string {
return common.ChooseString(c.RawRegion, os.Getenv("SDK_REGION"), os.Getenv("OS_REGION_NAME"))
osClient *gophercloud.ProviderClient
}
func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
errs := make([]error, 0)
if strings.HasPrefix(c.Provider, "rackspace") {
if c.Region() == "" {
errs = append(errs, fmt.Errorf("region must be specified when using rackspace"))
if c.EndpointType != "internal" && c.EndpointType != "internalURL" &&
c.EndpointType != "admin" && c.EndpointType != "adminURL" &&
c.EndpointType != "public" && c.EndpointType != "publicURL" &&
c.EndpointType != "" {
return []error{fmt.Errorf("Invalid endpoint type provided")}
}
if c.Region == "" {
c.Region = os.Getenv("OS_REGION_NAME")
}
// Legacy RackSpace stuff. We're keeping this around to keep things BC.
if c.APIKey == "" {
c.APIKey = os.Getenv("SDK_API_KEY")
}
if c.Password == "" {
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")
}
// Get as much as possible from the end
ao, _ := openstack.AuthOptionsFromEnv()
// Override values if we have them in our config
overrides := []struct {
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
}
}
if len(errs) > 0 {
return errs
// Build the client itself
client, err := openstack.NewClient(ao.IdentityEndpoint)
if err != nil {
return []error{err}
}
// If we have insecure set, then create a custom HTTP client that
// ignores SSL errors.
if c.Insecure {
config := &tls.Config{InsecureSkipVerify: true}
transport := &http.Transport{TLSClientConfig: config}
client.HTTPClient.Transport = transport
}
// Auth
err = openstack.Authenticate(client, ao)
if err != nil {
return []error{err}
}
c.osClient = client
return nil
}
func (c *AccessConfig) computeV2Client() (*gophercloud.ServiceClient, error) {
return openstack.NewComputeV2(c.osClient, gophercloud.EndpointOpts{
Region: c.Region,
Availability: c.getEndpointType(),
})
}
func (c *AccessConfig) getEndpointType() gophercloud.Availability {
if c.EndpointType == "internal" || c.EndpointType == "internalURL" {
return gophercloud.AvailabilityInternal
}
if c.EndpointType == "admin" || c.EndpointType == "adminURL" {
return gophercloud.AvailabilityAdmin
}
return gophercloud.AvailabilityPublic
}

View File

@ -1,37 +0,0 @@
package openstack
import (
"testing"
)
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 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())
}
}

View File

@ -4,7 +4,8 @@ import (
"fmt"
"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.
@ -16,7 +17,7 @@ type Artifact struct {
BuilderIdValue string
// OpenStack connection for performing API stuff.
Conn gophercloud.CloudServersProvider
Client *gophercloud.ServiceClient
}
func (a *Artifact) BuilderId() string {
@ -42,5 +43,5 @@ func (a *Artifact) State(name string) interface{} {
func (a *Artifact) Destroy() error {
log.Printf("Destroying image: %s", a.ImageId)
return a.Conn.DeleteImageById(a.ImageId)
return images.Delete(a.Client, a.ImageId).ExtractErr()
}

View File

@ -5,11 +5,11 @@ package openstack
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"log"
"github.com/mitchellh/gophercloud-fork-40444fb"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"
@ -20,6 +20,7 @@ const BuilderId = "mitchellh.openstack"
type Config struct {
common.PackerConfig `mapstructure:",squash"`
AccessConfig `mapstructure:",squash"`
ImageConfig `mapstructure:",squash"`
RunConfig `mapstructure:",squash"`
@ -55,43 +56,35 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, 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 {
return nil, 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
return nil, fmt.Errorf("Error initializing compute client: %s", err)
}
// Setup the state bag and initial state for the steps
state := new(multistep.BasicStateBag)
state.Put("config", b.config)
state.Put("csp", csp)
state.Put("hook", hook)
state.Put("ui", ui)
// Build the steps
steps := []multistep.Step{
&StepLoadExtensions{},
&StepLoadFlavor{
Flavor: b.config.Flavor,
},
&StepKeyPair{
Debug: b.config.PackerDebug,
DebugKeyPath: fmt.Sprintf("os_%s.pem", b.config.PackerBuildName),
},
&StepRunSourceServer{
Name: b.config.ImageName,
Flavor: b.config.Flavor,
SourceImage: b.config.SourceImage,
SecurityGroups: b.config.SecurityGroups,
Networks: b.config.Networks,
AvailabilityZone: b.config.AvailabilityZone,
UserData: b.config.UserData,
UserDataFile: b.config.UserDataFile,
},
&StepWaitForRackConnect{
Wait: b.config.RackconnectWait,
@ -100,12 +93,15 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
FloatingIpPool: b.config.FloatingIpPool,
FloatingIp: b.config.FloatingIp,
},
&common.StepConnectSSH{
SSHAddress: SSHAddress(csp, b.config.SSHInterface, b.config.SSHPort),
SSHConfig: SSHConfig(b.config.SSHUsername),
SSHWaitTimeout: b.config.SSHTimeout(),
&communicator.StepConnect{
Config: &b.config.RunConfig.Comm,
Host: CommHost(
computeClient,
b.config.SSHInterface),
SSHConfig: SSHConfig(b.config.RunConfig.Comm.SSHUsername),
},
&common.StepProvision{},
&StepStopServer{},
&stepCreateImage{},
}
@ -135,7 +131,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
artifact := &Artifact{
ImageId: state.Get("image").(string),
BuilderIdValue: BuilderId,
Conn: csp,
Client: computeClient,
}
return artifact, nil

View File

@ -9,7 +9,6 @@ func testConfig() map[string]interface{} {
return map[string]interface{}{
"username": "foo",
"password": "bar",
"provider": "foo",
"region": "DFW",
"image_name": "foo",
"source_image": "foo",
@ -40,55 +39,3 @@ func TestBuilder_Prepare_BadType(t *testing.T) {
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")
}
}

View File

@ -2,45 +2,37 @@ package openstack
import (
"errors"
"fmt"
"time"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/template/interpolate"
)
// RunConfig contains configuration for running an instance from a source
// image and details on how to access that launched image.
type RunConfig struct {
Comm communicator.Config `mapstructure:",squash"`
SSHInterface string `mapstructure:"ssh_interface"`
SourceImage string `mapstructure:"source_image"`
Flavor string `mapstructure:"flavor"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPort int `mapstructure:"ssh_port"`
SSHInterface string `mapstructure:"ssh_interface"`
OpenstackProvider string `mapstructure:"openstack_provider"`
UseFloatingIp bool `mapstructure:"use_floating_ip"`
AvailabilityZone string `mapstructure:"availability_zone"`
RackconnectWait bool `mapstructure:"rackconnect_wait"`
FloatingIpPool string `mapstructure:"floating_ip_pool"`
FloatingIp string `mapstructure:"floating_ip"`
SecurityGroups []string `mapstructure:"security_groups"`
Networks []string `mapstructure:"networks"`
UserData string `mapstructure:"user_data"`
UserDataFile string `mapstructure:"user_data_file"`
// Unexported fields that are calculated from others
sshTimeout time.Duration
// Not really used, but here for BC
OpenstackProvider string `mapstructure:"openstack_provider"`
UseFloatingIp bool `mapstructure:"use_floating_ip"`
}
func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
// Defaults
if c.SSHUsername == "" {
c.SSHUsername = "root"
}
if c.SSHPort == 0 {
c.SSHPort = 22
}
if c.RawSSHTimeout == "" {
c.RawSSHTimeout = "5m"
if c.Comm.SSHUsername == "" {
c.Comm.SSHUsername = "root"
}
if c.UseFloatingIp && c.FloatingIpPool == "" {
@ -48,8 +40,7 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
}
// Validation
var err error
errs := make([]error, 0)
errs := c.Comm.Prepare(ctx)
if c.SourceImage == "" {
errs = append(errs, errors.New("A source_image must be specified"))
}
@ -58,18 +49,5 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
errs = append(errs, errors.New("A flavor must be specified"))
}
if c.SSHUsername == "" {
errs = append(errs, errors.New("An ssh_username must be specified"))
}
c.sshTimeout, err = time.ParseDuration(c.RawSSHTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
}
return errs
}
func (c *RunConfig) SSHTimeout() time.Duration {
return c.sshTimeout
}

View File

@ -3,6 +3,8 @@ package openstack
import (
"os"
"testing"
"github.com/mitchellh/packer/helper/communicator"
)
func init() {
@ -17,7 +19,10 @@ func testRunConfig() *RunConfig {
return &RunConfig{
SourceImage: "abcd",
Flavor: "m1.small",
SSHUsername: "root",
Comm: communicator.Config{
SSHUsername: "foo",
},
}
}
@ -47,41 +52,28 @@ func TestRunConfigPrepare_SourceImage(t *testing.T) {
func TestRunConfigPrepare_SSHPort(t *testing.T) {
c := testRunConfig()
c.SSHPort = 0
c.Comm.SSHPort = 0
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
if c.SSHPort != 22 {
t.Fatalf("invalid value: %d", c.SSHPort)
if c.Comm.SSHPort != 22 {
t.Fatalf("invalid value: %d", c.Comm.SSHPort)
}
c.SSHPort = 44
c.Comm.SSHPort = 44
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
if c.SSHPort != 44 {
t.Fatalf("invalid value: %d", c.SSHPort)
}
}
func TestRunConfigPrepare_SSHTimeout(t *testing.T) {
c := testRunConfig()
c.RawSSHTimeout = ""
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
c.RawSSHTimeout = "bad"
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
if c.Comm.SSHPort != 44 {
t.Fatalf("invalid value: %d", c.Comm.SSHPort)
}
}
func TestRunConfigPrepare_SSHUsername(t *testing.T) {
c := testRunConfig()
c.SSHUsername = ""
c.Comm.SSHUsername = ""
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}

View File

@ -3,12 +3,12 @@ package openstack
import (
"errors"
"fmt"
"github.com/mitchellh/multistep"
"github.com/racker/perigee"
"log"
"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
@ -28,26 +28,27 @@ type StateChangeConf struct {
Pending []string
Refresh StateRefreshFunc
StepState multistep.StateBag
Target string
Target []string
}
// ServerStateRefreshFunc returns a StateRefreshFunc that is used to watch
// 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) {
resp, err := csp.ServerById(s.Id)
serverNew, err := servers.Get(client, s.ID).Extract()
if err != nil {
urce, ok := err.(*perigee.UnexpectedResponseCodeError)
if ok && (urce.Actual == 404) {
log.Printf("404 on ServerStateRefresh, returning DELETED")
errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError)
if ok && errCode.Actual == 404 {
log.Printf("[INFO] 404 on ServerStateRefresh, returning DELETED")
return nil, "DELETED", 0, nil
} else {
log.Printf("Error on ServerStateRefresh: %s", err)
log.Printf("[ERROR] Error on ServerStateRefresh: %s", err)
return nil, "", 0, err
}
}
return resp, resp.Status, resp.Progress, nil
return serverNew, serverNew.Status, serverNew.Progress, nil
}
}
@ -64,9 +65,11 @@ func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
return
}
if currentState == conf.Target {
for _, t := range conf.Target {
if currentState == t {
return
}
}
if conf.StepState != nil {
if _, ok := conf.StepState.GetOk(multistep.StateCancelled); ok {

View File

@ -1,51 +1,53 @@
package openstack
import (
"golang.org/x/crypto/ssh"
"errors"
"fmt"
"github.com/mitchellh/multistep"
"log"
"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
// 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) {
// CommHost looks up the host for the communicator.
func CommHost(
client *gophercloud.ServiceClient,
sshinterface string) func(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 != "" {
return fmt.Sprintf("%s:%d", ip.Ip, port), nil
}
ip_pools, err := s.AllAddressPools()
if err != nil {
return "", errors.New("Error parsing SSH addresses")
}
for pool, addresses := range ip_pools {
// If we have a specific interface, try that
if sshinterface != "" {
if pool != sshinterface {
continue
}
}
if pool != "" {
for _, address := range addresses {
if address.Addr != "" && address.Version == 4 {
return fmt.Sprintf("%s:%d", address.Addr, port), nil
}
}
if addr := sshAddrFromPool(s, sshinterface); addr != "" {
return addr, nil
}
}
serverState, err := csp.ServerById(s.Id)
// If we have a floating IP, use that
ip := state.Get("access_ip").(*floatingip.FloatingIP)
if ip != nil && ip.IP != "" {
return ip.IP, nil
}
if s.AccessIPv4 != "" {
return s.AccessIPv4, nil
}
// Try to get it from the requested interface
if addr := sshAddrFromPool(s, sshinterface); addr != "" {
return addr, nil
}
s, err := servers.Get(client, s.ID).Extract()
if err != nil {
return "", err
}
state.Put("server", serverState)
state.Put("server", s)
time.Sleep(1 * time.Second)
return "", errors.New("couldn't determine IP address for server")
@ -72,3 +74,42 @@ func SSHConfig(username string) func(multistep.StateBag) (*ssh.ClientConfig, err
}, nil
}
}
func sshAddrFromPool(s *servers.Server, desired string) string {
// Get all the addresses associated with this server. This
// was taken directly from Terraform.
for pool, networkAddresses := range s.Addresses {
// If we have an SSH interface specified, skip it if no match
if desired != "" && pool != desired {
log.Printf(
"[INFO] Skipping pool %s, doesn't match requested %s",
pool, desired)
continue
}
elements, ok := networkAddresses.([]interface{})
if !ok {
log.Printf(
"[ERROR] Unknown return type for address field: %#v",
networkAddresses)
continue
}
for _, element := range elements {
var addr string
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 addr
}
}
}
return ""
}

View File

@ -2,10 +2,11 @@ package openstack
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/gophercloud-fork-40444fb"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
)
type StepAllocateIp struct {
@ -15,53 +16,83 @@ type StepAllocateIp struct {
func (s *StepAllocateIp) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
csp := state.Get("csp").(gophercloud.CloudServersProvider)
server := state.Get("server").(*gophercloud.Server)
config := state.Get("config").(Config)
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
// statebag below, because it is requested by Cleanup()
state.Put("access_ip", instanceIp)
state.Put("access_ip", &instanceIp)
if s.FloatingIp != "" {
instanceIp.Ip = s.FloatingIp
instanceIp.IP = s.FloatingIp
} 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 {
err := fmt.Errorf("Error creating floating ip from pool '%s'", s.FloatingIpPool)
state.Put("error", err)
ui.Error(err.Error())
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 err := csp.AssociateFloatingIp(server.Id, instanceIp); err != nil {
err := fmt.Errorf("Error associating floating IP %s with instance.", instanceIp.Ip)
if instanceIp.IP != "" {
ui.Say(fmt.Sprintf("Associating floating IP with server..."))
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)
ui.Error(err.Error())
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
}
func (s *StepAllocateIp) Cleanup(state multistep.StateBag) {
config := state.Get("config").(Config)
ui := state.Get("ui").(packer.Ui)
csp := state.Get("csp").(gophercloud.CloudServersProvider)
instanceIp := state.Get("access_ip").(gophercloud.FloatingIp)
if s.FloatingIpPool != "" && instanceIp.Id != 0 {
if err := csp.DeleteFloatingIp(instanceIp); err != nil {
ui.Error(fmt.Sprintf("Error deleting temporary floating IP %s", instanceIp.Ip))
instanceIp := state.Get("access_ip").(*floatingip.FloatingIP)
// We need the v2 compute client
client, err := config.computeV2Client()
if err != nil {
ui.Error(fmt.Sprintf(
"Error deleting temporary floating IP %s", instanceIp.IP))
return
}
ui.Say(fmt.Sprintf("Deleted temporary floating IP %s", instanceIp.Ip))
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
}
ui.Say(fmt.Sprintf("Deleted temporary floating IP %s", instanceIp.IP))
}
}

View File

@ -2,28 +2,36 @@ package openstack
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"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{}
func (s *stepCreateImage) Run(state multistep.StateBag) multistep.StepAction {
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)
// 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
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,
}
imageId, err := csp.CreateImage(server.Id, createOpts)
}).ExtractImageID()
if err != nil {
err := fmt.Errorf("Error creating image: %s", err)
state.Put("error", err)
@ -32,12 +40,12 @@ func (s *stepCreateImage) Run(state multistep.StateBag) multistep.StepAction {
}
// 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)
// Wait for the 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)
state.Put("error", err)
ui.Error(err.Error())
@ -52,10 +60,17 @@ func (s *stepCreateImage) Cleanup(multistep.StateBag) {
}
// 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 {
image, err := csp.ImageById(imageId)
image, err := images.Get(client, imageId).Extract()
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
}

View File

@ -2,14 +2,13 @@ package openstack
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/packer"
"log"
"os"
"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 {
@ -19,18 +18,28 @@ type StepKeyPair struct {
}
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)
// 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...")
keyName := fmt.Sprintf("packer %s", uuid.TimeOrderedUUID())
log.Printf("temporary keypair name: %s", keyName)
keyResp, err := csp.CreateKeyPair(gophercloud.NewKeyPair{Name: keyName})
keypair, err := keypairs.Create(computeClient, keypairs.CreateOpts{
Name: keyName,
}).Extract()
if err != nil {
state.Put("error", fmt.Errorf("Error creating temporary keypair: %s", err))
return multistep.ActionHalt
}
if keyResp.PrivateKey == "" {
if keypair.PrivateKey == "" {
state.Put("error", fmt.Errorf("The temporary keypair returned was blank"))
return multistep.ActionHalt
}
@ -47,7 +56,7 @@ func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction {
defer f.Close()
// 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))
return multistep.ActionHalt
}
@ -66,7 +75,7 @@ func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction {
// Set some state data for use in future steps
state.Put("keyPair", keyName)
state.Put("privateKey", keyResp.PrivateKey)
state.Put("privateKey", keypair.PrivateKey)
return multistep.ActionContinue
}
@ -77,11 +86,19 @@ func (s *StepKeyPair) Cleanup(state multistep.StateBag) {
return
}
csp := state.Get("csp").(gophercloud.CloudServersProvider)
config := state.Get("config").(Config)
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...")
err := csp.DeleteKeyPair(s.keyName)
err = keypairs.Delete(computeClient, s.keyName).ExtractErr()
if err != nil {
ui.Error(fmt.Sprintf(
"Error cleaning up keypair. Please delete the key manually: %s", s.keyName))

View File

@ -0,0 +1,58 @@
package openstack
import (
"fmt"
"log"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions"
"github.com/rackspace/gophercloud/pagination"
)
// StepLoadExtensions 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 StepLoadExtensions struct{}
func (s *StepLoadExtensions) 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("Discovering enabled extensions...")
result := make(map[string]struct{}, 15)
pager := extensions.List(client)
err = pager.EachPage(func(p pagination.Page) (bool, error) {
// Extract the extensions from this page
exts, err := extensions.ExtractExtensions(p)
if err != nil {
return false, err
}
for _, ext := range exts {
log.Printf("[DEBUG] Discovered extension: %s", ext.Alias)
result[ext.Alias] = struct{}{}
}
return true, nil
})
if err != nil {
err = fmt.Errorf("Error loading extensions: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
state.Put("extensions", result)
return multistep.ActionContinue
}
func (s *StepLoadExtensions) Cleanup(state multistep.StateBag) {
}

View File

@ -0,0 +1,61 @@
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) {
}

View File

@ -2,51 +2,70 @@ package openstack
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"io/ioutil"
"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 {
Flavor string
Name string
SourceImage string
SecurityGroups []string
Networks []string
AvailabilityZone string
UserData string
UserDataFile string
server *gophercloud.Server
server *servers.Server
}
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)
ui := state.Get("ui").(packer.Ui)
// XXX - validate image and flavor is available
securityGroups := make([]map[string]interface{}, len(s.SecurityGroups))
for i, groupName := range s.SecurityGroups {
securityGroups[i] = make(map[string]interface{})
securityGroups[i]["name"] = groupName
// 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
}
networks := make([]gophercloud.NetworkConfig, len(s.Networks))
networks := make([]servers.Network, len(s.Networks))
for i, networkUuid := range s.Networks {
networks[i].Uuid = networkUuid
networks[i].UUID = networkUuid
}
server := gophercloud.NewServer{
userData := []byte(s.UserData)
if s.UserDataFile != "" {
userData, err = ioutil.ReadFile(s.UserDataFile)
if err != nil {
err = fmt.Errorf("Error reading user data file: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
}
ui.Say("Launching server...")
s.server, err = servers.Create(computeClient, keypairs.CreateOptsExt{
CreateOptsBuilder: servers.CreateOpts{
Name: s.Name,
ImageRef: s.SourceImage,
FlavorRef: s.Flavor,
KeyPairName: keyName,
SecurityGroup: securityGroups,
FlavorRef: flavor,
SecurityGroups: s.SecurityGroups,
Networks: networks,
}
AvailabilityZone: s.AvailabilityZone,
UserData: userData,
},
serverResp, err := csp.CreateServer(server)
KeyName: keyName,
}).Extract()
if err != nil {
err := fmt.Errorf("Error launching source server: %s", err)
state.Put("error", err)
@ -54,25 +73,25 @@ func (s *StepRunSourceServer) Run(state multistep.StateBag) multistep.StepAction
return multistep.ActionHalt
}
s.server, err = csp.ServerById(serverResp.Id)
log.Printf("server id: %s", s.server.Id)
ui.Message(fmt.Sprintf("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{
Pending: []string{"BUILD"},
Target: "ACTIVE",
Refresh: ServerStateRefreshFunc(csp, s.server),
Target: []string{"ACTIVE"},
Refresh: ServerStateRefreshFunc(computeClient, s.server),
StepState: state,
}
latestServer, err := WaitForState(&stateChange)
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)
ui.Error(err.Error())
return multistep.ActionHalt
}
s.server = latestServer.(*gophercloud.Server)
s.server = latestServer.(*servers.Server)
state.Put("server", s.server)
return multistep.ActionContinue
@ -83,19 +102,26 @@ func (s *StepRunSourceServer) Cleanup(state multistep.StateBag) {
return
}
csp := state.Get("csp").(gophercloud.CloudServersProvider)
config := state.Get("config").(Config)
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...")
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))
return
}
stateChange := StateChangeConf{
Pending: []string{"ACTIVE", "BUILD", "REBUILD", "SUSPENDED"},
Refresh: ServerStateRefreshFunc(csp, s.server),
Target: "DELETED",
Pending: []string{"ACTIVE", "BUILD", "REBUILD", "SUSPENDED", "SHUTOFF", "STOPPED"},
Refresh: ServerStateRefreshFunc(computeClient, s.server),
Target: []string{"DELETED"},
}
WaitForState(&stateChange)

View File

@ -0,0 +1,58 @@
package openstack
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
)
type StepStopServer struct{}
func (s *StepStopServer) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
config := state.Get("config").(Config)
extensions := state.Get("extensions").(map[string]struct{})
server := state.Get("server").(*servers.Server)
// Verify we have the extension
if _, ok := extensions["os-server-start-stop"]; !ok {
ui.Say("OpenStack cluster doesn't support stop, skipping...")
return multistep.ActionContinue
}
// 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("Stopping server...")
if err := startstop.Stop(client, server.ID).ExtractErr(); err != nil {
err = fmt.Errorf("Error stopping server: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
ui.Message("Waiting for server to stop...")
stateChange := StateChangeConf{
Pending: []string{"ACTIVE"},
Target: []string{"SHUTOFF", "STOPPED"},
Refresh: ServerStateRefreshFunc(client, server),
StepState: state,
}
if _, err := WaitForState(&stateChange); err != nil {
err := fmt.Errorf("Error waiting for server (%s) to stop: %s", server.ID, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *StepStopServer) Cleanup(state multistep.StateBag) {}

View File

@ -2,11 +2,11 @@ package openstack
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"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 {
@ -18,14 +18,22 @@ func (s *StepWaitForRackConnect) Run(state multistep.StateBag) multistep.StepAct
return multistep.ActionContinue
}
csp := state.Get("csp").(gophercloud.CloudServersProvider)
server := state.Get("server").(*gophercloud.Server)
config := state.Get("config").(Config)
server := state.Get("server").(*servers.Server)
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 {
server, err := csp.ServerById(server.Id)
server, err = servers.Get(computeClient, server.ID).Extract()
if err != nil {
return multistep.ActionHalt
}

View File

@ -73,6 +73,12 @@ func NewDriver() (Driver, error) {
log.Printf("prlctl path: %s", prlctlPath)
drivers = map[string]Driver{
"11": &Parallels10Driver{
Parallels9Driver: Parallels9Driver{
PrlctlPath: prlctlPath,
dhcp_lease_file: dhcp_lease_file,
},
},
"10": &Parallels10Driver{
Parallels9Driver: Parallels9Driver{
PrlctlPath: prlctlPath,

View File

@ -1,6 +1,7 @@
package common
// Parallels10Driver are inherited from Parallels9Driver.
// Used for Parallels v 10 & 11
type Parallels10Driver struct {
Parallels9Driver
}

View File

@ -1,15 +1,13 @@
package common
import (
"fmt"
"golang.org/x/crypto/ssh"
"github.com/mitchellh/multistep"
commonssh "github.com/mitchellh/packer/common/ssh"
packerssh "github.com/mitchellh/packer/communicator/ssh"
"golang.org/x/crypto/ssh"
)
func SSHAddress(state multistep.StateBag) (string, error) {
func CommHost(state multistep.StateBag) (string, error) {
vmName := state.Get("vmName").(string)
driver := state.Get("driver").(Driver)
@ -23,19 +21,19 @@ func SSHAddress(state multistep.StateBag) (string, error) {
return "", err
}
return fmt.Sprintf("%s:22", ip), nil
return ip, nil
}
func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*ssh.ClientConfig, error) {
return func(state multistep.StateBag) (*ssh.ClientConfig, error) {
auth := []ssh.AuthMethod{
ssh.Password(config.SSHPassword),
ssh.Password(config.Comm.SSHPassword),
ssh.KeyboardInteractive(
packerssh.PasswordKeyboardInteractive(config.SSHPassword)),
packerssh.PasswordKeyboardInteractive(config.Comm.SSHPassword)),
}
if config.SSHKeyPath != "" {
signer, err := commonssh.FileSigner(config.SSHKeyPath)
signer, err := commonssh.FileSigner(config.Comm.SSHPrivateKey)
if err != nil {
return nil, err
}
@ -44,7 +42,7 @@ func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*ssh.ClientConfig
}
return &ssh.ClientConfig{
User: config.SSHUser,
User: config.Comm.SSHUsername,
Auth: auth,
}, nil
}

View File

@ -1,52 +1,29 @@
package common
import (
"errors"
"fmt"
"os"
"time"
commonssh "github.com/mitchellh/packer/common/ssh"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/template/interpolate"
)
type SSHConfig struct {
SSHKeyPath string `mapstructure:"ssh_key_path"`
SSHPassword string `mapstructure:"ssh_password"`
SSHPort uint `mapstructure:"ssh_port"`
SSHUser string `mapstructure:"ssh_username"`
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
Comm communicator.Config `mapstructure:",squash"`
SSHWaitTimeout time.Duration
// These are deprecated, but we keep them around for BC
// TODO(@mitchellh): remove
SSHKeyPath string `mapstructure:"ssh_key_path"`
SSHWaitTimeout time.Duration `mapstructure:"ssh_wait_timeout"`
}
func (c *SSHConfig) Prepare(ctx *interpolate.Context) []error {
if c.SSHPort == 0 {
c.SSHPort = 22
}
if c.RawSSHWaitTimeout == "" {
c.RawSSHWaitTimeout = "20m"
}
var errs []error
// TODO: backwards compatibility, write fixer instead
if c.SSHKeyPath != "" {
if _, err := os.Stat(c.SSHKeyPath); err != nil {
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
} else if _, err := commonssh.FileSigner(c.SSHKeyPath); err != nil {
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
c.Comm.SSHPrivateKey = c.SSHKeyPath
}
if c.SSHWaitTimeout != 0 {
c.Comm.SSHTimeout = c.SSHWaitTimeout
}
if c.SSHUser == "" {
errs = append(errs, errors.New("An ssh_username must be specified."))
}
var err error
c.SSHWaitTimeout, err = time.ParseDuration(c.RawSSHWaitTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
}
return errs
return c.Comm.Prepare(ctx)
}

View File

@ -4,11 +4,15 @@ import (
"io/ioutil"
"os"
"testing"
"github.com/mitchellh/packer/helper/communicator"
)
func testSSHConfig() *SSHConfig {
return &SSHConfig{
SSHUser: "foo",
Comm: communicator.Config{
SSHUsername: "foo",
},
}
}
@ -19,8 +23,8 @@ func TestSSHConfigPrepare(t *testing.T) {
t.Fatalf("err: %#v", errs)
}
if c.SSHPort != 22 {
t.Errorf("bad ssh port: %d", c.SSHPort)
if c.Comm.SSHPort != 22 {
t.Errorf("bad ssh port: %d", c.Comm.SSHPort)
}
}
@ -78,46 +82,14 @@ func TestSSHConfigPrepare_SSHUser(t *testing.T) {
var errs []error
c = testSSHConfig()
c.SSHUser = ""
c.Comm.SSHUsername = ""
errs = c.Prepare(testConfigTemplate(t))
if len(errs) == 0 {
t.Fatalf("should have error")
}
c = testSSHConfig()
c.SSHUser = "exists"
errs = c.Prepare(testConfigTemplate(t))
if len(errs) > 0 {
t.Fatalf("should not have error: %#v", errs)
}
}
func TestSSHConfigPrepare_SSHWaitTimeout(t *testing.T) {
var c *SSHConfig
var errs []error
// Defaults
c = testSSHConfig()
c.RawSSHWaitTimeout = ""
errs = c.Prepare(testConfigTemplate(t))
if len(errs) > 0 {
t.Fatalf("should not have error: %#v", errs)
}
if c.RawSSHWaitTimeout != "20m" {
t.Fatalf("bad value: %s", c.RawSSHWaitTimeout)
}
// Test with a bad value
c = testSSHConfig()
c.RawSSHWaitTimeout = "this is not good"
errs = c.Prepare(testConfigTemplate(t))
if len(errs) == 0 {
t.Fatal("should have error")
}
// Test with a good one
c = testSSHConfig()
c.RawSSHWaitTimeout = "5s"
c.Comm.SSHUsername = "exists"
errs = c.Prepare(testConfigTemplate(t))
if len(errs) > 0 {
t.Fatalf("should not have error: %#v", errs)

View File

@ -9,6 +9,7 @@ import (
"github.com/mitchellh/multistep"
parallelscommon "github.com/mitchellh/packer/builder/parallels/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"
@ -245,10 +246,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
VMName: b.config.VMName,
Ctx: b.config.ctx,
},
&common.StepConnectSSH{
SSHAddress: parallelscommon.SSHAddress,
&communicator.StepConnect{
Config: &b.config.SSHConfig.Comm,
Host: parallelscommon.CommHost,
SSHConfig: parallelscommon.SSHConfigFunc(b.config.SSHConfig),
SSHWaitTimeout: b.config.SSHWaitTimeout,
},
&parallelscommon.StepUploadVersion{
Path: b.config.PrlctlVersionFile,

View File

@ -3,11 +3,13 @@ package pvm
import (
"errors"
"fmt"
"log"
"github.com/mitchellh/multistep"
parallelscommon "github.com/mitchellh/packer/builder/parallels/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer"
"log"
)
// Builder implements packer.Builder and builds the actual Parallels
@ -80,10 +82,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
VMName: b.config.VMName,
Ctx: b.config.ctx,
},
&common.StepConnectSSH{
SSHAddress: parallelscommon.SSHAddress,
&communicator.StepConnect{
Config: &b.config.SSHConfig.Comm,
Host: parallelscommon.CommHost,
SSHConfig: parallelscommon.SSHConfigFunc(b.config.SSHConfig),
SSHWaitTimeout: b.config.SSHWaitTimeout,
},
&parallelscommon.StepUploadVersion{
Path: b.config.PrlctlVersionFile,

View File

@ -33,7 +33,7 @@ type Config struct {
func NewConfig(raws ...interface{}) (*Config, []string, error) {
c := new(Config)
err := config.Decode(&c, &config.DecodeOpts{
err := config.Decode(c, &config.DecodeOpts{
Interpolate: true,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{

View File

@ -12,7 +12,7 @@ import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
commonssh "github.com/mitchellh/packer/common/ssh"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"
@ -78,6 +78,7 @@ type Builder struct {
type Config struct {
common.PackerConfig `mapstructure:",squash"`
Comm communicator.Config `mapstructure:",squash"`
Accelerator string `mapstructure:"accelerator"`
BootCommand []string `mapstructure:"boot_command"`
@ -103,25 +104,24 @@ type Config struct {
ShutdownCommand string `mapstructure:"shutdown_command"`
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
SSHPassword string `mapstructure:"ssh_password"`
SSHPort uint `mapstructure:"ssh_port"`
SSHUser string `mapstructure:"ssh_username"`
SSHKeyPath string `mapstructure:"ssh_key_path"`
VNCPortMin uint `mapstructure:"vnc_port_min"`
VNCPortMax uint `mapstructure:"vnc_port_max"`
VMName string `mapstructure:"vm_name"`
// These are deprecated, but we keep them around for BC
// TODO(@mitchellh): remove
SSHKeyPath string `mapstructure:"ssh_key_path"`
SSHWaitTimeout time.Duration `mapstructure:"ssh_wait_timeout"`
// TODO(mitchellh): deprecate
RunOnce bool `mapstructure:"run_once"`
RawBootWait string `mapstructure:"boot_wait"`
RawSingleISOUrl string `mapstructure:"iso_url"`
RawShutdownTimeout string `mapstructure:"shutdown_timeout"`
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
bootWait time.Duration ``
shutdownTimeout time.Duration ``
sshWaitTimeout time.Duration ``
ctx interpolate.Context
}
@ -139,9 +139,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
return nil, err
}
var errs *packer.MultiError
warnings := make([]string, 0)
if b.config.DiskSize == 0 {
b.config.DiskSize = 40000
}
@ -190,10 +187,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
b.config.SSHHostPortMax = 4444
}
if b.config.SSHPort == 0 {
b.config.SSHPort = 22
}
if b.config.VNCPortMin == 0 {
b.config.VNCPortMin = 5900
}
@ -222,6 +215,21 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
b.config.DiskInterface = "virtio"
}
// TODO: backwards compatibility, write fixer instead
if b.config.SSHKeyPath != "" {
b.config.Comm.SSHPrivateKey = b.config.SSHKeyPath
}
if b.config.SSHWaitTimeout != 0 {
b.config.Comm.SSHTimeout = b.config.SSHWaitTimeout
}
var errs *packer.MultiError
warnings := make([]string, 0)
if es := b.config.Comm.Prepare(&b.config.ctx); len(es) > 0 {
errs = packer.MultiErrorAppend(errs, es...)
}
if !(b.config.Format == "qcow2" || b.config.Format == "raw") {
errs = packer.MultiErrorAppend(
errs, errors.New("invalid format, only 'qcow2' or 'raw' are allowed"))
@ -314,42 +322,17 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
b.config.RawShutdownTimeout = "5m"
}
if b.config.RawSSHWaitTimeout == "" {
b.config.RawSSHWaitTimeout = "20m"
}
b.config.shutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
}
if b.config.SSHKeyPath != "" {
if _, err := os.Stat(b.config.SSHKeyPath); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
} else if _, err := commonssh.FileSigner(b.config.SSHKeyPath); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
}
}
if b.config.SSHHostPortMin > b.config.SSHHostPortMax {
errs = packer.MultiErrorAppend(
errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
}
if b.config.SSHUser == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("An ssh_username must be specified."))
}
b.config.sshWaitTimeout, err = time.ParseDuration(b.config.RawSSHWaitTimeout)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
}
if b.config.VNCPortMin > b.config.VNCPortMax {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
@ -409,10 +392,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
steprun,
&stepBootWait{},
&stepTypeBootCommand{},
&common.StepConnectSSH{
SSHAddress: sshAddress,
&communicator.StepConnect{
Config: &b.config.Comm,
Host: commHost,
SSHConfig: sshConfig,
SSHWaitTimeout: b.config.sshWaitTimeout,
SSHPort: commPort,
},
new(common.StepProvision),
new(stepShutdown),

View File

@ -79,8 +79,8 @@ func TestBuilderPrepare_Defaults(t *testing.T) {
t.Errorf("bad max ssh host port: %d", b.config.SSHHostPortMax)
}
if b.config.SSHPort != 22 {
t.Errorf("bad ssh port: %d", b.config.SSHPort)
if b.config.Comm.SSHPort != 22 {
t.Errorf("bad ssh port: %d", b.config.Comm.SSHPort)
}
if b.config.VMName != "packer-foo" {
@ -595,10 +595,6 @@ func TestBuilderPrepare_SSHWaitTimeout(t *testing.T) {
t.Fatalf("err: %s", err)
}
if b.config.RawSSHWaitTimeout != "20m" {
t.Fatalf("bad value: %s", b.config.RawSSHWaitTimeout)
}
// Test with a bad value
config["ssh_wait_timeout"] = "this is not good"
b = Builder{}

View File

@ -1,30 +1,32 @@
package qemu
import (
"fmt"
gossh "golang.org/x/crypto/ssh"
"github.com/mitchellh/multistep"
commonssh "github.com/mitchellh/packer/common/ssh"
"github.com/mitchellh/packer/communicator/ssh"
gossh "golang.org/x/crypto/ssh"
)
func sshAddress(state multistep.StateBag) (string, error) {
func commHost(state multistep.StateBag) (string, error) {
return "127.0.0.1", nil
}
func commPort(state multistep.StateBag) (int, error) {
sshHostPort := state.Get("sshHostPort").(uint)
return fmt.Sprintf("127.0.0.1:%d", sshHostPort), nil
return int(sshHostPort), nil
}
func sshConfig(state multistep.StateBag) (*gossh.ClientConfig, error) {
config := state.Get("config").(*Config)
auth := []gossh.AuthMethod{
gossh.Password(config.SSHPassword),
gossh.Password(config.Comm.SSHPassword),
gossh.KeyboardInteractive(
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
ssh.PasswordKeyboardInteractive(config.Comm.SSHPassword)),
}
if config.SSHKeyPath != "" {
signer, err := commonssh.FileSigner(config.SSHKeyPath)
if config.Comm.SSHPrivateKey != "" {
signer, err := commonssh.FileSigner(config.Comm.SSHPrivateKey)
if err != nil {
return nil, err
}
@ -33,7 +35,7 @@ func sshConfig(state multistep.StateBag) (*gossh.ClientConfig, error) {
}
return &gossh.ClientConfig{
User: config.SSHUser,
User: config.Comm.SSHUsername,
Auth: auth,
}, nil
}

View File

@ -80,7 +80,8 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error
defaultArgs["-name"] = vmName
defaultArgs["-machine"] = fmt.Sprintf("type=%s", config.MachineType)
defaultArgs["-netdev"] = fmt.Sprintf("user,id=user.0,hostfwd=tcp::%v-:22", sshHostPort)
defaultArgs["-netdev"] = fmt.Sprintf(
"user,id=user.0,hostfwd=tcp::%v-:%d", sshHostPort, config.Comm.Port())
defaultArgs["-device"] = fmt.Sprintf("%s,netdev=user.0", config.NetDevice)
defaultArgs["-drive"] = fmt.Sprintf("file=%s,if=%s,cache=%s,discard=%s", imgPath, config.DiskInterface, config.DiskCache, config.DiskDiscard)
if !config.DiskImage {

View File

@ -2,7 +2,6 @@ package common
import (
"fmt"
"os"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/template/interpolate"
@ -17,13 +16,5 @@ func (c *OutputConfig) Prepare(ctx *interpolate.Context, pc *common.PackerConfig
c.OutputDir = fmt.Sprintf("output-%s", pc.PackerBuildName)
}
var errs []error
if !pc.PackerForce {
if _, err := os.Stat(c.OutputDir); err == nil {
errs = append(errs, fmt.Errorf(
"Output directory '%s' already exists. It must not exist.", c.OutputDir))
}
}
return errs
return nil
}

View File

@ -39,27 +39,7 @@ func TestOutputConfigPrepare_exists(t *testing.T) {
PackerForce: false,
}
errs := c.Prepare(testConfigTemplate(t), pc)
if len(errs) == 0 {
t.Fatal("should have errors")
}
}
func TestOutputConfigPrepare_forceExists(t *testing.T) {
td, err := ioutil.TempDir("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.RemoveAll(td)
c := new(OutputConfig)
c.OutputDir = td
pc := &common.PackerConfig{
PackerBuildName: "foo",
PackerForce: true,
}
errs := c.Prepare(testConfigTemplate(t), pc)
if len(errs) > 0 {
if len(errs) != 0 {
t.Fatal("should not have errors")
}
}

View File

@ -1,29 +1,31 @@
package common
import (
"fmt"
gossh "golang.org/x/crypto/ssh"
"github.com/mitchellh/multistep"
commonssh "github.com/mitchellh/packer/common/ssh"
"github.com/mitchellh/packer/communicator/ssh"
gossh "golang.org/x/crypto/ssh"
)
func SSHAddress(state multistep.StateBag) (string, error) {
func CommHost(state multistep.StateBag) (string, error) {
return "127.0.0.1", nil
}
func SSHPort(state multistep.StateBag) (int, error) {
sshHostPort := state.Get("sshHostPort").(uint)
return fmt.Sprintf("127.0.0.1:%d", sshHostPort), nil
return int(sshHostPort), nil
}
func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*gossh.ClientConfig, error) {
return func(state multistep.StateBag) (*gossh.ClientConfig, error) {
auth := []gossh.AuthMethod{
gossh.Password(config.SSHPassword),
gossh.Password(config.Comm.SSHPassword),
gossh.KeyboardInteractive(
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
ssh.PasswordKeyboardInteractive(config.Comm.SSHPassword)),
}
if config.SSHKeyPath != "" {
signer, err := commonssh.FileSigner(config.SSHKeyPath)
signer, err := commonssh.FileSigner(config.Comm.SSHPrivateKey)
if err != nil {
return nil, err
}
@ -32,7 +34,7 @@ func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*gossh.ClientConf
}
return &gossh.ClientConfig{
User: config.SSHUser,
User: config.Comm.SSHUsername,
Auth: auth,
}, nil
}

View File

@ -2,24 +2,23 @@ package common
import (
"errors"
"fmt"
"os"
"time"
commonssh "github.com/mitchellh/packer/common/ssh"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/template/interpolate"
)
type SSHConfig struct {
Comm communicator.Config `mapstructure:",squash"`
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
SSHKeyPath string `mapstructure:"ssh_key_path"`
SSHPassword string `mapstructure:"ssh_password"`
SSHPort uint `mapstructure:"ssh_port"`
SSHUser string `mapstructure:"ssh_username"`
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
SSHSkipNatMapping bool `mapstructure:"ssh_skip_nat_mapping"`
SSHWaitTimeout time.Duration
// These are deprecated, but we keep them around for BC
// TODO(@mitchellh): remove
SSHKeyPath string `mapstructure:"ssh_key_path"`
SSHWaitTimeout time.Duration `mapstructure:"ssh_wait_timeout"`
}
func (c *SSHConfig) Prepare(ctx *interpolate.Context) []error {
@ -31,37 +30,19 @@ func (c *SSHConfig) Prepare(ctx *interpolate.Context) []error {
c.SSHHostPortMax = 4444
}
if c.SSHPort == 0 {
c.SSHPort = 22
}
if c.RawSSHWaitTimeout == "" {
c.RawSSHWaitTimeout = "20m"
}
var errs []error
// TODO: backwards compatibility, write fixer instead
if c.SSHKeyPath != "" {
if _, err := os.Stat(c.SSHKeyPath); err != nil {
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
} else if _, err := commonssh.FileSigner(c.SSHKeyPath); err != nil {
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
c.Comm.SSHPrivateKey = c.SSHKeyPath
}
if c.SSHWaitTimeout != 0 {
c.Comm.SSHTimeout = c.SSHWaitTimeout
}
errs := c.Comm.Prepare(ctx)
if c.SSHHostPortMin > c.SSHHostPortMax {
errs = append(errs,
errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
}
if c.SSHUser == "" {
errs = append(errs, errors.New("An ssh_username must be specified."))
}
var err error
c.SSHWaitTimeout, err = time.ParseDuration(c.RawSSHWaitTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
}
return errs
}

View File

@ -4,11 +4,15 @@ import (
"io/ioutil"
"os"
"testing"
"github.com/mitchellh/packer/helper/communicator"
)
func testSSHConfig() *SSHConfig {
return &SSHConfig{
SSHUser: "foo",
Comm: communicator.Config{
SSHUsername: "foo",
},
}
}
@ -27,8 +31,8 @@ func TestSSHConfigPrepare(t *testing.T) {
t.Errorf("bad max ssh host port: %d", c.SSHHostPortMax)
}
if c.SSHPort != 22 {
t.Errorf("bad ssh port: %d", c.SSHPort)
if c.Comm.SSHPort != 22 {
t.Errorf("bad ssh port: %d", c.Comm.SSHPort)
}
}
@ -109,46 +113,14 @@ func TestSSHConfigPrepare_SSHUser(t *testing.T) {
var errs []error
c = testSSHConfig()
c.SSHUser = ""
c.Comm.SSHUsername = ""
errs = c.Prepare(testConfigTemplate(t))
if len(errs) == 0 {
t.Fatalf("should have error")
}
c = testSSHConfig()
c.SSHUser = "exists"
errs = c.Prepare(testConfigTemplate(t))
if len(errs) > 0 {
t.Fatalf("should not have error: %#v", errs)
}
}
func TestSSHConfigPrepare_SSHWaitTimeout(t *testing.T) {
var c *SSHConfig
var errs []error
// Defaults
c = testSSHConfig()
c.RawSSHWaitTimeout = ""
errs = c.Prepare(testConfigTemplate(t))
if len(errs) > 0 {
t.Fatalf("should not have error: %#v", errs)
}
if c.RawSSHWaitTimeout != "20m" {
t.Fatalf("bad value: %s", c.RawSSHWaitTimeout)
}
// Test with a bad value
c = testSSHConfig()
c.RawSSHWaitTimeout = "this is not good"
errs = c.Prepare(testConfigTemplate(t))
if len(errs) == 0 {
t.Fatal("should have error")
}
// Test with a good one
c = testSSHConfig()
c.RawSSHWaitTimeout = "5s"
c.Comm.SSHUsername = "exists"
errs = c.Prepare(testConfigTemplate(t))
if len(errs) > 0 {
t.Fatalf("should not have error: %#v", errs)

View File

@ -20,6 +20,7 @@ type StepExport struct {
Format string
OutputDir string
ExportOpts []string
SkipNatMapping bool
}
func (s *StepExport) Run(state multistep.StateBag) multistep.StepAction {
@ -30,9 +31,10 @@ func (s *StepExport) Run(state multistep.StateBag) multistep.StepAction {
// Wait a second to ensure VM is really shutdown.
log.Println("1 second timeout to ensure VM is really shutdown")
time.Sleep(1 * time.Second)
ui.Say("Preparing to export machine...")
// Clear out the Packer-created forwarding rule
ui.Say("Preparing to export machine...")
if !s.SkipNatMapping {
ui.Message(fmt.Sprintf(
"Deleting forwarded port mapping for SSH (host port %d)",
state.Get("sshHostPort")))
@ -43,17 +45,17 @@ func (s *StepExport) Run(state multistep.StateBag) multistep.StepAction {
ui.Error(err.Error())
return multistep.ActionHalt
}
}
// Export the VM to an OVF
outputPath := filepath.Join(s.OutputDir, vmName+"."+s.Format)
command = []string{
command := []string{
"export",
vmName,
"--output",
outputPath,
}
command = append(command, s.ExportOpts...)
ui.Say("Exporting virtual machine...")

View File

@ -2,11 +2,13 @@ package common
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"math/rand"
"net"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer"
)
// This step adds a NAT port forwarding definition so that SSH is available
@ -19,9 +21,10 @@ import (
//
// Produces:
type StepForwardSSH struct {
GuestPort uint
CommConfig *communicator.Config
HostPortMin uint
HostPortMax uint
SkipNatMapping bool
}
func (s *StepForwardSSH) Run(state multistep.StateBag) multistep.StepAction {
@ -29,19 +32,21 @@ func (s *StepForwardSSH) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
vmName := state.Get("vmName").(string)
guestPort := s.CommConfig.Port()
sshHostPort := guestPort
if !s.SkipNatMapping {
log.Printf("Looking for available SSH port between %d and %d",
s.HostPortMin, s.HostPortMax)
var sshHostPort uint
var offset uint = 0
offset := 0
portRange := int(s.HostPortMax - s.HostPortMin)
if portRange > 0 {
// Have to check if > 0 to avoid a panic
offset = uint(rand.Intn(portRange))
offset = rand.Intn(portRange)
}
for {
sshHostPort = offset + s.HostPortMin
sshHostPort = offset + int(s.HostPortMin)
log.Printf("Trying port: %d", sshHostPort)
l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", sshHostPort))
if err == nil {
@ -55,7 +60,7 @@ func (s *StepForwardSSH) Run(state multistep.StateBag) multistep.StepAction {
command := []string{
"modifyvm", vmName,
"--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, guestPort),
}
if err := driver.VBoxManage(command...); err != nil {
err := fmt.Errorf("Error creating port forwarding rule: %s", err)
@ -63,6 +68,7 @@ func (s *StepForwardSSH) Run(state multistep.StateBag) multistep.StepAction {
ui.Error(err.Error())
return multistep.ActionHalt
}
}
// Save the port we're using so that future steps can use it
state.Put("sshHostPort", sshHostPort)

View File

@ -49,7 +49,7 @@ func (s *StepHTTPServer) Run(state multistep.StateBag) multistep.StepAction {
}
httpPort = offset + s.HTTPPortMin
httpAddr = fmt.Sprintf(":%d", httpPort)
httpAddr = fmt.Sprintf("0.0.0.0:%d", httpPort)
log.Printf("Trying port: %d", httpPort)
s.l, err = net.Listen("tcp", httpAddr)
if err == nil {

View File

@ -22,7 +22,16 @@ type StepOutputDir struct {
func (s *StepOutputDir) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
if _, err := os.Stat(s.Path); err == nil && s.Force {
if _, err := os.Stat(s.Path); err == nil {
if !s.Force {
err := fmt.Errorf(
"Output directory exists: %s\n\n"+
"Use the force flag to delete it prior to building.",
s.Path)
state.Put("error", err)
return multistep.ActionHalt
}
ui.Say("Deleting previous output directory...")
os.RemoveAll(s.Path)
}

View File

@ -45,6 +45,30 @@ func TestStepOutputDir(t *testing.T) {
}
}
func TestStepOutputDir_exists(t *testing.T) {
state := testState(t)
step := testStepOutputDir(t)
// Make the dir
if err := os.MkdirAll(step.Path, 0755); err != nil {
t.Fatalf("bad: %s", err)
}
// Test the run
if action := step.Run(state); action != multistep.ActionHalt {
t.Fatalf("bad action: %#v", action)
}
if _, ok := state.GetOk("error"); !ok {
t.Fatal("should have error")
}
// Test the cleanup
step.Cleanup(state)
if _, err := os.Stat(step.Path); err != nil {
t.Fatalf("err: %s", err)
}
}
func TestStepOutputDir_cancelled(t *testing.T) {
state := testState(t)
step := testStepOutputDir(t)

View File

@ -38,6 +38,19 @@ func (s *StepRemoveDevices) Run(state multistep.StateBag) multistep.StepAction {
ui.Error(err.Error())
return multistep.ActionHalt
}
// Don't forget to remove the floppy controller as well
command = []string{
"storagectl", vmName,
"--name", "Floppy Controller",
"--remove",
}
if err := driver.VBoxManage(command...); err != nil {
err := fmt.Errorf("Error removing floppy controller: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
if _, ok := state.GetOk("attachedIso"); ok {

View File

@ -102,10 +102,13 @@ func TestStepRemoveDevices_floppyPath(t *testing.T) {
}
// Test that both were removed
if len(driver.VBoxManageCalls) != 1 {
if len(driver.VBoxManageCalls) != 2 {
t.Fatalf("bad: %#v", driver.VBoxManageCalls)
}
if driver.VBoxManageCalls[0][3] != "Floppy Controller" {
t.Fatalf("bad: %#v", driver.VBoxManageCalls)
}
if driver.VBoxManageCalls[1][3] != "Floppy Controller" {
t.Fatalf("bad: %#v", driver.VBoxManageCalls)
}
}

View File

@ -11,6 +11,7 @@ import (
"github.com/mitchellh/multistep"
vboxcommon "github.com/mitchellh/packer/builder/virtualbox/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"
@ -230,6 +231,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Description: "ISO",
ResultKey: "iso_path",
Url: b.config.ISOUrls,
Extension: "iso",
},
&vboxcommon.StepOutputDir{
Force: b.config.PackerForce,
@ -252,9 +254,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
},
new(vboxcommon.StepAttachFloppy),
&vboxcommon.StepForwardSSH{
GuestPort: b.config.SSHPort,
CommConfig: &b.config.SSHConfig.Comm,
HostPortMin: b.config.SSHHostPortMin,
HostPortMax: b.config.SSHHostPortMax,
SkipNatMapping: b.config.SSHSkipNatMapping,
},
&vboxcommon.StepVBoxManage{
Commands: b.config.VBoxManage,
@ -269,10 +272,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
VMName: b.config.VMName,
Ctx: b.config.ctx,
},
&common.StepConnectSSH{
SSHAddress: vboxcommon.SSHAddress,
&communicator.StepConnect{
Config: &b.config.SSHConfig.Comm,
Host: vboxcommon.CommHost,
SSHConfig: vboxcommon.SSHConfigFunc(b.config.SSHConfig),
SSHWaitTimeout: b.config.SSHWaitTimeout,
SSHPort: vboxcommon.SSHPort,
},
&vboxcommon.StepUploadVersion{
Path: b.config.VBoxVersionFile,
@ -296,6 +300,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Format: b.config.Format,
OutputDir: b.config.OutputDir,
ExportOpts: b.config.ExportOpts.ExportOpts,
SkipNatMapping: b.config.SSHSkipNatMapping,
},
}

View File

@ -0,0 +1,30 @@
package iso
import (
"testing"
builderT "github.com/mitchellh/packer/helper/builder/testing"
)
func TestBuilderAcc_basic(t *testing.T) {
builderT.Test(t, builderT.TestCase{
Builder: &Builder{},
Template: testBuilderAccBasic,
})
}
const testBuilderAccBasic = `
{
"builders": [{
"type": "test",
"guest_os_type": "Ubuntu_64",
"iso_url": "http://releases.ubuntu.com/12.04/ubuntu-12.04.5-server-amd64.iso",
"iso_checksum": "769474248a3897f4865817446f9a4a53",
"iso_checksum_type": "md5",
"ssh_username": "packer",
"ssh_password": "packer",
"ssh_wait_timeout": "30s",
"shutdown_command": "echo 'packer' | sudo -S shutdown -P now"
}]
}
`

View File

@ -10,6 +10,7 @@ import (
"github.com/mitchellh/multistep"
vboxcommon "github.com/mitchellh/packer/builder/virtualbox/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer"
)
@ -82,9 +83,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
},
new(vboxcommon.StepAttachFloppy),
&vboxcommon.StepForwardSSH{
GuestPort: b.config.SSHPort,
CommConfig: &b.config.SSHConfig.Comm,
HostPortMin: b.config.SSHHostPortMin,
HostPortMax: b.config.SSHHostPortMax,
SkipNatMapping: b.config.SSHSkipNatMapping,
},
&vboxcommon.StepVBoxManage{
Commands: b.config.VBoxManage,
@ -99,10 +101,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
VMName: b.config.VMName,
Ctx: b.config.ctx,
},
&common.StepConnectSSH{
SSHAddress: vboxcommon.SSHAddress,
&communicator.StepConnect{
Config: &b.config.SSHConfig.Comm,
Host: vboxcommon.CommHost,
SSHConfig: vboxcommon.SSHConfigFunc(b.config.SSHConfig),
SSHWaitTimeout: b.config.SSHWaitTimeout,
SSHPort: vboxcommon.SSHPort,
},
&vboxcommon.StepUploadVersion{
Path: b.config.VBoxVersionFile,
@ -126,6 +129,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Format: b.config.Format,
OutputDir: b.config.OutputDir,
ExportOpts: b.config.ExportOpts.ExportOpts,
SkipNatMapping: b.config.SSHSkipNatMapping,
},
}

View File

@ -40,8 +40,8 @@ type Config struct {
}
func NewConfig(raws ...interface{}) (*Config, []string, error) {
var c Config
err := config.Decode(&c, &config.DecodeOpts{
c := new(Config)
err := config.Decode(c, &config.DecodeOpts{
Interpolate: true,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{
@ -132,5 +132,5 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
c.ImportFlags = append(c.ImportFlags, "--options", c.ImportOpts)
}
return &c, warnings, nil
return c, warnings, nil
}

Some files were not shown because too many files have changed in this diff Show More