Commit 899cd7b8 authored by marc-ta's avatar marc-ta

Merge pull request #1 from mitchellh/master

update to most recent
parents 793698f8 10bad00a
## 0.8.0 (unreleased) ## 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: 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 * **New config function: `template_dir`**: The directory to the template
being built. This should be used for template-relative paths. [GH-54] being built. This should be used for template-relative paths. [GH-54]
IMPROVEMENTS: 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 * builder/openstack: Add `rackconnect_wait` for Rackspace customers to wait for
RackConnect data to appear RackConnect data to appear
* buidler/openstakc: Add `ssh_interface` option for rackconnect for users that * buidler/openstack: Add `ssh_interface` option for rackconnect for users that
have prohibitive firewalls have prohibitive firewalls
* builder/openstack: Flavor names can be used as well as refs
* builder/openstack: Add `availability_zone` [GH-2016]
* builder/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: Add `-name` flag for specifying name from CLI [GH-2042]
* command/push: Push configuration in templates supports variables [GH-1861] * command/push: Push configuration in templates supports variables [GH-1861]
* post-processor/docker-save: Can be chained [GH-2179]
* post-processor/docker-tag: Support `force` option [GH-2055] * post-processor/docker-tag: Support `force` option [GH-2055]
* post-processor/docker-tag: Can be chained [GH-2179]
BUG FIXES: BUG FIXES:
* core: Fix potential panic for post-processor plugin exits [GH-2098] * core: Fix potential panic for post-processor plugin exits [GH-2098]
* 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: Remove deprecated ec2-upload-bundle paramger [GH-1931]
* builder/amazon: Use IAM Profile to upload bundle if provided [GH-1985]
* builder/amazon: Use correct exit code after SSH authentication failed [GH-2004]
* builder/amazon: Retry finding created instance for eventual * builder/amazon: Retry finding created instance for eventual
consistency. [GH-2129] consistency. [GH-2129]
* builder/amazon: If no AZ is specified, use AZ chosen automatically by * builder/amazon: If no AZ is specified, use AZ chosen automatically by
AWS for spot instance. [GH-2017] AWS for spot instance. [GH-2017]
* builder/amazon: Private key file (only available in debug mode) * builder/amazon: Private key file (only available in debug mode)
is deleted on cleanup. [GH-1801] is deleted on cleanup. [GH-1801]
* builder/amazon: AMI copy won't copy to the source region [GH-2123]
* builder/amazon: Validate AMI doesn't exist with name prior to build [GH-1774]
* builder/amazon: Improved retry logic around waiting for instances. [GH-1764]
* builder/amazon: Fix issues with creating Block Devices. [GH-2195]
* builder/amazon/chroot: Retry waiting for disk attachments [GH-2046] * builder/amazon/chroot: Retry waiting for disk attachments [GH-2046]
* builder/amazon/instance: Use `-i` in sudo commands so PATH is inherited. [GH-1930] * builder/amazon/instance: Use `-i` in sudo commands so PATH is inherited. [GH-1930]
* builder/amazon/instance: Use `--region` flag for bundle upload command. [GH-1931]
* builder/digitalocean: Wait for droplet to unlock before changing state,
should lower the "pending event" errors.
* builder/digitalocean: Ignore invalid fields from the ever-changing v2 API * builder/digitalocean: Ignore invalid fields from the ever-changing v2 API
* builder/digitalocean: Private images can be used as a source [GH-1792]
* builder/docker: Fixed hang on prompt while copying script * builder/docker: Fixed hang on prompt while copying script
* builder/docker: Use `docker exec` for newer versions of Docker for * builder/docker: Use `docker exec` for newer versions of Docker for
running scripts [GH-1993] running scripts [GH-1993]
* builder/docker: Fix crash that could occur at certain timed ctrl-c [GH-1838] * 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/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/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: Case-insensitive match of MAC address to find IP [GH-1989]
* builder/vmware: More robust IP parsing from ifconfig output [GH-1999] * builder/vmware: More robust IP parsing from ifconfig output [GH-1999]
* builder/vmware: Nested output directories for ESXi work [GH-2174]
* 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] * command/validate: don't crash for invalid builds [GH-2139]
* post-processor/atlas: Find common archive prefix for Windows [GH-1874] * post-processor/atlas: Find common archive prefix for Windows [GH-1874]
* post-processor/atlas: Fix index out of range panic [GH-1959]
* post-processor/vagrant-cloud: Fixed failing on response * post-processor/vagrant-cloud: Fixed failing on response
* post-processor/vagrant-cloud: Don't delete version on error [GH-2014] * post-processor/vagrant-cloud: Don't delete version on error [GH-2014]
* provisioner/puppet-masterless: Allow manifest_file to be a directory * provisioner/puppet-masterless: Allow manifest_file to be a directory
* provisioner/salt-masterless: Add `--retcode-passthrough` to salt-call * provisioner/salt-masterless: Add `--retcode-passthrough` to salt-call
* provisioner/shell: chmod executable script to 0755, not 0777 [GH-1708]
* provisioner/shell: inline commands failing will fail the provisioner [GH-2069]
* provisioner/shell: single quotes in env vars are escaped [GH-2229]
## 0.7.5 (December 9, 2014) ## 0.7.5 (December 9, 2014)
......
...@@ -31,7 +31,7 @@ testrace: ...@@ -31,7 +31,7 @@ testrace:
go test -race $(TEST) $(TESTARGS) go test -race $(TEST) $(TESTARGS)
updatedeps: updatedeps:
go get -d -v -p 2 ./... go get -u -d -v -p 2 ./...
vet: vet:
@go tool vet 2>/dev/null ; if [ $$? -eq 3 ]; then \ @go tool vet 2>/dev/null ; if [ $$? -eq 3 ]; then \
......
...@@ -147,6 +147,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -147,6 +147,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
// Build the steps // Build the steps
steps := []multistep.Step{ steps := []multistep.Step{
&awscommon.StepPreValidate{
DestAmiName: b.config.AMIName,
ForceDeregister: b.config.AMIForceDeregister,
},
&StepInstanceInfo{}, &StepInstanceInfo{},
&awscommon.StepSourceAMIInfo{ &awscommon.StepSourceAMIInfo{
SourceAmi: b.config.SourceAmi, SourceAmi: b.config.SourceAmi,
...@@ -164,9 +168,15 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -164,9 +168,15 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&StepChrootProvision{}, &StepChrootProvision{},
&StepEarlyCleanup{}, &StepEarlyCleanup{},
&StepSnapshot{}, &StepSnapshot{},
&awscommon.StepDeregisterAMI{
ForceDeregister: b.config.AMIForceDeregister,
AMIName: b.config.AMIName,
},
&StepRegisterAMI{}, &StepRegisterAMI{},
&awscommon.StepAMIRegionCopy{ &awscommon.StepAMIRegionCopy{
Regions: b.config.AMIRegions, AccessConfig: &b.config.AccessConfig,
Regions: b.config.AMIRegions,
Name: b.config.AMIName,
}, },
&awscommon.StepModifyAMIAttributes{ &awscommon.StepModifyAMIAttributes{
Description: b.config.AMIDescription, Description: b.config.AMIDescription,
......
...@@ -17,6 +17,7 @@ type AMIConfig struct { ...@@ -17,6 +17,7 @@ type AMIConfig struct {
AMIRegions []string `mapstructure:"ami_regions"` AMIRegions []string `mapstructure:"ami_regions"`
AMITags map[string]string `mapstructure:"tags"` AMITags map[string]string `mapstructure:"tags"`
AMIEnhancedNetworking bool `mapstructure:"enhanced_networking"` AMIEnhancedNetworking bool `mapstructure:"enhanced_networking"`
AMIForceDeregister bool `mapstructure:"force_deregister"`
} }
func (c *AMIConfig) Prepare(ctx *interpolate.Context) []error { func (c *AMIConfig) Prepare(ctx *interpolate.Context) []error {
......
...@@ -29,13 +29,23 @@ func buildBlockDevices(b []BlockDevice) []*ec2.BlockDeviceMapping { ...@@ -29,13 +29,23 @@ func buildBlockDevices(b []BlockDevice) []*ec2.BlockDeviceMapping {
for _, blockDevice := range b { for _, blockDevice := range b {
ebsBlockDevice := &ec2.EBSBlockDevice{ ebsBlockDevice := &ec2.EBSBlockDevice{
SnapshotID: &blockDevice.SnapshotId,
Encrypted: &blockDevice.Encrypted,
IOPS: &blockDevice.IOPS,
VolumeType: &blockDevice.VolumeType, VolumeType: &blockDevice.VolumeType,
VolumeSize: &blockDevice.VolumeSize, VolumeSize: &blockDevice.VolumeSize,
DeleteOnTermination: &blockDevice.DeleteOnTermination, DeleteOnTermination: &blockDevice.DeleteOnTermination,
} }
// IOPS is only valid for SSD Volumes
if blockDevice.VolumeType != "" && blockDevice.VolumeType != "standard" && blockDevice.VolumeType != "gp2" {
ebsBlockDevice.IOPS = &blockDevice.IOPS
}
// You cannot specify Encrypted if you specify a Snapshot ID
if blockDevice.SnapshotId != "" {
ebsBlockDevice.SnapshotID = &blockDevice.SnapshotId
} else {
ebsBlockDevice.Encrypted = &blockDevice.Encrypted
}
mapping := &ec2.BlockDeviceMapping{ mapping := &ec2.BlockDeviceMapping{
EBS: ebsBlockDevice, EBS: ebsBlockDevice,
DeviceName: &blockDevice.DeviceName, DeviceName: &blockDevice.DeviceName,
......
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"testing" "testing"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awsutil"
"github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ec2"
) )
...@@ -28,11 +29,48 @@ func TestBlockDevice(t *testing.T) { ...@@ -28,11 +29,48 @@ func TestBlockDevice(t *testing.T) {
DeviceName: aws.String("/dev/sdb"), DeviceName: aws.String("/dev/sdb"),
VirtualName: aws.String("ephemeral0"), VirtualName: aws.String("ephemeral0"),
EBS: &ec2.EBSBlockDevice{ EBS: &ec2.EBSBlockDevice{
Encrypted: aws.Boolean(false),
SnapshotID: aws.String("snap-1234"), SnapshotID: aws.String("snap-1234"),
VolumeType: aws.String("standard"), VolumeType: aws.String("standard"),
VolumeSize: aws.Long(8), VolumeSize: aws.Long(8),
DeleteOnTermination: aws.Boolean(true), DeleteOnTermination: aws.Boolean(true),
},
},
},
{
Config: &BlockDevice{
DeviceName: "/dev/sdb",
VolumeSize: 8,
},
Result: &ec2.BlockDeviceMapping{
DeviceName: aws.String("/dev/sdb"),
VirtualName: aws.String(""),
EBS: &ec2.EBSBlockDevice{
Encrypted: aws.Boolean(false),
VolumeType: aws.String(""),
VolumeSize: aws.Long(8),
DeleteOnTermination: aws.Boolean(false),
},
},
},
{
Config: &BlockDevice{
DeviceName: "/dev/sdb",
VirtualName: "ephemeral0",
VolumeType: "io1",
VolumeSize: 8,
DeleteOnTermination: true,
IOPS: 1000,
},
Result: &ec2.BlockDeviceMapping{
DeviceName: aws.String("/dev/sdb"),
VirtualName: aws.String("ephemeral0"),
EBS: &ec2.EBSBlockDevice{
Encrypted: aws.Boolean(false),
VolumeType: aws.String("io1"),
VolumeSize: aws.Long(8),
DeleteOnTermination: aws.Boolean(true),
IOPS: aws.Long(1000), IOPS: aws.Long(1000),
}, },
}, },
...@@ -48,11 +86,11 @@ func TestBlockDevice(t *testing.T) { ...@@ -48,11 +86,11 @@ func TestBlockDevice(t *testing.T) {
expected := []*ec2.BlockDeviceMapping{tc.Result} expected := []*ec2.BlockDeviceMapping{tc.Result}
got := blockDevices.BuildAMIDevices() got := blockDevices.BuildAMIDevices()
if !reflect.DeepEqual(expected, got) { if !reflect.DeepEqual(expected, got) {
t.Fatalf("bad: %#v", expected) t.Fatalf("Bad block device, \nexpected: %s\n\ngot: %s", awsutil.StringValue(expected), awsutil.StringValue(got))
} }
if !reflect.DeepEqual(expected, blockDevices.BuildLaunchDevices()) { if !reflect.DeepEqual(expected, blockDevices.BuildLaunchDevices()) {
t.Fatalf("bad: %#v", expected) t.Fatalf("Bad block device, \nexpected: %s\n\ngot: %s", awsutil.StringValue(expected), awsutil.StringValue(blockDevices.BuildLaunchDevices()))
} }
} }
} }
...@@ -7,6 +7,7 @@ import ( ...@@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/mitchellh/packer/common/uuid" "github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
) )
...@@ -21,40 +22,32 @@ type RunConfig struct { ...@@ -21,40 +22,32 @@ type RunConfig struct {
SourceAmi string `mapstructure:"source_ami"` SourceAmi string `mapstructure:"source_ami"`
SpotPrice string `mapstructure:"spot_price"` SpotPrice string `mapstructure:"spot_price"`
SpotPriceAutoProduct string `mapstructure:"spot_price_auto_product"` 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"` SecurityGroupId string `mapstructure:"security_group_id"`
SecurityGroupIds []string `mapstructure:"security_group_ids"` SecurityGroupIds []string `mapstructure:"security_group_ids"`
SubnetId string `mapstructure:"subnet_id"` SubnetId string `mapstructure:"subnet_id"`
TemporaryKeyPairName string `mapstructure:"temporary_key_pair_name"` TemporaryKeyPairName string `mapstructure:"temporary_key_pair_name"`
UserData string `mapstructure:"user_data"` UserData string `mapstructure:"user_data"`
UserDataFile string `mapstructure:"user_data_file"` UserDataFile string `mapstructure:"user_data_file"`
WindowsPasswordTimeout time.Duration `mapstructure:"windows_password_timeout"`
VpcId string `mapstructure:"vpc_id"` VpcId string `mapstructure:"vpc_id"`
// Unexported fields that are calculated from others // Communicator settings
sshTimeout time.Duration Comm communicator.Config `mapstructure:",squash"`
SSHPrivateIp bool `mapstructure:"ssh_private_ip"`
} }
func (c *RunConfig) Prepare(ctx *interpolate.Context) []error { 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 == "" { if c.TemporaryKeyPairName == "" {
c.TemporaryKeyPairName = fmt.Sprintf( c.TemporaryKeyPairName = fmt.Sprintf(
"packer %s", uuid.TimeOrderedUUID()) "packer %s", uuid.TimeOrderedUUID())
} }
if c.WindowsPasswordTimeout == 0 {
c.WindowsPasswordTimeout = 10 * time.Minute
}
// Validation // Validation
var errs []error errs := c.Comm.Prepare(ctx)
if c.SourceAmi == "" { if c.SourceAmi == "" {
errs = append(errs, errors.New("A source_ami must be specified")) errs = append(errs, errors.New("A source_ami must be specified"))
} }
...@@ -70,10 +63,6 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error { ...@@ -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 != "" { if c.UserData != "" && c.UserDataFile != "" {
errs = append(errs, fmt.Errorf("Only one of user_data or user_data_file can be specified.")) errs = append(errs, fmt.Errorf("Only one of user_data or user_data_file can be specified."))
} else if c.UserDataFile != "" { } else if c.UserDataFile != "" {
...@@ -91,15 +80,5 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error { ...@@ -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 return errs
} }
func (c *RunConfig) SSHTimeout() time.Duration {
return c.sshTimeout
}
...@@ -4,6 +4,8 @@ import ( ...@@ -4,6 +4,8 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"testing" "testing"
"github.com/mitchellh/packer/helper/communicator"
) )
func init() { func init() {
...@@ -19,7 +21,10 @@ func testConfig() *RunConfig { ...@@ -19,7 +21,10 @@ func testConfig() *RunConfig {
return &RunConfig{ return &RunConfig{
SourceAmi: "abcd", SourceAmi: "abcd",
InstanceType: "m1.small", InstanceType: "m1.small",
SSHUsername: "root",
Comm: communicator.Config{
SSHUsername: "foo",
},
} }
} }
...@@ -62,41 +67,28 @@ func TestRunConfigPrepare_SpotAuto(t *testing.T) { ...@@ -62,41 +67,28 @@ func TestRunConfigPrepare_SpotAuto(t *testing.T) {
func TestRunConfigPrepare_SSHPort(t *testing.T) { func TestRunConfigPrepare_SSHPort(t *testing.T) {
c := testConfig() 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)
}
c.SSHPort = 44
if err := c.Prepare(nil); len(err) != 0 { if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
if c.SSHPort != 44 { if c.Comm.SSHPort != 22 {
t.Fatalf("invalid value: %d", c.SSHPort) t.Fatalf("invalid value: %d", c.Comm.SSHPort)
} }
}
func TestRunConfigPrepare_SSHTimeout(t *testing.T) { c.Comm.SSHPort = 44
c := testConfig()
c.RawSSHTimeout = ""
if err := c.Prepare(nil); len(err) != 0 { if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
c.RawSSHTimeout = "bad" if c.Comm.SSHPort != 44 {
if err := c.Prepare(nil); len(err) != 1 { t.Fatalf("invalid value: %d", c.Comm.SSHPort)
t.Fatalf("err: %s", err)
} }
} }
func TestRunConfigPrepare_SSHUsername(t *testing.T) { func TestRunConfigPrepare_SSHUsername(t *testing.T) {
c := testConfig() c := testConfig()
c.SSHUsername = "" c.Comm.SSHUsername = ""
if err := c.Prepare(nil); len(err) != 1 { if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
......
...@@ -10,9 +10,9 @@ import ( ...@@ -10,9 +10,9 @@ import (
"golang.org/x/crypto/ssh" "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. // 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) { return func(state multistep.StateBag) (string, error) {
for j := 0; j < 2; j++ { for j := 0; j < 2; j++ {
var host string var host string
...@@ -28,7 +28,7 @@ func SSHAddress(e *ec2.EC2, port int, private bool) func(multistep.StateBag) (st ...@@ -28,7 +28,7 @@ func SSHAddress(e *ec2.EC2, port int, private bool) func(multistep.StateBag) (st
} }
if host != "" { if host != "" {
return fmt.Sprintf("%s:%d", host, port), nil return host, nil
} }
r, err := e.DescribeInstances(&ec2.DescribeInstancesInput{ r, err := e.DescribeInstances(&ec2.DescribeInstancesInput{
......
...@@ -67,10 +67,10 @@ func AMIStateRefreshFunc(conn *ec2.EC2, imageId string) StateRefreshFunc { ...@@ -67,10 +67,10 @@ func AMIStateRefreshFunc(conn *ec2.EC2, imageId string) StateRefreshFunc {
// InstanceStateRefreshFunc returns a StateRefreshFunc that is used to watch // InstanceStateRefreshFunc returns a StateRefreshFunc that is used to watch
// an EC2 instance. // an EC2 instance.
func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) StateRefreshFunc { func InstanceStateRefreshFunc(conn *ec2.EC2, instanceId string) StateRefreshFunc {
return func() (interface{}, string, error) { return func() (interface{}, string, error) {
resp, err := conn.DescribeInstances(&ec2.DescribeInstancesInput{ resp, err := conn.DescribeInstances(&ec2.DescribeInstancesInput{
InstanceIDs: []*string{i.InstanceID}, InstanceIDs: []*string{&instanceId},
}) })
if err != nil { if err != nil {
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidInstanceID.NotFound" { if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidInstanceID.NotFound" {
...@@ -91,7 +91,7 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) StateRefreshFunc { ...@@ -91,7 +91,7 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) StateRefreshFunc {
return nil, "", nil return nil, "", nil
} }
i = resp.Reservations[0].Instances[0] i := resp.Reservations[0].Instances[0]
return i, *i.State.Name, nil return i, *i.State.Name, nil
} }
} }
......
...@@ -14,6 +14,7 @@ import ( ...@@ -14,6 +14,7 @@ import (
type StepAMIRegionCopy struct { type StepAMIRegionCopy struct {
AccessConfig *AccessConfig AccessConfig *AccessConfig
Regions []string Regions []string
Name string
} }
func (s *StepAMIRegionCopy) Run(state multistep.StateBag) multistep.StepAction { func (s *StepAMIRegionCopy) Run(state multistep.StateBag) multistep.StepAction {
...@@ -32,12 +33,18 @@ 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 var wg sync.WaitGroup
errs := new(packer.MultiError) errs := new(packer.MultiError)
for _, region := range s.Regions { 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) wg.Add(1)
ui.Message(fmt.Sprintf("Copying to: %s", region)) ui.Message(fmt.Sprintf("Copying to: %s", region))
go func(region string) { go func(region string) {
defer wg.Done() 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() lock.Lock()
defer lock.Unlock() defer lock.Unlock()
...@@ -69,7 +76,7 @@ func (s *StepAMIRegionCopy) Cleanup(state multistep.StateBag) { ...@@ -69,7 +76,7 @@ func (s *StepAMIRegionCopy) Cleanup(state multistep.StateBag) {
// amiRegionCopy does a copy for the given AMI to the target region and // amiRegionCopy does a copy for the given AMI to the target region and
// returns the resulting ID or error. // 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) { target string, source string) (string, error) {
// Connect to the region where the AMI will be copied to // Connect to the region where the AMI will be copied to
...@@ -83,6 +90,7 @@ func amiRegionCopy(state multistep.StateBag, config *AccessConfig, imageId strin ...@@ -83,6 +90,7 @@ func amiRegionCopy(state multistep.StateBag, config *AccessConfig, imageId strin
resp, err := regionconn.CopyImage(&ec2.CopyImageInput{ resp, err := regionconn.CopyImage(&ec2.CopyImageInput{
SourceRegion: &source, SourceRegion: &source,
SourceImageID: &imageId, SourceImageID: &imageId,
Name: &name,
}) })
if err != nil { if err != nil {
......
...@@ -25,19 +25,56 @@ func (s *StepCreateTags) Run(state multistep.StateBag) multistep.StepAction { ...@@ -25,19 +25,56 @@ func (s *StepCreateTags) Run(state multistep.StateBag) multistep.StepAction {
var ec2Tags []*ec2.Tag var ec2Tags []*ec2.Tag
for key, value := range s.Tags { for key, value := range s.Tags {
ui.Message(fmt.Sprintf("Adding tag: \"%s\": \"%s\"", key, value)) 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{ regionconn := ec2.New(&aws.Config{
Credentials: ec2conn.Config.Credentials, Credentials: ec2conn.Config.Credentials,
Region: region, Region: region,
}) })
_, err := regionconn.CreateTags(&ec2.CreateTagsInput{
Resources: []*string{&ami}, _, err = regionconn.CreateTags(&ec2.CreateTagsInput{
Resources: resourceIds,
Tags: ec2Tags, Tags: ec2Tags,
}) })
if err != nil { 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) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
......
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) {
}
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
}
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) {}
package common package common
import ( import (
"encoding/base64"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
...@@ -53,7 +54,14 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi ...@@ -53,7 +54,14 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi
return multistep.ActionHalt 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) userData = string(contents)
} }
ui.Say("Launching a source AWS instance...") ui.Say("Launching a source AWS instance...")
...@@ -174,11 +182,15 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi ...@@ -174,11 +182,15 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi
ImageID: &s.SourceAMI, ImageID: &s.SourceAMI,
InstanceType: &s.InstanceType, InstanceType: &s.InstanceType,
UserData: &userData, UserData: &userData,
SecurityGroupIDs: securityGroupIds,
IAMInstanceProfile: &ec2.IAMInstanceProfileSpecification{Name: &s.IamInstanceProfile}, IAMInstanceProfile: &ec2.IAMInstanceProfileSpecification{Name: &s.IamInstanceProfile},
SubnetID: &s.SubnetId,
NetworkInterfaces: []*ec2.InstanceNetworkInterfaceSpecification{ 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{ Placement: &ec2.SpotPlacement{
AvailabilityZone: &availabilityZone, AvailabilityZone: &availabilityZone,
...@@ -223,36 +235,17 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi ...@@ -223,36 +235,17 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi
instanceId = *spotResp.SpotInstanceRequests[0].InstanceID instanceId = *spotResp.SpotInstanceRequests[0].InstanceID
} }
instanceResp, err := ec2conn.DescribeInstances(&ec2.DescribeInstancesInput{ ui.Message(fmt.Sprintf("Instance ID: %s", instanceId))
InstanceIDs: []*string{&instanceId}}) ui.Say(fmt.Sprintf("Waiting for instance (%v) to become ready...", instanceId))
for i := 0; i < 10; i++ {
if err == nil {
break
}
time.Sleep(3 * time.Second)
instanceResp, err = ec2conn.DescribeInstances(&ec2.DescribeInstancesInput{
InstanceIDs: []*string{&instanceId}})
}
if err != nil {
err := fmt.Errorf("Error finding source instance (%s): %s", instanceId, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
s.instance = instanceResp.Reservations[0].Instances[0]
ui.Message(fmt.Sprintf("Instance ID: %s", *s.instance.InstanceID))
ui.Say(fmt.Sprintf("Waiting for instance (%s) to become ready...", *s.instance.InstanceID))
stateChange := StateChangeConf{ stateChange := StateChangeConf{
Pending: []string{"pending"}, Pending: []string{"pending"},
Target: "running", Target: "running",
Refresh: InstanceStateRefreshFunc(ec2conn, s.instance), Refresh: InstanceStateRefreshFunc(ec2conn, instanceId),
StepState: state, StepState: state,
} }
latestInstance, err := WaitForState(&stateChange) latestInstance, err := WaitForState(&stateChange)
if err != nil { 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) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
...@@ -329,7 +322,7 @@ func (s *StepRunSourceInstance) Cleanup(state multistep.StateBag) { ...@@ -329,7 +322,7 @@ func (s *StepRunSourceInstance) Cleanup(state multistep.StateBag) {
} }
stateChange := StateChangeConf{ stateChange := StateChangeConf{
Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"}, Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"},
Refresh: InstanceStateRefreshFunc(ec2conn, s.instance), Refresh: InstanceStateRefreshFunc(ec2conn, *s.instance.InstanceID),
Target: "terminated", Target: "terminated",
} }
......
...@@ -9,12 +9,13 @@ import ( ...@@ -9,12 +9,13 @@ import (
"github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ec2"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common/uuid" "github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
type StepSecurityGroup struct { type StepSecurityGroup struct {
CommConfig *communicator.Config
SecurityGroupIds []string SecurityGroupIds []string
SSHPort int
VpcId string VpcId string
createdGroupId string createdGroupId string
...@@ -30,8 +31,9 @@ func (s *StepSecurityGroup) Run(state multistep.StateBag) multistep.StepAction { ...@@ -30,8 +31,9 @@ func (s *StepSecurityGroup) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionContinue return multistep.ActionContinue
} }
if s.SSHPort == 0 { port := s.CommConfig.Port()
panic("SSHPort must be set to a non-zero value.") if port == 0 {
panic("port must be set to a non-zero value.")
} }
// Create the group // Create the group
...@@ -57,15 +59,17 @@ func (s *StepSecurityGroup) Run(state multistep.StateBag) multistep.StepAction { ...@@ -57,15 +59,17 @@ func (s *StepSecurityGroup) Run(state multistep.StateBag) multistep.StepAction {
req := &ec2.AuthorizeSecurityGroupIngressInput{ req := &ec2.AuthorizeSecurityGroupIngressInput{
GroupID: groupResp.GroupID, GroupID: groupResp.GroupID,
IPProtocol: aws.String("tcp"), IPProtocol: aws.String("tcp"),
FromPort: aws.Long(int64(s.SSHPort)), FromPort: aws.Long(int64(port)),
ToPort: aws.Long(int64(s.SSHPort)), ToPort: aws.Long(int64(port)),
CIDRIP: aws.String("0.0.0.0/0"), CIDRIP: aws.String("0.0.0.0/0"),
} }
// We loop and retry this a few times because sometimes the security // We loop and retry this a few times because sometimes the security
// group isn't available immediately because AWS resources are eventaully // group isn't available immediately because AWS resources are eventaully
// consistent. // 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++ { for i := 0; i < 5; i++ {
_, err = ec2conn.AuthorizeSecurityGroupIngress(req) _, err = ec2conn.AuthorizeSecurityGroupIngress(req)
if err == nil { if err == nil {
......
...@@ -20,7 +20,7 @@ func isalphanumeric(b byte) bool { ...@@ -20,7 +20,7 @@ func isalphanumeric(b byte) bool {
// Clean up AMI name by replacing invalid characters with "-" // Clean up AMI name by replacing invalid characters with "-"
func templateCleanAMIName(s string) string { func templateCleanAMIName(s string) string {
allowed := []byte{'(', ')', ',', '/', '-', '_'} allowed := []byte{'(', ')', ',', '/', '-', '_', ' '}
b := []byte(s) b := []byte(s)
newb := make([]byte, len(b)) newb := make([]byte, len(b))
for i, c := range b { for i, c := range b {
......
...@@ -5,8 +5,8 @@ import ( ...@@ -5,8 +5,8 @@ import (
) )
func TestAMITemplatePrepare_clean(t *testing.T) { func TestAMITemplatePrepare_clean(t *testing.T) {
origName := "AMZamz09(),/-_:&^$%" origName := "AMZamz09(),/-_:&^ $%"
expected := "AMZamz09(),/-_-----" expected := "AMZamz09(),/-_--- --"
name := templateCleanAMIName(origName) name := templateCleanAMIName(origName)
......
...@@ -13,6 +13,7 @@ import ( ...@@ -13,6 +13,7 @@ import (
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common" awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config" "github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
...@@ -78,6 +79,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -78,6 +79,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
// Build the steps // Build the steps
steps := []multistep.Step{ steps := []multistep.Step{
&awscommon.StepPreValidate{
DestAmiName: b.config.AMIName,
ForceDeregister: b.config.AMIForceDeregister,
},
&awscommon.StepSourceAMIInfo{ &awscommon.StepSourceAMIInfo{
SourceAmi: b.config.SourceAmi, SourceAmi: b.config.SourceAmi,
EnhancedNetworking: b.config.AMIEnhancedNetworking, EnhancedNetworking: b.config.AMIEnhancedNetworking,
...@@ -86,11 +91,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -86,11 +91,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Debug: b.config.PackerDebug, Debug: b.config.PackerDebug,
DebugKeyPath: fmt.Sprintf("ec2_%s.pem", b.config.PackerBuildName), DebugKeyPath: fmt.Sprintf("ec2_%s.pem", b.config.PackerBuildName),
KeyPairName: b.config.TemporaryKeyPairName, KeyPairName: b.config.TemporaryKeyPairName,
PrivateKeyFile: b.config.SSHPrivateKeyFile, PrivateKeyFile: b.config.RunConfig.Comm.SSHPrivateKey,
}, },
&awscommon.StepSecurityGroup{ &awscommon.StepSecurityGroup{
SecurityGroupIds: b.config.SecurityGroupIds, SecurityGroupIds: b.config.SecurityGroupIds,
SSHPort: b.config.SSHPort, CommConfig: &b.config.RunConfig.Comm,
VpcId: b.config.VpcId, VpcId: b.config.VpcId,
}, },
&awscommon.StepRunSourceInstance{ &awscommon.StepRunSourceInstance{
...@@ -109,20 +114,31 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -109,20 +114,31 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
BlockDevices: b.config.BlockDevices, BlockDevices: b.config.BlockDevices,
Tags: b.config.RunTags, Tags: b.config.RunTags,
}, },
&common.StepConnectSSH{ &awscommon.StepGetPassword{
SSHAddress: awscommon.SSHAddress( Comm: &b.config.RunConfig.Comm,
ec2conn, b.config.SSHPort, b.config.SSHPrivateIp), Timeout: b.config.WindowsPasswordTimeout,
SSHConfig: awscommon.SSHConfig(b.config.SSHUsername), },
SSHWaitTimeout: b.config.SSHTimeout(), &communicator.StepConnect{
Config: &b.config.RunConfig.Comm,
Host: awscommon.SSHHost(
ec2conn,
b.config.SSHPrivateIp),
SSHConfig: awscommon.SSHConfig(
b.config.RunConfig.Comm.SSHUsername),
}, },
&common.StepProvision{}, &common.StepProvision{},
&stepStopInstance{SpotPrice: b.config.SpotPrice}, &stepStopInstance{SpotPrice: b.config.SpotPrice},
// TODO(mitchellh): verify works with spots // TODO(mitchellh): verify works with spots
&stepModifyInstance{}, &stepModifyInstance{},
&awscommon.StepDeregisterAMI{
ForceDeregister: b.config.AMIForceDeregister,
AMIName: b.config.AMIName,
},
&stepCreateAMI{}, &stepCreateAMI{},
&awscommon.StepAMIRegionCopy{ &awscommon.StepAMIRegionCopy{
AccessConfig: &b.config.AccessConfig, AccessConfig: &b.config.AccessConfig,
Regions: b.config.AMIRegions, Regions: b.config.AMIRegions,
Name: b.config.AMIName,
}, },
&awscommon.StepModifyAMIAttributes{ &awscommon.StepModifyAMIAttributes{
Description: b.config.AMIDescription, Description: b.config.AMIDescription,
......
package ebs package ebs
import ( import (
"fmt"
"os" "os"
"testing" "testing"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/mitchellh/packer/builder/amazon/common"
builderT "github.com/mitchellh/packer/helper/builder/testing" builderT "github.com/mitchellh/packer/helper/builder/testing"
"github.com/mitchellh/packer/packer"
) )
func TestBuilderAcc_basic(t *testing.T) { func TestBuilderAcc_basic(t *testing.T) {
...@@ -15,6 +19,64 @@ 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) { func testAccPreCheck(t *testing.T) {
if v := os.Getenv("AWS_ACCESS_KEY_ID"); v == "" { if v := os.Getenv("AWS_ACCESS_KEY_ID"); v == "" {
t.Fatal("AWS_ACCESS_KEY_ID must be set for acceptance tests") t.Fatal("AWS_ACCESS_KEY_ID must be set for acceptance tests")
...@@ -25,6 +87,16 @@ func testAccPreCheck(t *testing.T) { ...@@ -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 = ` const testBuilderAccBasic = `
{ {
"builders": [{ "builders": [{
...@@ -37,3 +109,35 @@ const testBuilderAccBasic = ` ...@@ -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)
}
...@@ -40,7 +40,7 @@ func (s *stepStopInstance) Run(state multistep.StateBag) multistep.StepAction { ...@@ -40,7 +40,7 @@ func (s *stepStopInstance) Run(state multistep.StateBag) multistep.StepAction {
stateChange := awscommon.StateChangeConf{ stateChange := awscommon.StateChangeConf{
Pending: []string{"running", "stopping"}, Pending: []string{"running", "stopping"},
Target: "stopped", Target: "stopped",
Refresh: awscommon.InstanceStateRefreshFunc(ec2conn, instance), Refresh: awscommon.InstanceStateRefreshFunc(ec2conn, *instance.InstanceID),
StepState: state, StepState: state,
} }
_, err = awscommon.WaitForState(&stateChange) _, err = awscommon.WaitForState(&stateChange)
......
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"
}
}
]
}
`
...@@ -13,6 +13,7 @@ import ( ...@@ -13,6 +13,7 @@ import (
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common" awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config" "github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
...@@ -73,15 +74,25 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { ...@@ -73,15 +74,25 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
} }
if b.config.BundleUploadCommand == "" { if b.config.BundleUploadCommand == "" {
b.config.BundleUploadCommand = "sudo -i -n ec2-upload-bundle " + if b.config.IamInstanceProfile != "" {
"-b {{.BucketName}} " + b.config.BundleUploadCommand = "sudo -i -n ec2-upload-bundle " +
"-m {{.ManifestPath}} " + "-b {{.BucketName}} " +
"-a {{.AccessKey}} " + "-m {{.ManifestPath}} " +
"-s {{.SecretKey}} " + "-d {{.BundleDirectory}} " +
"-d {{.BundleDirectory}} " + "--batch " +
"--batch " + "--region {{.Region}} " +
"--location {{.Region}} " + "--retry"
"--retry" } else {
b.config.BundleUploadCommand = "sudo -i -n ec2-upload-bundle " +
"-b {{.BucketName}} " +
"-m {{.ManifestPath}} " +
"-a {{.AccessKey}} " +
"-s {{.SecretKey}} " +
"-d {{.BundleDirectory}} " +
"--batch " +
"--region {{.Region}} " +
"--retry"
}
} }
if b.config.BundleVolCommand == "" { if b.config.BundleVolCommand == "" {
...@@ -157,6 +168,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -157,6 +168,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
// Build the steps // Build the steps
steps := []multistep.Step{ steps := []multistep.Step{
&awscommon.StepPreValidate{
DestAmiName: b.config.AMIName,
ForceDeregister: b.config.AMIForceDeregister,
},
&awscommon.StepSourceAMIInfo{ &awscommon.StepSourceAMIInfo{
SourceAmi: b.config.SourceAmi, SourceAmi: b.config.SourceAmi,
EnhancedNetworking: b.config.AMIEnhancedNetworking, EnhancedNetworking: b.config.AMIEnhancedNetworking,
...@@ -165,11 +180,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -165,11 +180,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Debug: b.config.PackerDebug, Debug: b.config.PackerDebug,
DebugKeyPath: fmt.Sprintf("ec2_%s.pem", b.config.PackerBuildName), DebugKeyPath: fmt.Sprintf("ec2_%s.pem", b.config.PackerBuildName),
KeyPairName: b.config.TemporaryKeyPairName, KeyPairName: b.config.TemporaryKeyPairName,
PrivateKeyFile: b.config.SSHPrivateKeyFile, PrivateKeyFile: b.config.RunConfig.Comm.SSHPrivateKey,
}, },
&awscommon.StepSecurityGroup{ &awscommon.StepSecurityGroup{
CommConfig: &b.config.RunConfig.Comm,
SecurityGroupIds: b.config.SecurityGroupIds, SecurityGroupIds: b.config.SecurityGroupIds,
SSHPort: b.config.SSHPort,
VpcId: b.config.VpcId, VpcId: b.config.VpcId,
}, },
&awscommon.StepRunSourceInstance{ &awscommon.StepRunSourceInstance{
...@@ -187,11 +202,17 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -187,11 +202,17 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
BlockDevices: b.config.BlockDevices, BlockDevices: b.config.BlockDevices,
Tags: b.config.RunTags, Tags: b.config.RunTags,
}, },
&common.StepConnectSSH{ &awscommon.StepGetPassword{
SSHAddress: awscommon.SSHAddress( Comm: &b.config.RunConfig.Comm,
ec2conn, b.config.SSHPort, b.config.SSHPrivateIp), Timeout: b.config.WindowsPasswordTimeout,
SSHConfig: awscommon.SSHConfig(b.config.SSHUsername), },
SSHWaitTimeout: b.config.SSHTimeout(), &communicator.StepConnect{
Config: &b.config.RunConfig.Comm,
Host: awscommon.SSHHost(
ec2conn,
b.config.SSHPrivateIp),
SSHConfig: awscommon.SSHConfig(
b.config.RunConfig.Comm.SSHUsername),
}, },
&common.StepProvision{}, &common.StepProvision{},
&StepUploadX509Cert{}, &StepUploadX509Cert{},
...@@ -201,10 +222,15 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -201,10 +222,15 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&StepUploadBundle{ &StepUploadBundle{
Debug: b.config.PackerDebug, Debug: b.config.PackerDebug,
}, },
&awscommon.StepDeregisterAMI{
ForceDeregister: b.config.AMIForceDeregister,
AMIName: b.config.AMIName,
},
&StepRegisterAMI{}, &StepRegisterAMI{},
&awscommon.StepAMIRegionCopy{ &awscommon.StepAMIRegionCopy{
AccessConfig: &b.config.AccessConfig, AccessConfig: &b.config.AccessConfig,
Regions: b.config.AMIRegions, Regions: b.config.AMIRegions,
Name: b.config.AMIName,
}, },
&awscommon.StepModifyAMIAttributes{ &awscommon.StepModifyAMIAttributes{
Description: b.config.AMIDescription, Description: b.config.AMIDescription,
......
// All of the methods used to communicate with the digital_ocean API
// are here. Their API is on a path to V2, so just plain JSON is used
// in place of a proper client library for now.
package digitalocean
type Region struct {
Slug string `json:"slug"`
Name string `json:"name"`
// v1 only
Id uint `json:"id,omitempty"`
// v2 only
Sizes []string `json:"sizes,omitempty"`
Available bool `json:"available,omitempty"`
Features []string `json:"features,omitempty"`
}
type RegionsResp struct {
Regions []Region
}
type Size struct {
Slug string `json:"slug"`
// v1 only
Id uint `json:"id,omitempty"`
Name string `json:"name,omitempty"`
// v2 only
Memory uint `json:"memory,omitempty"`
VCPUS uint `json:"vcpus,omitempty"`
Disk uint `json:"disk,omitempty"`
Transfer float64 `json:"transfer,omitempty"`
PriceMonthly float64 `json:"price_monthly,omitempty"`
PriceHourly float64 `json:"price_hourly,omitempty"`
}
type SizesResp struct {
Sizes []Size
}
type Image struct {
Id uint `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
Distribution string `json:"distribution"`
// v2 only
Public bool `json:"public,omitempty"`
ActionIds []string `json:"action_ids,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
}
type ImagesResp struct {
Images []Image
}
type DigitalOceanClient interface {
CreateKey(string, string) (uint, error)
DestroyKey(uint) error
CreateDroplet(string, string, string, string, uint, bool) (uint, error)
DestroyDroplet(uint) error
PowerOffDroplet(uint) error
ShutdownDroplet(uint) error
CreateSnapshot(uint, string) error
Images() ([]Image, error)
DestroyImage(uint) error
DropletStatus(uint) (string, string, error)
Image(string) (Image, error)
Regions() ([]Region, error)
Region(string) (Region, error)
Sizes() ([]Size, error)
Size(string) (Size, error)
}
// All of the methods used to communicate with the digital_ocean API
// are here. Their API is on a path to V2, so just plain JSON is used
// in place of a proper client library for now.
package digitalocean
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/mitchellh/mapstructure"
)
type DigitalOceanClientV1 struct {
// The http client for communicating
client *http.Client
// Credentials
ClientID string
APIKey string
// The base URL of the API
APIURL string
}
// Creates a new client for communicating with DO
func DigitalOceanClientNewV1(client string, key string, url string) *DigitalOceanClientV1 {
c := &DigitalOceanClientV1{
client: &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
},
},
APIURL: url,
ClientID: client,
APIKey: key,
}
return c
}
// Creates an SSH Key and returns it's id
func (d DigitalOceanClientV1) CreateKey(name string, pub string) (uint, error) {
params := url.Values{}
params.Set("name", name)
params.Set("ssh_pub_key", pub)
body, err := NewRequestV1(d, "ssh_keys/new", params)
if err != nil {
return 0, err
}
// Read the SSH key's ID we just created
key := body["ssh_key"].(map[string]interface{})
keyId := key["id"].(float64)
return uint(keyId), nil
}
// Destroys an SSH key
func (d DigitalOceanClientV1) DestroyKey(id uint) error {
path := fmt.Sprintf("ssh_keys/%v/destroy", id)
_, err := NewRequestV1(d, path, url.Values{})
return err
}
// Creates a droplet and returns it's id
func (d DigitalOceanClientV1) CreateDroplet(name string, size string, image string, region string, keyId uint, privateNetworking bool) (uint, error) {
params := url.Values{}
params.Set("name", name)
found_size, err := d.Size(size)
if err != nil {
return 0, fmt.Errorf("Invalid size or lookup failure: '%s': %s", size, err)
}
found_image, err := d.Image(image)
if err != nil {
return 0, fmt.Errorf("Invalid image or lookup failure: '%s': %s", image, err)
}
found_region, err := d.Region(region)
if err != nil {
return 0, fmt.Errorf("Invalid region or lookup failure: '%s': %s", region, err)
}
params.Set("size_slug", found_size.Slug)
params.Set("image_slug", found_image.Slug)
params.Set("region_slug", found_region.Slug)
params.Set("ssh_key_ids", fmt.Sprintf("%v", keyId))
params.Set("private_networking", fmt.Sprintf("%v", privateNetworking))
body, err := NewRequestV1(d, "droplets/new", params)
if err != nil {
return 0, err
}
// Read the Droplets ID
droplet := body["droplet"].(map[string]interface{})
dropletId := droplet["id"].(float64)
return uint(dropletId), err
}
// Destroys a droplet
func (d DigitalOceanClientV1) DestroyDroplet(id uint) error {
path := fmt.Sprintf("droplets/%v/destroy", id)
_, err := NewRequestV1(d, path, url.Values{})
return err
}
// Powers off a droplet
func (d DigitalOceanClientV1) PowerOffDroplet(id uint) error {
path := fmt.Sprintf("droplets/%v/power_off", id)
_, err := NewRequestV1(d, path, url.Values{})
return err
}
// Shutsdown a droplet. This is a "soft" shutdown.
func (d DigitalOceanClientV1) ShutdownDroplet(id uint) error {
path := fmt.Sprintf("droplets/%v/shutdown", id)
_, err := NewRequestV1(d, path, url.Values{})
return err
}
// Creates a snaphot of a droplet by it's ID
func (d DigitalOceanClientV1) CreateSnapshot(id uint, name string) error {
path := fmt.Sprintf("droplets/%v/snapshot", id)
params := url.Values{}
params.Set("name", name)
_, err := NewRequestV1(d, path, params)
return err
}
// Returns all available images.
func (d DigitalOceanClientV1) Images() ([]Image, error) {
resp, err := NewRequestV1(d, "images", url.Values{})
if err != nil {
return nil, err
}
var result ImagesResp
if err := mapstructure.Decode(resp, &result); err != nil {
return nil, err
}
return result.Images, nil
}
// Destroys an image by its ID.
func (d DigitalOceanClientV1) DestroyImage(id uint) error {
path := fmt.Sprintf("images/%d/destroy", id)
_, err := NewRequestV1(d, path, url.Values{})
return err
}
// Returns DO's string representation of status "off" "new" "active" etc.
func (d DigitalOceanClientV1) DropletStatus(id uint) (string, string, error) {
path := fmt.Sprintf("droplets/%v", id)
body, err := NewRequestV1(d, path, url.Values{})
if err != nil {
return "", "", err
}
var ip string
// Read the droplet's "status"
droplet := body["droplet"].(map[string]interface{})
status := droplet["status"].(string)
if droplet["ip_address"] != nil {
ip = droplet["ip_address"].(string)
}
return ip, status, err
}
// Sends an api request and returns a generic map[string]interface of
// the response.
func NewRequestV1(d DigitalOceanClientV1, path string, params url.Values) (map[string]interface{}, error) {
client := d.client
// Add the authentication parameters
params.Set("client_id", d.ClientID)
params.Set("api_key", d.APIKey)
url := fmt.Sprintf("%s/%s?%s", d.APIURL, path, params.Encode())
// Do some basic scrubbing so sensitive information doesn't appear in logs
scrubbedUrl := strings.Replace(url, d.ClientID, "CLIENT_ID", -1)
scrubbedUrl = strings.Replace(scrubbedUrl, d.APIKey, "API_KEY", -1)
log.Printf("sending new request to digitalocean: %s", scrubbedUrl)
var lastErr error
for attempts := 1; attempts < 10; attempts++ {
resp, err := client.Get(url)
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, err
}
log.Printf("response from digitalocean: %s", body)
var decodedResponse map[string]interface{}
err = json.Unmarshal(body, &decodedResponse)
if err != nil {
err = errors.New(fmt.Sprintf("Failed to decode JSON response (HTTP %v) from DigitalOcean: %s",
resp.StatusCode, body))
return decodedResponse, err
}
// Check for errors sent by digitalocean
status := decodedResponse["status"].(string)
if status == "OK" {
return decodedResponse, nil
}
if status == "ERROR" {
statusRaw, ok := decodedResponse["error_message"]
if ok {
status = statusRaw.(string)
} else {
status = fmt.Sprintf(
"Unknown error. Full response body: %s", body)
}
}
lastErr = errors.New(fmt.Sprintf("Received error from DigitalOcean (%d): %s",
resp.StatusCode, status))
log.Println(lastErr)
if strings.Contains(status, "a pending event") {
// Retry, DigitalOcean sends these dumb "pending event"
// errors all the time.
time.Sleep(5 * time.Second)
continue
}
// Some other kind of error. Just return.
return decodedResponse, lastErr
}
return nil, lastErr
}
func (d DigitalOceanClientV1) Image(slug_or_name_or_id string) (Image, error) {
images, err := d.Images()
if err != nil {
return Image{}, err
}
for _, image := range images {
if strings.EqualFold(image.Slug, slug_or_name_or_id) {
return image, nil
}
}
for _, image := range images {
if strings.EqualFold(image.Name, slug_or_name_or_id) {
return image, nil
}
}
for _, image := range images {
id, err := strconv.Atoi(slug_or_name_or_id)
if err == nil {
if image.Id == uint(id) {
return image, nil
}
}
}
err = errors.New(fmt.Sprintf("Unknown image '%v'", slug_or_name_or_id))
return Image{}, err
}
// Returns all available regions.
func (d DigitalOceanClientV1) Regions() ([]Region, error) {
resp, err := NewRequestV1(d, "regions", url.Values{})
if err != nil {
return nil, err
}
var result RegionsResp
if err := mapstructure.Decode(resp, &result); err != nil {
return nil, err
}
return result.Regions, nil
}
func (d DigitalOceanClientV1) Region(slug_or_name_or_id string) (Region, error) {
regions, err := d.Regions()
if err != nil {
return Region{}, err
}
for _, region := range regions {
if strings.EqualFold(region.Slug, slug_or_name_or_id) {
return region, nil
}
}
for _, region := range regions {
if strings.EqualFold(region.Name, slug_or_name_or_id) {
return region, nil
}
}
for _, region := range regions {
id, err := strconv.Atoi(slug_or_name_or_id)
if err == nil {
if region.Id == uint(id) {
return region, nil
}
}
}
err = errors.New(fmt.Sprintf("Unknown region '%v'", slug_or_name_or_id))
return Region{}, err
}
// Returns all available sizes.
func (d DigitalOceanClientV1) Sizes() ([]Size, error) {
resp, err := NewRequestV1(d, "sizes", url.Values{})
if err != nil {
return nil, err
}
var result SizesResp
if err := mapstructure.Decode(resp, &result); err != nil {
return nil, err
}
return result.Sizes, nil
}
func (d DigitalOceanClientV1) Size(slug_or_name_or_id string) (Size, error) {
sizes, err := d.Sizes()
if err != nil {
return Size{}, err
}
for _, size := range sizes {
if strings.EqualFold(size.Slug, slug_or_name_or_id) {
return size, nil
}
}
for _, size := range sizes {
if strings.EqualFold(size.Name, slug_or_name_or_id) {
return size, nil
}
}
for _, size := range sizes {
id, err := strconv.Atoi(slug_or_name_or_id)
if err == nil {
if size.Id == uint(id) {
return size, nil
}
}
}
err = errors.New(fmt.Sprintf("Unknown size '%v'", slug_or_name_or_id))
return Size{}, err
}
This diff is collapsed.
...@@ -4,6 +4,8 @@ import ( ...@@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"log" "log"
"strconv" "strconv"
"github.com/digitalocean/godo"
) )
type Artifact struct { type Artifact struct {
...@@ -11,13 +13,13 @@ type Artifact struct { ...@@ -11,13 +13,13 @@ type Artifact struct {
snapshotName string snapshotName string
// The ID of the image // The ID of the image
snapshotId uint snapshotId int
// The name of the region // The name of the region
regionName string regionName string
// The client for making API calls // The client for making API calls
client DigitalOceanClient client *godo.Client
} }
func (*Artifact) BuilderId() string { func (*Artifact) BuilderId() string {
...@@ -43,5 +45,6 @@ func (a *Artifact) State(name string) interface{} { ...@@ -43,5 +45,6 @@ func (a *Artifact) State(name string) interface{} {
func (a *Artifact) Destroy() error { func (a *Artifact) Destroy() error {
log.Printf("Destroying image: %d (%s)", a.snapshotId, a.snapshotName) log.Printf("Destroying image: %d (%s)", a.snapshotId, a.snapshotName)
return a.client.DestroyImage(a.snapshotId) _, err := a.client.Images.Delete(a.snapshotId)
return err
} }
...@@ -4,208 +4,39 @@ ...@@ -4,208 +4,39 @@
package digitalocean package digitalocean
import ( import (
"errors"
"fmt" "fmt"
"log" "log"
"os"
"time"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/common/uuid" "github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate" "golang.org/x/oauth2"
) )
// see https://api.digitalocean.com/images/?client_id=[client_id]&api_key=[api_key]
// name="Ubuntu 12.04.4 x64", id=6374128,
const DefaultImage = "ubuntu-12-04-x64"
// see https://api.digitalocean.com/regions/?client_id=[client_id]&api_key=[api_key]
// name="New York 3", id=8
const DefaultRegion = "nyc3"
// see https://api.digitalocean.com/sizes/?client_id=[client_id]&api_key=[api_key]
// name="512MB", id=66 (the smallest droplet size)
const DefaultSize = "512mb"
// The unique id for the builder // The unique id for the builder
const BuilderId = "pearkes.digitalocean" const BuilderId = "pearkes.digitalocean"
// Configuration tells the builder the credentials
// to use while communicating with DO and describes the image
// you are creating
type Config struct {
common.PackerConfig `mapstructure:",squash"`
ClientID string `mapstructure:"client_id"`
APIKey string `mapstructure:"api_key"`
APIURL string `mapstructure:"api_url"`
APIToken string `mapstructure:"api_token"`
RegionID uint `mapstructure:"region_id"`
SizeID uint `mapstructure:"size_id"`
ImageID uint `mapstructure:"image_id"`
Region string `mapstructure:"region"`
Size string `mapstructure:"size"`
Image string `mapstructure:"image"`
PrivateNetworking bool `mapstructure:"private_networking"`
SnapshotName string `mapstructure:"snapshot_name"`
DropletName string `mapstructure:"droplet_name"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPort uint `mapstructure:"ssh_port"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
RawStateTimeout string `mapstructure:"state_timeout"`
// These are unexported since they're set by other fields
// being set.
sshTimeout time.Duration
stateTimeout time.Duration
ctx *interpolate.Context
}
type Builder struct { type Builder struct {
config Config config Config
runner multistep.Runner runner multistep.Runner
} }
func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
err := config.Decode(&b.config, &config.DecodeOpts{ c, warnings, errs := NewConfig(raws...)
Interpolate: true, if errs != nil {
}, raws...) return warnings, errs
if err != nil {
return nil, err
}
// Optional configuration with defaults
if b.config.APIKey == "" {
// Default to environment variable for api_key, if it exists
b.config.APIKey = os.Getenv("DIGITALOCEAN_API_KEY")
}
if b.config.ClientID == "" {
// Default to environment variable for client_id, if it exists
b.config.ClientID = os.Getenv("DIGITALOCEAN_CLIENT_ID")
}
if b.config.APIURL == "" {
// Default to environment variable for api_url, if it exists
b.config.APIURL = os.Getenv("DIGITALOCEAN_API_URL")
}
if b.config.APIToken == "" {
// Default to environment variable for api_token, if it exists
b.config.APIToken = os.Getenv("DIGITALOCEAN_API_TOKEN")
}
if b.config.Region == "" {
if b.config.RegionID != 0 {
b.config.Region = fmt.Sprintf("%v", b.config.RegionID)
} else {
b.config.Region = DefaultRegion
}
}
if b.config.Size == "" {
if b.config.SizeID != 0 {
b.config.Size = fmt.Sprintf("%v", b.config.SizeID)
} else {
b.config.Size = DefaultSize
}
}
if b.config.Image == "" {
if b.config.ImageID != 0 {
b.config.Image = fmt.Sprintf("%v", b.config.ImageID)
} else {
b.config.Image = DefaultImage
}
}
if b.config.SnapshotName == "" {
// Default to packer-{{ unix timestamp (utc) }}
b.config.SnapshotName = "packer-{{timestamp}}"
}
if b.config.DropletName == "" {
// Default to packer-[time-ordered-uuid]
b.config.DropletName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
} }
b.config = *c
if b.config.SSHUsername == "" {
// Default to "root". You can override this if your
// SourceImage has a different user account then the DO default
b.config.SSHUsername = "root"
}
if b.config.SSHPort == 0 {
// Default to port 22 per DO default
b.config.SSHPort = 22
}
if b.config.RawSSHTimeout == "" {
// Default to 1 minute timeouts
b.config.RawSSHTimeout = "1m"
}
if b.config.RawStateTimeout == "" {
// Default to 6 minute timeouts waiting for
// desired state. i.e waiting for droplet to become active
b.config.RawStateTimeout = "6m"
}
var errs *packer.MultiError
if b.config.APIToken == "" {
// Required configurations that will display errors if not set
if b.config.ClientID == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("a client_id for v1 auth or api_token for v2 auth must be specified"))
}
if b.config.APIKey == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("a api_key for v1 auth or api_token for v2 auth must be specified"))
}
}
if b.config.APIURL == "" {
b.config.APIURL = "https://api.digitalocean.com"
}
sshTimeout, err := time.ParseDuration(b.config.RawSSHTimeout)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
}
b.config.sshTimeout = sshTimeout
stateTimeout, err := time.ParseDuration(b.config.RawStateTimeout)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing state_timeout: %s", err))
}
b.config.stateTimeout = stateTimeout
if errs != nil && len(errs.Errors) > 0 {
return nil, errs
}
common.ScrubConfig(b.config, b.config.ClientID, b.config.APIKey)
return nil, nil return nil, nil
} }
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
var client DigitalOceanClient client := godo.NewClient(oauth2.NewClient(oauth2.NoContext, &apiTokenSource{
// Initialize the DO API client AccessToken: b.config.APIToken,
if b.config.APIToken == "" { }))
client = DigitalOceanClientNewV1(b.config.ClientID, b.config.APIKey, b.config.APIURL)
} else {
client = DigitalOceanClientNewV2(b.config.APIToken, b.config.APIURL)
}
// Set up the state // Set up the state
state := new(multistep.BasicStateBag) state := new(multistep.BasicStateBag)
...@@ -216,13 +47,16 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -216,13 +47,16 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
// Build the steps // Build the steps
steps := []multistep.Step{ steps := []multistep.Step{
new(stepCreateSSHKey), &stepCreateSSHKey{
Debug: b.config.PackerDebug,
DebugKeyPath: fmt.Sprintf("do_%s.pem", b.config.PackerBuildName),
},
new(stepCreateDroplet), new(stepCreateDroplet),
new(stepDropletInfo), new(stepDropletInfo),
&common.StepConnectSSH{ &communicator.StepConnect{
SSHAddress: sshAddress, Config: &b.config.Comm,
SSHConfig: sshConfig, Host: commHost,
SSHWaitTimeout: 5 * time.Minute, SSHConfig: sshConfig,
}, },
new(common.StepProvision), new(common.StepProvision),
new(stepShutdown), new(stepShutdown),
...@@ -252,26 +86,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -252,26 +86,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
return nil, nil return nil, nil
} }
sregion := state.Get("region")
var region string
if sregion != nil {
region = sregion.(string)
} else {
region = fmt.Sprintf("%v", state.Get("region_id").(uint))
}
found_region, err := client.Region(region)
if err != nil {
return nil, err
}
artifact := &Artifact{ artifact := &Artifact{
snapshotName: state.Get("snapshot_name").(string), snapshotName: state.Get("snapshot_name").(string),
snapshotId: state.Get("snapshot_image_id").(uint), snapshotId: state.Get("snapshot_image_id").(int),
regionName: found_region.Name, regionName: state.Get("region").(string),
client: client, client: client,
} }
......
package digitalocean
import (
"os"
"testing"
builderT "github.com/mitchellh/packer/helper/builder/testing"
)
func TestBuilderAcc_basic(t *testing.T) {
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Builder: &Builder{},
Template: testBuilderAccBasic,
})
}
func testAccPreCheck(t *testing.T) {
if v := os.Getenv("DIGITALOCEAN_API_TOKEN"); v == "" {
t.Fatal("DIGITALOCEAN_API_TOKEN must be set for acceptance tests")
}
}
const testBuilderAccBasic = `
{
"builders": [{
"type": "test",
"region": "nyc2",
"size": "512mb",
"image": "ubuntu-12-04-x64"
}]
}
`
package digitalocean package digitalocean
import ( import (
"github.com/mitchellh/packer/packer"
"os"
"strconv" "strconv"
"testing" "testing"
) "time"
func init() { "github.com/mitchellh/packer/packer"
// Clear out the credential env vars )
os.Setenv("DIGITALOCEAN_API_KEY", "")
os.Setenv("DIGITALOCEAN_CLIENT_ID", "")
}
func testConfig() map[string]interface{} { func testConfig() map[string]interface{} {
return map[string]interface{}{ return map[string]interface{}{
"client_id": "foo", "api_token": "bar",
"api_key": "bar", "region": "nyc2",
"size": "512mb",
"image": "foo",
} }
} }
...@@ -43,90 +40,6 @@ func TestBuilder_Prepare_BadType(t *testing.T) { ...@@ -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) { func TestBuilderPrepare_InvalidKey(t *testing.T) {
var b Builder var b Builder
config := testConfig() config := testConfig()
...@@ -147,22 +60,18 @@ func TestBuilderPrepare_Region(t *testing.T) { ...@@ -147,22 +60,18 @@ func TestBuilderPrepare_Region(t *testing.T) {
config := testConfig() config := testConfig()
// Test default // Test default
delete(config, "region")
warnings, err := b.Prepare(config) warnings, err := b.Prepare(config)
if len(warnings) > 0 { if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings) t.Fatalf("bad: %#v", warnings)
} }
if err != nil { if err == nil {
t.Fatalf("should not have error: %s", err) t.Fatalf("should error")
}
if b.config.Region != DefaultRegion {
t.Errorf("found %s, expected %s", b.config.Region, DefaultRegion)
} }
expected := "sfo1" expected := "sfo1"
// Test set // Test set
config["region_id"] = 0
config["region"] = expected config["region"] = expected
b = Builder{} b = Builder{}
warnings, err = b.Prepare(config) warnings, err = b.Prepare(config)
...@@ -183,22 +92,18 @@ func TestBuilderPrepare_Size(t *testing.T) { ...@@ -183,22 +92,18 @@ func TestBuilderPrepare_Size(t *testing.T) {
config := testConfig() config := testConfig()
// Test default // Test default
delete(config, "size")
warnings, err := b.Prepare(config) warnings, err := b.Prepare(config)
if len(warnings) > 0 { if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings) t.Fatalf("bad: %#v", warnings)
} }
if err != nil { if err == nil {
t.Fatalf("should not have error: %s", err) t.Fatalf("should error")
}
if b.config.Size != DefaultSize {
t.Errorf("found %s, expected %s", b.config.Size, DefaultSize)
} }
expected := "1024mb" expected := "1024mb"
// Test set // Test set
config["size_id"] = 0
config["size"] = expected config["size"] = expected
b = Builder{} b = Builder{}
warnings, err = b.Prepare(config) warnings, err = b.Prepare(config)
...@@ -219,22 +124,18 @@ func TestBuilderPrepare_Image(t *testing.T) { ...@@ -219,22 +124,18 @@ func TestBuilderPrepare_Image(t *testing.T) {
config := testConfig() config := testConfig()
// Test default // Test default
delete(config, "image")
warnings, err := b.Prepare(config) warnings, err := b.Prepare(config)
if len(warnings) > 0 { if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings) t.Fatalf("bad: %#v", warnings)
} }
if err != nil { if err == nil {
t.Fatalf("should not have error: %s", err) t.Fatal("should error")
}
if b.config.Image != DefaultImage {
t.Errorf("found %s, expected %s", b.config.Image, DefaultImage)
} }
expected := "ubuntu-14-04-x64" expected := "ubuntu-14-04-x64"
// Test set // Test set
config["image_id"] = 0
config["image"] = expected config["image"] = expected
b = Builder{} b = Builder{}
warnings, err = b.Prepare(config) warnings, err = b.Prepare(config)
...@@ -263,8 +164,8 @@ func TestBuilderPrepare_SSHUsername(t *testing.T) { ...@@ -263,8 +164,8 @@ func TestBuilderPrepare_SSHUsername(t *testing.T) {
t.Fatalf("should not have error: %s", err) t.Fatalf("should not have error: %s", err)
} }
if b.config.SSHUsername != "root" { if b.config.Comm.SSHUsername != "root" {
t.Errorf("invalid: %s", b.config.SSHUsername) t.Errorf("invalid: %s", b.config.Comm.SSHUsername)
} }
// Test set // Test set
...@@ -278,50 +179,9 @@ func TestBuilderPrepare_SSHUsername(t *testing.T) { ...@@ -278,50 +179,9 @@ func TestBuilderPrepare_SSHUsername(t *testing.T) {
t.Fatalf("should not have error: %s", err) t.Fatalf("should not have error: %s", err)
} }
if b.config.SSHUsername != "foo" { if b.config.Comm.SSHUsername != "foo" {
t.Errorf("invalid: %s", b.config.SSHUsername) 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) { func TestBuilderPrepare_StateTimeout(t *testing.T) {
...@@ -337,8 +197,8 @@ func TestBuilderPrepare_StateTimeout(t *testing.T) { ...@@ -337,8 +197,8 @@ func TestBuilderPrepare_StateTimeout(t *testing.T) {
t.Fatalf("should not have error: %s", err) t.Fatalf("should not have error: %s", err)
} }
if b.config.RawStateTimeout != "6m" { if b.config.StateTimeout != 6*time.Minute {
t.Errorf("invalid: %s", b.config.RawStateTimeout) t.Errorf("invalid: %s", b.config.StateTimeout)
} }
// Test set // Test set
......
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
}
package digitalocean package digitalocean
import ( import (
"golang.org/x/crypto/ssh"
"fmt" "fmt"
"golang.org/x/crypto/ssh"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
) )
func sshAddress(state multistep.StateBag) (string, error) { func commHost(state multistep.StateBag) (string, error) {
config := state.Get("config").(Config)
ipAddress := state.Get("droplet_ip").(string) 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) { func sshConfig(state multistep.StateBag) (*ssh.ClientConfig, error) {
...@@ -22,7 +22,7 @@ func sshConfig(state multistep.StateBag) (*ssh.ClientConfig, error) { ...@@ -22,7 +22,7 @@ func sshConfig(state multistep.StateBag) (*ssh.ClientConfig, error) {
} }
return &ssh.ClientConfig{ return &ssh.ClientConfig{
User: config.SSHUsername, User: config.Comm.SSHUsername,
Auth: []ssh.AuthMethod{ Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer), ssh.PublicKeys(signer),
}, },
......
...@@ -3,25 +3,36 @@ package digitalocean ...@@ -3,25 +3,36 @@ package digitalocean
import ( import (
"fmt" "fmt"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
type stepCreateDroplet struct { type stepCreateDroplet struct {
dropletId uint dropletId int
} }
func (s *stepCreateDroplet) Run(state multistep.StateBag) multistep.StepAction { func (s *stepCreateDroplet) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(DigitalOceanClient) client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(Config) c := state.Get("config").(Config)
sshKeyId := state.Get("ssh_key_id").(uint) sshKeyId := state.Get("ssh_key_id").(int)
ui.Say("Creating droplet...")
// Create the droplet based on configuration // Create the droplet based on configuration
dropletId, err := client.CreateDroplet(c.DropletName, c.Size, c.Image, c.Region, sshKeyId, c.PrivateNetworking) ui.Say("Creating droplet...")
droplet, _, err := client.Droplets.Create(&godo.DropletCreateRequest{
Name: c.DropletName,
Region: c.Region,
Size: c.Size,
Image: godo.DropletCreateImage{
Slug: c.Image,
},
SSHKeys: []godo.DropletCreateSSHKey{
godo.DropletCreateSSHKey{ID: int(sshKeyId)},
},
PrivateNetworking: c.PrivateNetworking,
UserData: c.UserData,
})
if err != nil { if err != nil {
err := fmt.Errorf("Error creating droplet: %s", err) err := fmt.Errorf("Error creating droplet: %s", err)
state.Put("error", err) state.Put("error", err)
...@@ -30,10 +41,10 @@ func (s *stepCreateDroplet) Run(state multistep.StateBag) multistep.StepAction { ...@@ -30,10 +41,10 @@ func (s *stepCreateDroplet) Run(state multistep.StateBag) multistep.StepAction {
} }
// We use this in cleanup // We use this in cleanup
s.dropletId = dropletId s.dropletId = droplet.ID
// Store the droplet id for later // Store the droplet id for later
state.Put("droplet_id", dropletId) state.Put("droplet_id", droplet.ID)
return multistep.ActionContinue return multistep.ActionContinue
} }
...@@ -44,19 +55,14 @@ func (s *stepCreateDroplet) Cleanup(state multistep.StateBag) { ...@@ -44,19 +55,14 @@ func (s *stepCreateDroplet) Cleanup(state multistep.StateBag) {
return return
} }
client := state.Get("client").(DigitalOceanClient) client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(Config)
// Destroy the droplet we just created // Destroy the droplet we just created
ui.Say("Destroying droplet...") ui.Say("Destroying droplet...")
_, err := client.Droplets.Delete(s.dropletId)
err := client.DestroyDroplet(s.dropletId)
if err != nil { if err != nil {
curlstr := fmt.Sprintf("curl '%v/droplets/%v/destroy?client_id=%v&api_key=%v'",
c.APIURL, s.dropletId, c.ClientID, c.APIKey)
ui.Error(fmt.Sprintf( ui.Error(fmt.Sprintf(
"Error destroying droplet. Please destroy it manually: %v", curlstr)) "Error destroying droplet. Please destroy it manually: %s", err))
} }
} }
...@@ -7,19 +7,25 @@ import ( ...@@ -7,19 +7,25 @@ import (
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"log" "log"
"os"
"runtime"
"code.google.com/p/gosshold/ssh" "code.google.com/p/gosshold/ssh"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common/uuid" "github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
type stepCreateSSHKey struct { type stepCreateSSHKey struct {
keyId uint Debug bool
DebugKeyPath string
keyId int
} }
func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction { func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(DigitalOceanClient) client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
ui.Say("Creating temporary ssh key for droplet...") ui.Say("Creating temporary ssh key for droplet...")
...@@ -46,7 +52,10 @@ func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction { ...@@ -46,7 +52,10 @@ func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction {
name := fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID()) name := fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
// Create the key! // Create the key!
keyId, err := client.CreateKey(name, pub_sshformat) key, _, err := client.Keys.Create(&godo.KeyCreateRequest{
Name: name,
PublicKey: pub_sshformat,
})
if err != nil { if err != nil {
err := fmt.Errorf("Error creating temporary SSH key: %s", err) err := fmt.Errorf("Error creating temporary SSH key: %s", err)
state.Put("error", err) state.Put("error", err)
...@@ -55,12 +64,37 @@ func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction { ...@@ -55,12 +64,37 @@ func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction {
} }
// We use this to check cleanup // We use this to check cleanup
s.keyId = keyId s.keyId = key.ID
log.Printf("temporary ssh key name: %s", name) log.Printf("temporary ssh key name: %s", name)
// Remember some state for the future // Remember some state for the future
state.Put("ssh_key_id", keyId) state.Put("ssh_key_id", key.ID)
// If we're in debug mode, output the private key to the working directory.
if s.Debug {
ui.Message(fmt.Sprintf("Saving key for debug purposes: %s", s.DebugKeyPath))
f, err := os.Create(s.DebugKeyPath)
if err != nil {
state.Put("error", fmt.Errorf("Error saving debug key: %s", err))
return multistep.ActionHalt
}
defer f.Close()
// Write the key out
if _, err := f.Write(pem.EncodeToMemory(&priv_blk)); err != nil {
state.Put("error", fmt.Errorf("Error saving debug key: %s", err))
return multistep.ActionHalt
}
// Chmod it so that it is SSH ready
if runtime.GOOS != "windows" {
if err := f.Chmod(0600); err != nil {
state.Put("error", fmt.Errorf("Error setting permissions of debug key: %s", err))
return multistep.ActionHalt
}
}
}
return multistep.ActionContinue return multistep.ActionContinue
} }
...@@ -71,18 +105,14 @@ func (s *stepCreateSSHKey) Cleanup(state multistep.StateBag) { ...@@ -71,18 +105,14 @@ func (s *stepCreateSSHKey) Cleanup(state multistep.StateBag) {
return return
} }
client := state.Get("client").(DigitalOceanClient) client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(Config)
ui.Say("Deleting temporary ssh key...") ui.Say("Deleting temporary ssh key...")
err := client.DestroyKey(s.keyId) _, err := client.Keys.DeleteByID(s.keyId)
curlstr := fmt.Sprintf("curl -H 'Authorization: Bearer #TOKEN#' -X DELETE '%v/v2/account/keys/%v'", c.APIURL, s.keyId)
if err != nil { if err != nil {
log.Printf("Error cleaning up ssh key: %v", err.Error()) log.Printf("Error cleaning up ssh key: %s", err)
ui.Error(fmt.Sprintf( ui.Error(fmt.Sprintf(
"Error cleaning up ssh key. Please delete the key manually: %v", curlstr)) "Error cleaning up ssh key. Please delete the key manually: %s", err))
} }
} }
...@@ -3,6 +3,7 @@ package digitalocean ...@@ -3,6 +3,7 @@ package digitalocean
import ( import (
"fmt" "fmt"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
...@@ -10,14 +11,14 @@ import ( ...@@ -10,14 +11,14 @@ import (
type stepDropletInfo struct{} type stepDropletInfo struct{}
func (s *stepDropletInfo) Run(state multistep.StateBag) multistep.StepAction { func (s *stepDropletInfo) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(DigitalOceanClient) client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(Config) c := state.Get("config").(Config)
dropletId := state.Get("droplet_id").(uint) dropletId := state.Get("droplet_id").(int)
ui.Say("Waiting for droplet to become active...") ui.Say("Waiting for droplet to become active...")
err := waitForDropletState("active", dropletId, client, c.stateTimeout) err := waitForDropletState("active", dropletId, client, c.StateTimeout)
if err != nil { if err != nil {
err := fmt.Errorf("Error waiting for droplet to become active: %s", err) err := fmt.Errorf("Error waiting for droplet to become active: %s", err)
state.Put("error", err) state.Put("error", err)
...@@ -26,16 +27,25 @@ func (s *stepDropletInfo) Run(state multistep.StateBag) multistep.StepAction { ...@@ -26,16 +27,25 @@ func (s *stepDropletInfo) Run(state multistep.StateBag) multistep.StepAction {
} }
// Set the IP on the state for later // Set the IP on the state for later
ip, _, err := client.DropletStatus(dropletId) droplet, _, err := client.Droplets.Get(dropletId)
if err != nil { if err != nil {
err := fmt.Errorf("Error retrieving droplet ID: %s", err) err := fmt.Errorf("Error retrieving droplet: %s", err)
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
} }
state.Put("droplet_ip", ip) // Verify we have an IPv4 address
invalid := droplet.Networks == nil ||
len(droplet.Networks.V4) == 0
if invalid {
err := fmt.Errorf("IPv4 address not found for droplet!")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
state.Put("droplet_ip", droplet.Networks.V4[0].IPAddress)
return multistep.ActionContinue return multistep.ActionContinue
} }
......
...@@ -3,7 +3,9 @@ package digitalocean ...@@ -3,7 +3,9 @@ package digitalocean
import ( import (
"fmt" "fmt"
"log" "log"
"time"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
...@@ -11,12 +13,12 @@ import ( ...@@ -11,12 +13,12 @@ import (
type stepPowerOff struct{} type stepPowerOff struct{}
func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction { func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(DigitalOceanClient) client := state.Get("client").(*godo.Client)
c := state.Get("config").(Config) c := state.Get("config").(Config)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
dropletId := state.Get("droplet_id").(uint) dropletId := state.Get("droplet_id").(int)
_, status, err := client.DropletStatus(dropletId) droplet, _, err := client.Droplets.Get(dropletId)
if err != nil { if err != nil {
err := fmt.Errorf("Error checking droplet state: %s", err) err := fmt.Errorf("Error checking droplet state: %s", err)
state.Put("error", err) state.Put("error", err)
...@@ -24,14 +26,14 @@ func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction { ...@@ -24,14 +26,14 @@ func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionHalt return multistep.ActionHalt
} }
if status == "off" { if droplet.Status == "off" {
// Droplet is already off, don't do anything // Droplet is already off, don't do anything
return multistep.ActionContinue return multistep.ActionContinue
} }
// Pull the plug on the Droplet // Pull the plug on the Droplet
ui.Say("Forcefully shutting down Droplet...") ui.Say("Forcefully shutting down Droplet...")
err = client.PowerOffDroplet(dropletId) _, _, err = client.DropletActions.PowerOff(dropletId)
if err != nil { if err != nil {
err := fmt.Errorf("Error powering off droplet: %s", err) err := fmt.Errorf("Error powering off droplet: %s", err)
state.Put("error", err) state.Put("error", err)
...@@ -40,13 +42,22 @@ func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction { ...@@ -40,13 +42,22 @@ func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
} }
log.Println("Waiting for poweroff event to complete...") 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 { if err != nil {
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
} }
// Wait for the droplet to become unlocked for future steps
if err := waitForDropletUnlocked(client, dropletId, 2*time.Minute); err != nil {
// If we get an error the first time, actually report it
err := fmt.Errorf("Error powering off droplet: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue return multistep.ActionContinue
} }
......
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"log" "log"
"time" "time"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
...@@ -12,16 +13,16 @@ import ( ...@@ -12,16 +13,16 @@ import (
type stepShutdown struct{} type stepShutdown struct{}
func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction { func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(DigitalOceanClient) client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
dropletId := state.Get("droplet_id").(uint) dropletId := state.Get("droplet_id").(int)
// Gracefully power off the droplet. We have to retry this a number // Gracefully power off the droplet. We have to retry this a number
// of times because sometimes it says it completed when it actually // of times because sometimes it says it completed when it actually
// did absolutely nothing (*ALAKAZAM!* magic!). We give up after // did absolutely nothing (*ALAKAZAM!* magic!). We give up after
// a pretty arbitrary amount of time. // a pretty arbitrary amount of time.
ui.Say("Gracefully shutting down droplet...") ui.Say("Gracefully shutting down droplet...")
err := client.ShutdownDroplet(dropletId) _, _, err := client.DropletActions.Shutdown(dropletId)
if err != nil { if err != nil {
// If we get an error the first time, actually report it // If we get an error the first time, actually report it
err := fmt.Errorf("Error shutting down droplet: %s", err) err := fmt.Errorf("Error shutting down droplet: %s", err)
...@@ -48,7 +49,7 @@ func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction { ...@@ -48,7 +49,7 @@ func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
for attempts := 2; attempts > 0; attempts++ { for attempts := 2; attempts > 0; attempts++ {
log.Printf("ShutdownDroplet attempt #%d...", attempts) log.Printf("ShutdownDroplet attempt #%d...", attempts)
err := client.ShutdownDroplet(dropletId) _, _, err := client.DropletActions.Shutdown(dropletId)
if err != nil { if err != nil {
log.Printf("Shutdown retry error: %s", err) log.Printf("Shutdown retry error: %s", err)
} }
...@@ -64,7 +65,19 @@ func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction { ...@@ -64,7 +65,19 @@ func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
err = waitForDropletState("off", dropletId, client, 2*time.Minute) err = waitForDropletState("off", dropletId, client, 2*time.Minute)
if err != nil { if err != nil {
log.Printf("Error waiting for graceful off: %s", err) // If we get an error the first time, actually report it
err := fmt.Errorf("Error shutting down droplet: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if err := waitForDropletUnlocked(client, dropletId, 2*time.Minute); err != nil {
// If we get an error the first time, actually report it
err := fmt.Errorf("Error shutting down droplet: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
} }
return multistep.ActionContinue return multistep.ActionContinue
......
...@@ -4,7 +4,9 @@ import ( ...@@ -4,7 +4,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"log" "log"
"time"
"github.com/digitalocean/godo"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
...@@ -12,13 +14,13 @@ import ( ...@@ -12,13 +14,13 @@ import (
type stepSnapshot struct{} type stepSnapshot struct{}
func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction { func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(DigitalOceanClient) client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(Config) c := state.Get("config").(Config)
dropletId := state.Get("droplet_id").(uint) dropletId := state.Get("droplet_id").(int)
ui.Say(fmt.Sprintf("Creating snapshot: %v", c.SnapshotName)) ui.Say(fmt.Sprintf("Creating snapshot: %v", c.SnapshotName))
err := client.CreateSnapshot(dropletId, c.SnapshotName) _, _, err := client.DropletActions.Snapshot(dropletId, c.SnapshotName)
if err != nil { if err != nil {
err := fmt.Errorf("Error creating snapshot: %s", err) err := fmt.Errorf("Error creating snapshot: %s", err)
state.Put("error", err) state.Put("error", err)
...@@ -26,8 +28,20 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction { ...@@ -26,8 +28,20 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionHalt return multistep.ActionHalt
} }
// Wait for the droplet to become unlocked first. For snapshots
// this can end up taking quite a long time, so we hardcode this to
// 10 minutes.
if err := waitForDropletUnlocked(client, dropletId, 10*time.Minute); err != nil {
// If we get an error the first time, actually report it
err := fmt.Errorf("Error shutting down droplet: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// With the pending state over, verify that we're in the active state
ui.Say("Waiting for snapshot to complete...") ui.Say("Waiting for snapshot to complete...")
err = waitForDropletState("active", dropletId, client, c.stateTimeout) err = waitForDropletState("active", dropletId, client, c.StateTimeout)
if err != nil { if err != nil {
err := fmt.Errorf("Error waiting for snapshot to complete: %s", err) err := fmt.Errorf("Error waiting for snapshot to complete: %s", err)
state.Put("error", err) state.Put("error", err)
...@@ -36,7 +50,7 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction { ...@@ -36,7 +50,7 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
} }
log.Printf("Looking up snapshot ID for snapshot: %s", c.SnapshotName) log.Printf("Looking up snapshot ID for snapshot: %s", c.SnapshotName)
images, err := client.Images() images, _, err := client.Images.ListUser(&godo.ListOptions{PerPage: 200})
if err != nil { if err != nil {
err := fmt.Errorf("Error looking up snapshot ID: %s", err) err := fmt.Errorf("Error looking up snapshot ID: %s", err)
state.Put("error", err) state.Put("error", err)
...@@ -44,10 +58,10 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction { ...@@ -44,10 +58,10 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionHalt return multistep.ActionHalt
} }
var imageId uint var imageId int
for _, image := range images { for _, image := range images {
if image.Name == c.SnapshotName { if image.Name == c.SnapshotName {
imageId = image.Id imageId = image.ID
break break
} }
} }
...@@ -60,7 +74,6 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction { ...@@ -60,7 +74,6 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
} }
log.Printf("Snapshot image ID: %d", imageId) log.Printf("Snapshot image ID: %d", imageId)
state.Put("snapshot_image_id", imageId) state.Put("snapshot_image_id", imageId)
state.Put("snapshot_name", c.SnapshotName) state.Put("snapshot_name", c.SnapshotName)
state.Put("region", c.Region) state.Put("region", c.Region)
......
package digitalocean
import (
"golang.org/x/oauth2"
)
type apiTokenSource struct {
AccessToken string
}
func (t *apiTokenSource) Token() (*oauth2.Token, error) {
return &oauth2.Token{
AccessToken: t.AccessToken,
}, nil
}
...@@ -4,11 +4,64 @@ import ( ...@@ -4,11 +4,64 @@ import (
"fmt" "fmt"
"log" "log"
"time" "time"
"github.com/digitalocean/godo"
) )
// waitForDropletUnlocked waits for the Droplet to be unlocked to
// avoid "pending" errors when making state changes.
func waitForDropletUnlocked(
client *godo.Client, dropletId int, timeout time.Duration) error {
done := make(chan struct{})
defer close(done)
result := make(chan error, 1)
go func() {
attempts := 0
for {
attempts += 1
log.Printf("[DEBUG] Checking droplet lock state... (attempt: %d)", attempts)
droplet, _, err := client.Droplets.Get(dropletId)
if err != nil {
result <- err
return
}
if !droplet.Locked {
result <- nil
return
}
// Wait 3 seconds in between
time.Sleep(3 * time.Second)
// Verify we shouldn't exit
select {
case <-done:
// We finished, so just exit the goroutine
return
default:
// Keep going
}
}
}()
log.Printf("[DEBUG] Waiting for up to %d seconds for droplet to unlock", timeout/time.Second)
select {
case err := <-result:
return err
case <-time.After(timeout):
return fmt.Errorf(
"Timeout while waiting to for droplet to unlock")
}
}
// waitForState simply blocks until the droplet is in // waitForState simply blocks until the droplet is in
// a state we expect, while eventually timing out. // a state we expect, while eventually timing out.
func waitForDropletState(desiredState string, dropletId uint, client DigitalOceanClient, timeout time.Duration) error { func waitForDropletState(
desiredState string, dropletId int,
client *godo.Client, timeout time.Duration) error {
done := make(chan struct{}) done := make(chan struct{})
defer close(done) defer close(done)
...@@ -19,13 +72,13 @@ func waitForDropletState(desiredState string, dropletId uint, client DigitalOcea ...@@ -19,13 +72,13 @@ func waitForDropletState(desiredState string, dropletId uint, client DigitalOcea
attempts += 1 attempts += 1
log.Printf("Checking droplet status... (attempt: %d)", attempts) log.Printf("Checking droplet status... (attempt: %d)", attempts)
_, status, err := client.DropletStatus(dropletId) droplet, _, err := client.Droplets.Get(dropletId)
if err != nil { if err != nil {
result <- err result <- err
return return
} }
if status == desiredState { if droplet.Status == desiredState {
result <- nil result <- nil
return return
} }
......
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
...@@ -42,7 +43,15 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -42,7 +43,15 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&StepTempDir{}, &StepTempDir{},
&StepPull{}, &StepPull{},
&StepRun{}, &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 { if b.config.Commit {
......
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
}
}
}
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config" "github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
...@@ -13,6 +14,7 @@ import ( ...@@ -13,6 +14,7 @@ import (
type Config struct { type Config struct {
common.PackerConfig `mapstructure:",squash"` common.PackerConfig `mapstructure:",squash"`
Comm communicator.Config `mapstructure:",squash"`
Commit bool Commit bool
ExportPath string `mapstructure:"export_path"` ExportPath string `mapstructure:"export_path"`
...@@ -31,10 +33,10 @@ type Config struct { ...@@ -31,10 +33,10 @@ type Config struct {
} }
func NewConfig(raws ...interface{}) (*Config, []string, error) { func NewConfig(raws ...interface{}) (*Config, []string, error) {
var c Config c := new(Config)
var md mapstructure.Metadata var md mapstructure.Metadata
err := config.Decode(&c, &config.DecodeOpts{ err := config.Decode(c, &config.DecodeOpts{
Metadata: &md, Metadata: &md,
Interpolate: true, Interpolate: true,
InterpolateFilter: &interpolate.RenderFilter{ InterpolateFilter: &interpolate.RenderFilter{
...@@ -69,7 +71,15 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { ...@@ -69,7 +71,15 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
c.Pull = true c.Pull = true
} }
// Default to the normal Docker type
if c.Comm.Type == "" {
c.Comm.Type = "docker"
}
var errs *packer.MultiError var errs *packer.MultiError
if es := c.Comm.Prepare(&c.ctx); len(es) > 0 {
errs = packer.MultiErrorAppend(errs, es...)
}
if c.Image == "" { if c.Image == "" {
errs = packer.MultiErrorAppend(errs, errs = packer.MultiErrorAppend(errs,
fmt.Errorf("image must be specified")) fmt.Errorf("image must be specified"))
...@@ -91,5 +101,5 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { ...@@ -91,5 +101,5 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
return nil, nil, errs return nil, nil, errs
} }
return &c, nil, nil return c, nil, nil
} }
...@@ -22,6 +22,10 @@ type Driver interface { ...@@ -22,6 +22,10 @@ type Driver interface {
// Import imports a container from a tar file // Import imports a container from a tar file
Import(path, repo string) (string, error) 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 // Login. This will lock the driver from performing another Login
// until Logout is called. Therefore, any users MUST call Logout. // until Logout is called. Therefore, any users MUST call Logout.
Login(repo, email, username, password string) error Login(repo, email, username, password string) error
......
...@@ -116,6 +116,23 @@ func (d *DockerDriver) Import(path string, repo string) (string, error) { ...@@ -116,6 +116,23 @@ func (d *DockerDriver) Import(path string, repo string) (string, error) {
return strings.TrimSpace(stdout.String()), nil 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 { func (d *DockerDriver) Login(repo, email, user, pass string) error {
d.l.Lock() d.l.Lock()
......
...@@ -23,6 +23,11 @@ type MockDriver struct { ...@@ -23,6 +23,11 @@ type MockDriver struct {
ImportId string ImportId string
ImportErr error ImportErr error
IPAddressCalled bool
IPAddressID string
IPAddressResult string
IPAddressErr error
LoginCalled bool LoginCalled bool
LoginEmail string LoginEmail string
LoginUsername string LoginUsername string
...@@ -104,6 +109,12 @@ func (d *MockDriver) Import(path, repo string) (string, error) { ...@@ -104,6 +109,12 @@ func (d *MockDriver) Import(path, repo string) (string, error) {
return d.ImportId, d.ImportErr 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 { func (d *MockDriver) Login(r, e, u, p string) error {
d.LoginCalled = true d.LoginCalled = true
d.LoginRepo = r d.LoginRepo = r
......
...@@ -2,12 +2,11 @@ package docker ...@@ -2,12 +2,11 @@ package docker
import ( import (
"github.com/mitchellh/multistep" "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) containerId := state.Get("container_id").(string)
driver := state.Get("driver").(Driver) driver := state.Get("driver").(Driver)
tempDir := state.Get("temp_dir").(string) tempDir := state.Get("temp_dir").(string)
...@@ -28,8 +27,8 @@ func (s *StepProvision) Run(state multistep.StateBag) multistep.StepAction { ...@@ -28,8 +27,8 @@ func (s *StepProvision) Run(state multistep.StateBag) multistep.StepAction {
Version: version, Version: version,
} }
prov := common.StepProvision{Comm: comm} state.Put("communicator", comm)
return prov.Run(state) return multistep.ActionContinue
} }
func (s *StepProvision) Cleanup(state multistep.StateBag) {} func (s *StepConnectDocker) Cleanup(state multistep.StateBag) {}
...@@ -4,11 +4,12 @@ package googlecompute ...@@ -4,11 +4,12 @@ package googlecompute
import ( import (
"fmt" "fmt"
"log"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"log"
"time"
) )
// The unique ID for this builder. // The unique ID for this builder.
...@@ -60,10 +61,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -60,10 +61,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&StepInstanceInfo{ &StepInstanceInfo{
Debug: b.config.PackerDebug, Debug: b.config.PackerDebug,
}, },
&common.StepConnectSSH{ &communicator.StepConnect{
SSHAddress: sshAddress, Config: &b.config.Comm,
SSHConfig: sshConfig, Host: commHost,
SSHWaitTimeout: 5 * time.Minute, SSHConfig: sshConfig,
}, },
new(common.StepProvision), new(common.StepProvision),
new(StepTeardownInstance), new(StepTeardownInstance),
......
...@@ -7,6 +7,7 @@ import ( ...@@ -7,6 +7,7 @@ import (
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/common/uuid" "github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config" "github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
...@@ -17,6 +18,7 @@ import ( ...@@ -17,6 +18,7 @@ import (
// state of the config object. // state of the config object.
type Config struct { type Config struct {
common.PackerConfig `mapstructure:",squash"` common.PackerConfig `mapstructure:",squash"`
Comm communicator.Config `mapstructure:",squash"`
AccountFile string `mapstructure:"account_file"` AccountFile string `mapstructure:"account_file"`
ProjectId string `mapstructure:"project_id"` ProjectId string `mapstructure:"project_id"`
...@@ -31,23 +33,19 @@ type Config struct { ...@@ -31,23 +33,19 @@ type Config struct {
Network string `mapstructure:"network"` Network string `mapstructure:"network"`
SourceImage string `mapstructure:"source_image"` SourceImage string `mapstructure:"source_image"`
SourceImageProjectId string `mapstructure:"source_image_project_id"` 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"` RawStateTimeout string `mapstructure:"state_timeout"`
Tags []string `mapstructure:"tags"` Tags []string `mapstructure:"tags"`
Zone string `mapstructure:"zone"` Zone string `mapstructure:"zone"`
account accountFile account accountFile
privateKeyBytes []byte privateKeyBytes []byte
sshTimeout time.Duration
stateTimeout time.Duration stateTimeout time.Duration
ctx *interpolate.Context ctx *interpolate.Context
} }
func NewConfig(raws ...interface{}) (*Config, []string, error) { func NewConfig(raws ...interface{}) (*Config, []string, error) {
c := new(Config) c := new(Config)
err := config.Decode(&c, &config.DecodeOpts{ err := config.Decode(c, &config.DecodeOpts{
Interpolate: true, Interpolate: true,
InterpolateFilter: &interpolate.RenderFilter{ InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{ Exclude: []string{
...@@ -88,20 +86,12 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { ...@@ -88,20 +86,12 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
c.MachineType = "n1-standard-1" c.MachineType = "n1-standard-1"
} }
if c.RawSSHTimeout == "" {
c.RawSSHTimeout = "5m"
}
if c.RawStateTimeout == "" { if c.RawStateTimeout == "" {
c.RawStateTimeout = "5m" c.RawStateTimeout = "5m"
} }
if c.SSHUsername == "" { if c.Comm.SSHUsername == "" {
c.SSHUsername = "root" c.Comm.SSHUsername = "root"
}
if c.SSHPort == 0 {
c.SSHPort = 22
} }
var errs *packer.MultiError var errs *packer.MultiError
...@@ -122,14 +112,6 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { ...@@ -122,14 +112,6 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
errs, errors.New("a zone must be specified")) 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) stateTimeout, err := time.ParseDuration(c.RawStateTimeout)
if err != nil { if err != nil {
errs = packer.MultiErrorAppend( errs = packer.MultiErrorAppend(
......
package googlecompute package googlecompute
import ( import (
"golang.org/x/crypto/ssh"
"fmt" "fmt"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"golang.org/x/crypto/ssh"
) )
// sshAddress returns the ssh address. func commHost(state multistep.StateBag) (string, error) {
func sshAddress(state multistep.StateBag) (string, error) {
config := state.Get("config").(*Config)
ipAddress := state.Get("instance_ip").(string) ipAddress := state.Get("instance_ip").(string)
return fmt.Sprintf("%s:%d", ipAddress, config.SSHPort), nil return ipAddress, nil
} }
// sshConfig returns the ssh configuration. // sshConfig returns the ssh configuration.
...@@ -24,7 +23,7 @@ func sshConfig(state multistep.StateBag) (*ssh.ClientConfig, error) { ...@@ -24,7 +23,7 @@ func sshConfig(state multistep.StateBag) (*ssh.ClientConfig, error) {
} }
return &ssh.ClientConfig{ return &ssh.ClientConfig{
User: config.SSHUsername, User: config.Comm.SSHUsername,
Auth: []ssh.AuthMethod{ Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer), ssh.PublicKeys(signer),
}, },
......
...@@ -32,7 +32,7 @@ func (config *Config) getInstanceMetadata(sshPublicKey string) map[string]string ...@@ -32,7 +32,7 @@ func (config *Config) getInstanceMetadata(sshPublicKey string) map[string]string
// Merge any existing ssh keys with our public key // Merge any existing ssh keys with our public key
sshMetaKey := "sshKeys" 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 { if confSshKeys, exists := instanceMetadata[sshMetaKey]; exists {
sshKeys = fmt.Sprintf("%s\n%s", sshKeys, confSshKeys) sshKeys = fmt.Sprintf("%s\n%s", sshKeys, confSshKeys)
} }
......
package null package null
import ( import (
"log"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"log"
"time"
) )
const BuilderId = "fnoeding.null" const BuilderId = "fnoeding.null"
...@@ -27,10 +28,13 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { ...@@ -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) { func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
steps := []multistep.Step{ steps := []multistep.Step{
&common.StepConnectSSH{ &communicator.StepConnect{
SSHAddress: SSHAddress(b.config.Host, b.config.Port), Config: &b.config.CommConfig,
SSHConfig: SSHConfig(b.config.SSHUsername, b.config.SSHPassword, b.config.SSHPrivateKeyFile), Host: CommHost(b.config.CommConfig.SSHHost),
SSHWaitTimeout: 1 * time.Minute, SSHConfig: SSHConfig(
b.config.CommConfig.SSHUsername,
b.config.CommConfig.SSHPassword,
b.config.CommConfig.SSHPrivateKey),
}, },
&common.StepProvision{}, &common.StepProvision{},
} }
......
...@@ -2,7 +2,9 @@ package null ...@@ -2,7 +2,9 @@ package null
import ( import (
"fmt" "fmt"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/helper/config" "github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
...@@ -11,49 +13,40 @@ import ( ...@@ -11,49 +13,40 @@ import (
type Config struct { type Config struct {
common.PackerConfig `mapstructure:",squash"` common.PackerConfig `mapstructure:",squash"`
Host string `mapstructure:"host"` CommConfig communicator.Config `mapstructure:",squash"`
Port int `mapstructure:"port"`
SSHUsername string `mapstructure:"ssh_username"`
SSHPassword string `mapstructure:"ssh_password"`
SSHPrivateKeyFile string `mapstructure:"ssh_private_key_file"`
} }
func NewConfig(raws ...interface{}) (*Config, []string, error) { func NewConfig(raws ...interface{}) (*Config, []string, error) {
c := new(Config) var c Config
err := config.Decode(&c, &config.DecodeOpts{ err := config.Decode(&c, &config.DecodeOpts{
Interpolate: true, Interpolate: true,
InterpolateFilter: &interpolate.RenderFilter{ InterpolateFilter: &interpolate.RenderFilter{},
Exclude: []string{
"run_command",
},
},
}, raws...) }, raws...)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
if c.Port == 0 {
c.Port = 22
}
var errs *packer.MultiError 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, errs = packer.MultiErrorAppend(errs,
fmt.Errorf("host must be specified")) fmt.Errorf("host must be specified"))
} }
if c.SSHUsername == "" { if c.CommConfig.SSHUsername == "" {
errs = packer.MultiErrorAppend(errs, errs = packer.MultiErrorAppend(errs,
fmt.Errorf("ssh_username must be specified")) fmt.Errorf("ssh_username must be specified"))
} }
if c.SSHPassword == "" && c.SSHPrivateKeyFile == "" { if c.CommConfig.SSHPassword == "" && c.CommConfig.SSHPrivateKey == "" {
errs = packer.MultiErrorAppend(errs, errs = packer.MultiErrorAppend(errs,
fmt.Errorf("one of ssh_password and ssh_private_key_file must be specified")) 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, errs = packer.MultiErrorAppend(errs,
fmt.Errorf("only one of ssh_password and ssh_private_key_file must be specified")) 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) { ...@@ -62,5 +55,5 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
return nil, nil, errs return nil, nil, errs
} }
return c, nil, nil return &c, nil, nil
} }
package null package null
import ( import (
"os"
"testing" "testing"
"github.com/mitchellh/packer/helper/communicator"
) )
func testConfig() map[string]interface{} { func testConfig() map[string]interface{} {
return map[string]interface{}{ return map[string]interface{}{
"host": "foo", "ssh_host": "foo",
"ssh_username": "bar", "ssh_username": "bar",
"ssh_password": "baz", "ssh_password": "baz",
} }
...@@ -48,8 +51,8 @@ func TestConfigPrepare_port(t *testing.T) { ...@@ -48,8 +51,8 @@ func TestConfigPrepare_port(t *testing.T) {
// default port should be 22 // default port should be 22
delete(raw, "port") delete(raw, "port")
c, warns, errs := NewConfig(raw) c, warns, errs := NewConfig(raw)
if c.Port != 22 { if c.CommConfig.SSHPort != 22 {
t.Fatalf("bad: port should default to 22, not %d", c.Port) t.Fatalf("bad: port should default to 22, not %d", c.CommConfig.SSHPort)
} }
testConfigOk(t, warns, errs) testConfigOk(t, warns, errs)
} }
...@@ -58,12 +61,12 @@ func TestConfigPrepare_host(t *testing.T) { ...@@ -58,12 +61,12 @@ func TestConfigPrepare_host(t *testing.T) {
raw := testConfig() raw := testConfig()
// No host // No host
delete(raw, "host") delete(raw, "ssh_host")
_, warns, errs := NewConfig(raw) _, warns, errs := NewConfig(raw)
testConfigErr(t, warns, errs) testConfigErr(t, warns, errs)
// Good host // Good host
raw["host"] = "good" raw["ssh_host"] = "good"
_, warns, errs = NewConfig(raw) _, warns, errs = NewConfig(raw)
testConfigOk(t, warns, errs) testConfigOk(t, warns, errs)
} }
...@@ -97,7 +100,9 @@ func TestConfigPrepare_sshCredential(t *testing.T) { ...@@ -97,7 +100,9 @@ func TestConfigPrepare_sshCredential(t *testing.T) {
testConfigOk(t, warns, errs) testConfigOk(t, warns, errs)
// only ssh_private_key_file // 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") delete(raw, "ssh_password")
_, warns, errs = NewConfig(raw) _, warns, errs = NewConfig(raw)
testConfigOk(t, warns, errs) testConfigOk(t, warns, errs)
......
package null package null
import ( import (
gossh "golang.org/x/crypto/ssh"
"fmt" "fmt"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/communicator/ssh" "github.com/mitchellh/packer/communicator/ssh"
gossh "golang.org/x/crypto/ssh"
"io/ioutil" "io/ioutil"
) )
// SSHAddress returns a function that can be given to the SSH communicator func CommHost(host string) func(multistep.StateBag) (string, error) {
// for determining the SSH address
func SSHAddress(host string, port int) func(multistep.StateBag) (string, error) {
return func(state multistep.StateBag) (string, error) { return func(state multistep.StateBag) (string, error) {
return fmt.Sprintf("%s:%d", host, port), nil return host, nil
} }
} }
......
...@@ -4,99 +4,120 @@ import ( ...@@ -4,99 +4,120 @@ import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"os" "os"
"strings"
"github.com/mitchellh/gophercloud-fork-40444fb"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack"
) )
// AccessConfig is for common configuration related to openstack access // AccessConfig is for common configuration related to openstack access
type AccessConfig struct { type AccessConfig struct {
Username string `mapstructure:"username"` Username string `mapstructure:"username"`
Password string `mapstructure:"password"` UserID string `mapstructure:"user_id"`
ApiKey string `mapstructure:"api_key"` Password string `mapstructure:"password"`
Project string `mapstructure:"project"` APIKey string `mapstructure:"api_key"`
Provider string `mapstructure:"provider"` IdentityEndpoint string `mapstructure:"identity_endpoint"`
RawRegion string `mapstructure:"region"` TenantID string `mapstructure:"tenant_id"`
ProxyUrl string `mapstructure:"proxy_url"` TenantName string `mapstructure:"tenant_name"`
TenantId string `mapstructure:"tenant_id"` DomainID string `mapstructure:"domain_id"`
Insecure bool `mapstructure:"insecure"` DomainName string `mapstructure:"domain_name"`
Insecure bool `mapstructure:"insecure"`
Region string `mapstructure:"region"`
EndpointType string `mapstructure:"endpoint_type"`
osClient *gophercloud.ProviderClient
} }
// Auth returns a valid Auth object for access to openstack services, or func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
// an error if the authentication couldn't be resolved. if c.EndpointType != "internal" && c.EndpointType != "internalURL" &&
func (c *AccessConfig) Auth() (gophercloud.AccessProvider, error) { c.EndpointType != "admin" && c.EndpointType != "adminURL" &&
c.Username = common.ChooseString(c.Username, os.Getenv("SDK_USERNAME"), os.Getenv("OS_USERNAME")) c.EndpointType != "public" && c.EndpointType != "publicURL" &&
c.Password = common.ChooseString(c.Password, os.Getenv("SDK_PASSWORD"), os.Getenv("OS_PASSWORD")) c.EndpointType != "" {
c.ApiKey = common.ChooseString(c.ApiKey, os.Getenv("SDK_API_KEY")) return []error{fmt.Errorf("Invalid endpoint type provided")}
c.Project = common.ChooseString(c.Project, os.Getenv("SDK_PROJECT"), os.Getenv("OS_TENANT_NAME"))
c.Provider = common.ChooseString(c.Provider, os.Getenv("SDK_PROVIDER"), os.Getenv("OS_AUTH_URL"))
c.RawRegion = common.ChooseString(c.RawRegion, os.Getenv("SDK_REGION"), os.Getenv("OS_REGION_NAME"))
c.TenantId = common.ChooseString(c.TenantId, os.Getenv("OS_TENANT_ID"))
// OpenStack's auto-generated openrc.sh files do not append the suffix
// /tokens to the authentication URL. This ensures it is present when
// specifying the URL.
if strings.Contains(c.Provider, "://") && !strings.HasSuffix(c.Provider, "/tokens") {
c.Provider += "/tokens"
} }
authoptions := gophercloud.AuthOptions{ if c.Region == "" {
AllowReauth: true, c.Region = os.Getenv("OS_REGION_NAME")
ApiKey: c.ApiKey,
TenantId: c.TenantId,
TenantName: c.Project,
Username: c.Username,
Password: c.Password,
} }
default_transport := &http.Transport{} // Legacy RackSpace stuff. We're keeping this around to keep things BC.
if c.APIKey == "" {
if c.Insecure { c.APIKey = os.Getenv("SDK_API_KEY")
cfg := new(tls.Config) }
cfg.InsecureSkipVerify = true if c.Password == "" {
default_transport.TLSClientConfig = cfg c.Password = os.Getenv("SDK_PASSWORD")
}
if c.Region == "" {
c.Region = os.Getenv("SDK_REGION")
}
if c.TenantName == "" {
c.TenantName = os.Getenv("SDK_PROJECT")
}
if c.Username == "" {
c.Username = os.Getenv("SDK_USERNAME")
} }
// For corporate networks it may be the case where we want our API calls // Get as much as possible from the end
// to be sent through a separate HTTP proxy than external traffic. ao, _ := openstack.AuthOptionsFromEnv()
if c.ProxyUrl != "" {
url, err := url.Parse(c.ProxyUrl) // Override values if we have them in our config
if err != nil { overrides := []struct {
return nil, err From, To *string
}{
{&c.Username, &ao.Username},
{&c.UserID, &ao.UserID},
{&c.Password, &ao.Password},
{&c.APIKey, &ao.APIKey},
{&c.IdentityEndpoint, &ao.IdentityEndpoint},
{&c.TenantID, &ao.TenantID},
{&c.TenantName, &ao.TenantName},
{&c.DomainID, &ao.DomainID},
{&c.DomainName, &ao.DomainName},
}
for _, s := range overrides {
if *s.From != "" {
*s.To = *s.From
} }
}
// The gophercloud.Context has a UseCustomClient method which // Build the client itself
// would allow us to override with a new instance of http.Client. client, err := openstack.NewClient(ao.IdentityEndpoint)
default_transport.Proxy = http.ProxyURL(url) if err != nil {
return []error{err}
} }
if c.Insecure || c.ProxyUrl != "" { // If we have insecure set, then create a custom HTTP client that
http.DefaultTransport = default_transport // ignores SSL errors.
if c.Insecure {
config := &tls.Config{InsecureSkipVerify: true}
transport := &http.Transport{TLSClientConfig: config}
client.HTTPClient.Transport = transport
} }
return gophercloud.Authenticate(c.Provider, authoptions) // Auth
err = openstack.Authenticate(client, ao)
if err != nil {
return []error{err}
}
c.osClient = client
return nil
} }
func (c *AccessConfig) Region() string { func (c *AccessConfig) computeV2Client() (*gophercloud.ServiceClient, error) {
return common.ChooseString(c.RawRegion, os.Getenv("SDK_REGION"), os.Getenv("OS_REGION_NAME")) return openstack.NewComputeV2(c.osClient, gophercloud.EndpointOpts{
Region: c.Region,
Availability: c.getEndpointType(),
})
} }
func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error { func (c *AccessConfig) getEndpointType() gophercloud.Availability {
errs := make([]error, 0) if c.EndpointType == "internal" || c.EndpointType == "internalURL" {
if strings.HasPrefix(c.Provider, "rackspace") { return gophercloud.AvailabilityInternal
if c.Region() == "" {
errs = append(errs, fmt.Errorf("region must be specified when using rackspace"))
}
} }
if c.EndpointType == "admin" || c.EndpointType == "adminURL" {
if len(errs) > 0 { return gophercloud.AvailabilityAdmin
return errs
} }
return gophercloud.AvailabilityPublic
return nil
} }
package openstack
import (
"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())
}
}
...@@ -4,7 +4,8 @@ import ( ...@@ -4,7 +4,8 @@ import (
"fmt" "fmt"
"log" "log"
"github.com/mitchellh/gophercloud-fork-40444fb" "github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/compute/v2/images"
) )
// Artifact is an artifact implementation that contains built images. // Artifact is an artifact implementation that contains built images.
...@@ -16,7 +17,7 @@ type Artifact struct { ...@@ -16,7 +17,7 @@ type Artifact struct {
BuilderIdValue string BuilderIdValue string
// OpenStack connection for performing API stuff. // OpenStack connection for performing API stuff.
Conn gophercloud.CloudServersProvider Client *gophercloud.ServiceClient
} }
func (a *Artifact) BuilderId() string { func (a *Artifact) BuilderId() string {
...@@ -42,5 +43,5 @@ func (a *Artifact) State(name string) interface{} { ...@@ -42,5 +43,5 @@ func (a *Artifact) State(name string) interface{} {
func (a *Artifact) Destroy() error { func (a *Artifact) Destroy() error {
log.Printf("Destroying image: %s", a.ImageId) log.Printf("Destroying image: %s", a.ImageId)
return a.Conn.DeleteImageById(a.ImageId) return images.Delete(a.Client, a.ImageId).ExtractErr()
} }
...@@ -5,11 +5,11 @@ package openstack ...@@ -5,11 +5,11 @@ package openstack
import ( import (
"fmt" "fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"log" "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/helper/config"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
...@@ -20,9 +20,10 @@ const BuilderId = "mitchellh.openstack" ...@@ -20,9 +20,10 @@ const BuilderId = "mitchellh.openstack"
type Config struct { type Config struct {
common.PackerConfig `mapstructure:",squash"` common.PackerConfig `mapstructure:",squash"`
AccessConfig `mapstructure:",squash"`
ImageConfig `mapstructure:",squash"` AccessConfig `mapstructure:",squash"`
RunConfig `mapstructure:",squash"` ImageConfig `mapstructure:",squash"`
RunConfig `mapstructure:",squash"`
ctx interpolate.Context ctx interpolate.Context
} }
...@@ -55,43 +56,35 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { ...@@ -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) { 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 { if err != nil {
return nil, err return nil, fmt.Errorf("Error initializing compute client: %s", err)
}
api.Region = b.config.AccessConfig.Region()
csp, err := gophercloud.ServersApi(auth, api)
if err != nil {
log.Printf("Region: %s", b.config.AccessConfig.Region())
return nil, err
} }
// Setup the state bag and initial state for the steps // Setup the state bag and initial state for the steps
state := new(multistep.BasicStateBag) state := new(multistep.BasicStateBag)
state.Put("config", b.config) state.Put("config", b.config)
state.Put("csp", csp)
state.Put("hook", hook) state.Put("hook", hook)
state.Put("ui", ui) state.Put("ui", ui)
// Build the steps // Build the steps
steps := []multistep.Step{ steps := []multistep.Step{
&StepLoadExtensions{},
&StepLoadFlavor{
Flavor: b.config.Flavor,
},
&StepKeyPair{ &StepKeyPair{
Debug: b.config.PackerDebug, Debug: b.config.PackerDebug,
DebugKeyPath: fmt.Sprintf("os_%s.pem", b.config.PackerBuildName), DebugKeyPath: fmt.Sprintf("os_%s.pem", b.config.PackerBuildName),
}, },
&StepRunSourceServer{ &StepRunSourceServer{
Name: b.config.ImageName, Name: b.config.ImageName,
Flavor: b.config.Flavor, SourceImage: b.config.SourceImage,
SourceImage: b.config.SourceImage, SecurityGroups: b.config.SecurityGroups,
SecurityGroups: b.config.SecurityGroups, Networks: b.config.Networks,
Networks: b.config.Networks, AvailabilityZone: b.config.AvailabilityZone,
UserData: b.config.UserData,
UserDataFile: b.config.UserDataFile,
}, },
&StepWaitForRackConnect{ &StepWaitForRackConnect{
Wait: b.config.RackconnectWait, Wait: b.config.RackconnectWait,
...@@ -100,12 +93,15 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -100,12 +93,15 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
FloatingIpPool: b.config.FloatingIpPool, FloatingIpPool: b.config.FloatingIpPool,
FloatingIp: b.config.FloatingIp, FloatingIp: b.config.FloatingIp,
}, },
&common.StepConnectSSH{ &communicator.StepConnect{
SSHAddress: SSHAddress(csp, b.config.SSHInterface, b.config.SSHPort), Config: &b.config.RunConfig.Comm,
SSHConfig: SSHConfig(b.config.SSHUsername), Host: CommHost(
SSHWaitTimeout: b.config.SSHTimeout(), computeClient,
b.config.SSHInterface),
SSHConfig: SSHConfig(b.config.RunConfig.Comm.SSHUsername),
}, },
&common.StepProvision{}, &common.StepProvision{},
&StepStopServer{},
&stepCreateImage{}, &stepCreateImage{},
} }
...@@ -135,7 +131,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -135,7 +131,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
artifact := &Artifact{ artifact := &Artifact{
ImageId: state.Get("image").(string), ImageId: state.Get("image").(string),
BuilderIdValue: BuilderId, BuilderIdValue: BuilderId,
Conn: csp, Client: computeClient,
} }
return artifact, nil return artifact, nil
......
...@@ -9,7 +9,6 @@ func testConfig() map[string]interface{} { ...@@ -9,7 +9,6 @@ func testConfig() map[string]interface{} {
return map[string]interface{}{ return map[string]interface{}{
"username": "foo", "username": "foo",
"password": "bar", "password": "bar",
"provider": "foo",
"region": "DFW", "region": "DFW",
"image_name": "foo", "image_name": "foo",
"source_image": "foo", "source_image": "foo",
...@@ -40,55 +39,3 @@ func TestBuilder_Prepare_BadType(t *testing.T) { ...@@ -40,55 +39,3 @@ func TestBuilder_Prepare_BadType(t *testing.T) {
t.Fatalf("prepare should fail") t.Fatalf("prepare should fail")
} }
} }
func TestBuilderPrepare_ImageName(t *testing.T) {
var b Builder
config := testConfig()
// Test good
config["image_name"] = "foo"
warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test bad
config["image_name"] = "foo {{"
b = Builder{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Test bad
delete(config, "image_name")
b = Builder{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_InvalidKey(t *testing.T) {
var b Builder
config := testConfig()
// Add a random key
config["i_should_not_be_valid"] = true
warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
}
...@@ -2,45 +2,37 @@ package openstack ...@@ -2,45 +2,37 @@ package openstack
import ( import (
"errors" "errors"
"fmt"
"time"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/template/interpolate" "github.com/mitchellh/packer/template/interpolate"
) )
// RunConfig contains configuration for running an instance from a source // RunConfig contains configuration for running an instance from a source
// image and details on how to access that launched image. // image and details on how to access that launched image.
type RunConfig struct { type RunConfig struct {
SourceImage string `mapstructure:"source_image"` Comm communicator.Config `mapstructure:",squash"`
Flavor string `mapstructure:"flavor"` SSHInterface string `mapstructure:"ssh_interface"`
RawSSHTimeout string `mapstructure:"ssh_timeout"`
SSHUsername string `mapstructure:"ssh_username"` SourceImage string `mapstructure:"source_image"`
SSHPort int `mapstructure:"ssh_port"` Flavor string `mapstructure:"flavor"`
SSHInterface string `mapstructure:"ssh_interface"` AvailabilityZone string `mapstructure:"availability_zone"`
OpenstackProvider string `mapstructure:"openstack_provider"` RackconnectWait bool `mapstructure:"rackconnect_wait"`
UseFloatingIp bool `mapstructure:"use_floating_ip"` FloatingIpPool string `mapstructure:"floating_ip_pool"`
RackconnectWait bool `mapstructure:"rackconnect_wait"` FloatingIp string `mapstructure:"floating_ip"`
FloatingIpPool string `mapstructure:"floating_ip_pool"` SecurityGroups []string `mapstructure:"security_groups"`
FloatingIp string `mapstructure:"floating_ip"` Networks []string `mapstructure:"networks"`
SecurityGroups []string `mapstructure:"security_groups"` UserData string `mapstructure:"user_data"`
Networks []string `mapstructure:"networks"` UserDataFile string `mapstructure:"user_data_file"`
// Unexported fields that are calculated from others // Not really used, but here for BC
sshTimeout time.Duration OpenstackProvider string `mapstructure:"openstack_provider"`
UseFloatingIp bool `mapstructure:"use_floating_ip"`
} }
func (c *RunConfig) Prepare(ctx *interpolate.Context) []error { func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
// Defaults // Defaults
if c.SSHUsername == "" { if c.Comm.SSHUsername == "" {
c.SSHUsername = "root" c.Comm.SSHUsername = "root"
}
if c.SSHPort == 0 {
c.SSHPort = 22
}
if c.RawSSHTimeout == "" {
c.RawSSHTimeout = "5m"
} }
if c.UseFloatingIp && c.FloatingIpPool == "" { if c.UseFloatingIp && c.FloatingIpPool == "" {
...@@ -48,8 +40,7 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error { ...@@ -48,8 +40,7 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
} }
// Validation // Validation
var err error errs := c.Comm.Prepare(ctx)
errs := make([]error, 0)
if c.SourceImage == "" { if c.SourceImage == "" {
errs = append(errs, errors.New("A source_image must be specified")) errs = append(errs, errors.New("A source_image must be specified"))
} }
...@@ -58,18 +49,5 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error { ...@@ -58,18 +49,5 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
errs = append(errs, errors.New("A flavor must be specified")) 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 return errs
} }
func (c *RunConfig) SSHTimeout() time.Duration {
return c.sshTimeout
}
...@@ -3,6 +3,8 @@ package openstack ...@@ -3,6 +3,8 @@ package openstack
import ( import (
"os" "os"
"testing" "testing"
"github.com/mitchellh/packer/helper/communicator"
) )
func init() { func init() {
...@@ -17,7 +19,10 @@ func testRunConfig() *RunConfig { ...@@ -17,7 +19,10 @@ func testRunConfig() *RunConfig {
return &RunConfig{ return &RunConfig{
SourceImage: "abcd", SourceImage: "abcd",
Flavor: "m1.small", Flavor: "m1.small",
SSHUsername: "root",
Comm: communicator.Config{
SSHUsername: "foo",
},
} }
} }
...@@ -47,41 +52,28 @@ func TestRunConfigPrepare_SourceImage(t *testing.T) { ...@@ -47,41 +52,28 @@ func TestRunConfigPrepare_SourceImage(t *testing.T) {
func TestRunConfigPrepare_SSHPort(t *testing.T) { func TestRunConfigPrepare_SSHPort(t *testing.T) {
c := testRunConfig() 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)
}
c.SSHPort = 44
if err := c.Prepare(nil); len(err) != 0 { if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
if c.SSHPort != 44 { if c.Comm.SSHPort != 22 {
t.Fatalf("invalid value: %d", c.SSHPort) t.Fatalf("invalid value: %d", c.Comm.SSHPort)
} }
}
func TestRunConfigPrepare_SSHTimeout(t *testing.T) { c.Comm.SSHPort = 44
c := testRunConfig()
c.RawSSHTimeout = ""
if err := c.Prepare(nil); len(err) != 0 { if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
c.RawSSHTimeout = "bad" if c.Comm.SSHPort != 44 {
if err := c.Prepare(nil); len(err) != 1 { t.Fatalf("invalid value: %d", c.Comm.SSHPort)
t.Fatalf("err: %s", err)
} }
} }
func TestRunConfigPrepare_SSHUsername(t *testing.T) { func TestRunConfigPrepare_SSHUsername(t *testing.T) {
c := testRunConfig() c := testRunConfig()
c.SSHUsername = "" c.Comm.SSHUsername = ""
if err := c.Prepare(nil); len(err) != 0 { if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
......
...@@ -3,12 +3,12 @@ package openstack ...@@ -3,12 +3,12 @@ package openstack
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/mitchellh/multistep"
"github.com/racker/perigee"
"log" "log"
"time" "time"
"github.com/mitchellh/gophercloud-fork-40444fb" "github.com/mitchellh/multistep"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
) )
// StateRefreshFunc is a function type used for StateChangeConf that is // StateRefreshFunc is a function type used for StateChangeConf that is
...@@ -28,26 +28,27 @@ type StateChangeConf struct { ...@@ -28,26 +28,27 @@ type StateChangeConf struct {
Pending []string Pending []string
Refresh StateRefreshFunc Refresh StateRefreshFunc
StepState multistep.StateBag StepState multistep.StateBag
Target string Target []string
} }
// ServerStateRefreshFunc returns a StateRefreshFunc that is used to watch // ServerStateRefreshFunc returns a StateRefreshFunc that is used to watch
// an openstack server. // an openstack server.
func ServerStateRefreshFunc(csp gophercloud.CloudServersProvider, s *gophercloud.Server) StateRefreshFunc { func ServerStateRefreshFunc(
client *gophercloud.ServiceClient, s *servers.Server) StateRefreshFunc {
return func() (interface{}, string, int, error) { return func() (interface{}, string, int, error) {
resp, err := csp.ServerById(s.Id) serverNew, err := servers.Get(client, s.ID).Extract()
if err != nil { if err != nil {
urce, ok := err.(*perigee.UnexpectedResponseCodeError) errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError)
if ok && (urce.Actual == 404) { if ok && errCode.Actual == 404 {
log.Printf("404 on ServerStateRefresh, returning DELETED") log.Printf("[INFO] 404 on ServerStateRefresh, returning DELETED")
return nil, "DELETED", 0, nil return nil, "DELETED", 0, nil
} else { } else {
log.Printf("Error on ServerStateRefresh: %s", err) log.Printf("[ERROR] Error on ServerStateRefresh: %s", err)
return nil, "", 0, err return nil, "", 0, err
} }
} }
return resp, resp.Status, resp.Progress, nil
return serverNew, serverNew.Status, serverNew.Progress, nil
} }
} }
...@@ -64,8 +65,10 @@ func WaitForState(conf *StateChangeConf) (i interface{}, err error) { ...@@ -64,8 +65,10 @@ func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
return return
} }
if currentState == conf.Target { for _, t := range conf.Target {
return if currentState == t {
return
}
} }
if conf.StepState != nil { if conf.StepState != nil {
......
package openstack package openstack
import ( import (
"golang.org/x/crypto/ssh"
"errors" "errors"
"fmt" "fmt"
"github.com/mitchellh/multistep" "log"
"time" "time"
"github.com/mitchellh/gophercloud-fork-40444fb" "github.com/mitchellh/multistep"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
"golang.org/x/crypto/ssh"
) )
// SSHAddress returns a function that can be given to the SSH communicator // CommHost looks up the host for the communicator.
// for determining the SSH address based on the server AccessIPv4 setting.. func CommHost(
func SSHAddress(csp gophercloud.CloudServersProvider, sshinterface string, port int) func(multistep.StateBag) (string, error) { client *gophercloud.ServiceClient,
sshinterface string) func(multistep.StateBag) (string, error) {
return func(state multistep.StateBag) (string, error) { return func(state multistep.StateBag) (string, error) {
s := state.Get("server").(*gophercloud.Server) s := state.Get("server").(*servers.Server)
if ip := state.Get("access_ip").(gophercloud.FloatingIp); ip.Ip != "" { // If we have a specific interface, try that
return fmt.Sprintf("%s:%d", ip.Ip, port), nil if sshinterface != "" {
if addr := sshAddrFromPool(s, sshinterface); addr != "" {
return addr, nil
}
} }
ip_pools, err := s.AllAddressPools() // If we have a floating IP, use that
if err != nil { ip := state.Get("access_ip").(*floatingip.FloatingIP)
return "", errors.New("Error parsing SSH addresses") if ip != nil && ip.IP != "" {
return ip.IP, nil
} }
for pool, addresses := range ip_pools {
if sshinterface != "" { if s.AccessIPv4 != "" {
if pool != sshinterface { return s.AccessIPv4, nil
continue
}
}
if pool != "" {
for _, address := range addresses {
if address.Addr != "" && address.Version == 4 {
return fmt.Sprintf("%s:%d", address.Addr, port), nil
}
}
}
} }
serverState, err := csp.ServerById(s.Id) // 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 { if err != nil {
return "", err return "", err
} }
state.Put("server", serverState) state.Put("server", s)
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
return "", errors.New("couldn't determine IP address for server") return "", errors.New("couldn't determine IP address for server")
...@@ -72,3 +74,42 @@ func SSHConfig(username string) func(multistep.StateBag) (*ssh.ClientConfig, err ...@@ -72,3 +74,42 @@ func SSHConfig(username string) func(multistep.StateBag) (*ssh.ClientConfig, err
}, nil }, 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 ""
}
...@@ -2,10 +2,11 @@ package openstack ...@@ -2,10 +2,11 @@ package openstack
import ( import (
"fmt" "fmt"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip"
"github.com/mitchellh/gophercloud-fork-40444fb" "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
) )
type StepAllocateIp struct { type StepAllocateIp struct {
...@@ -15,53 +16,83 @@ type StepAllocateIp struct { ...@@ -15,53 +16,83 @@ type StepAllocateIp struct {
func (s *StepAllocateIp) Run(state multistep.StateBag) multistep.StepAction { func (s *StepAllocateIp) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
csp := state.Get("csp").(gophercloud.CloudServersProvider) config := state.Get("config").(Config)
server := state.Get("server").(*gophercloud.Server) server := state.Get("server").(*servers.Server)
// We need the v2 compute client
client, err := config.computeV2Client()
if err != nil {
err = fmt.Errorf("Error initializing compute client: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
var instanceIp floatingip.FloatingIP
var instanceIp gophercloud.FloatingIp
// This is here in case we error out before putting instanceIp into the // This is here in case we error out before putting instanceIp into the
// statebag below, because it is requested by Cleanup() // statebag below, because it is requested by Cleanup()
state.Put("access_ip", instanceIp) state.Put("access_ip", &instanceIp)
if s.FloatingIp != "" { if s.FloatingIp != "" {
instanceIp.Ip = s.FloatingIp instanceIp.IP = s.FloatingIp
} else if s.FloatingIpPool != "" { } else if s.FloatingIpPool != "" {
newIp, err := csp.CreateFloatingIp(s.FloatingIpPool) ui.Say(fmt.Sprintf("Creating floating IP..."))
ui.Message(fmt.Sprintf("Pool: %s", s.FloatingIpPool))
newIp, err := floatingip.Create(client, floatingip.CreateOpts{
Pool: s.FloatingIpPool,
}).Extract()
if err != nil { if err != nil {
err := fmt.Errorf("Error creating floating ip from pool '%s'", s.FloatingIpPool) err := fmt.Errorf("Error creating floating ip from pool '%s'", s.FloatingIpPool)
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
} }
instanceIp = newIp
ui.Say(fmt.Sprintf("Created temporary floating IP %s...", instanceIp.Ip)) instanceIp = *newIp
ui.Message(fmt.Sprintf("Created floating IP: %s", instanceIp.IP))
} }
if instanceIp.Ip != "" { if instanceIp.IP != "" {
if err := csp.AssociateFloatingIp(server.Id, instanceIp); err != nil { ui.Say(fmt.Sprintf("Associating floating IP with server..."))
err := fmt.Errorf("Error associating floating IP %s with instance.", instanceIp.Ip) ui.Message(fmt.Sprintf("IP: %s", instanceIp.IP))
err := floatingip.Associate(client, server.ID, instanceIp.IP).ExtractErr()
if err != nil {
err := fmt.Errorf(
"Error associating floating IP %s with instance: %s",
instanceIp.IP, err)
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
} else {
ui.Say(fmt.Sprintf("Added floating IP %s to instance...", instanceIp.Ip))
} }
}
state.Put("access_ip", instanceIp) ui.Message(fmt.Sprintf(
"Added floating IP %s to instance!", instanceIp.IP))
}
state.Put("access_ip", &instanceIp)
return multistep.ActionContinue return multistep.ActionContinue
} }
func (s *StepAllocateIp) Cleanup(state multistep.StateBag) { func (s *StepAllocateIp) Cleanup(state multistep.StateBag) {
config := state.Get("config").(Config)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
csp := state.Get("csp").(gophercloud.CloudServersProvider) instanceIp := state.Get("access_ip").(*floatingip.FloatingIP)
instanceIp := state.Get("access_ip").(gophercloud.FloatingIp)
if s.FloatingIpPool != "" && instanceIp.Id != 0 { // We need the v2 compute client
if err := csp.DeleteFloatingIp(instanceIp); err != nil { client, err := config.computeV2Client()
ui.Error(fmt.Sprintf("Error deleting temporary floating IP %s", instanceIp.Ip)) if err != nil {
ui.Error(fmt.Sprintf(
"Error deleting temporary floating IP %s", instanceIp.IP))
return
}
if s.FloatingIpPool != "" && instanceIp.ID != "" {
if err := floatingip.Delete(client, instanceIp.ID).ExtractErr(); err != nil {
ui.Error(fmt.Sprintf(
"Error deleting temporary floating IP %s", instanceIp.IP))
return return
} }
ui.Say(fmt.Sprintf("Deleted temporary floating IP %s", instanceIp.Ip))
ui.Say(fmt.Sprintf("Deleted temporary floating IP %s", instanceIp.IP))
} }
} }
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -73,6 +73,12 @@ func NewDriver() (Driver, error) { ...@@ -73,6 +73,12 @@ func NewDriver() (Driver, error) {
log.Printf("prlctl path: %s", prlctlPath) log.Printf("prlctl path: %s", prlctlPath)
drivers = map[string]Driver{ drivers = map[string]Driver{
"11": &Parallels10Driver{
Parallels9Driver: Parallels9Driver{
PrlctlPath: prlctlPath,
dhcp_lease_file: dhcp_lease_file,
},
},
"10": &Parallels10Driver{ "10": &Parallels10Driver{
Parallels9Driver: Parallels9Driver{ Parallels9Driver: Parallels9Driver{
PrlctlPath: prlctlPath, PrlctlPath: prlctlPath,
......
package common package common
// Parallels10Driver are inherited from Parallels9Driver. // Parallels10Driver are inherited from Parallels9Driver.
// Used for Parallels v 10 & 11
type Parallels10Driver struct { type Parallels10Driver struct {
Parallels9Driver Parallels9Driver
} }
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -33,7 +33,7 @@ type Config struct { ...@@ -33,7 +33,7 @@ type Config struct {
func NewConfig(raws ...interface{}) (*Config, []string, error) { func NewConfig(raws ...interface{}) (*Config, []string, error) {
c := new(Config) c := new(Config)
err := config.Decode(&c, &config.DecodeOpts{ err := config.Decode(c, &config.DecodeOpts{
Interpolate: true, Interpolate: true,
InterpolateFilter: &interpolate.RenderFilter{ InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{ Exclude: []string{
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -80,7 +80,8 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error ...@@ -80,7 +80,8 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error
defaultArgs["-name"] = vmName defaultArgs["-name"] = vmName
defaultArgs["-machine"] = fmt.Sprintf("type=%s", config.MachineType) 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["-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) defaultArgs["-drive"] = fmt.Sprintf("file=%s,if=%s,cache=%s,discard=%s", imgPath, config.DiskInterface, config.DiskCache, config.DiskDiscard)
if !config.DiskImage { if !config.DiskImage {
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment