commit
899cd7b845
87
CHANGELOG.md
87
CHANGELOG.md
|
@ -1,48 +1,131 @@
|
||||||
## 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)
|
||||||
|
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -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
|
||||||
|
|
||||||
// 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
|
||||||
&StepChrootProvision{},
|
&StepChrootProvision{},
|
||||||
&StepEarlyCleanup{},
|
&StepEarlyCleanup{},
|
||||||
&StepSnapshot{},
|
&StepSnapshot{},
|
||||||
|
&awscommon.StepDeregisterAMI{
|
||||||
|
ForceDeregister: b.config.AMIForceDeregister,
|
||||||
|
AMIName: b.config.AMIName,
|
||||||
|
},
|
||||||
&StepRegisterAMI{},
|
&StepRegisterAMI{},
|
||||||
&awscommon.StepAMIRegionCopy{
|
&awscommon.StepAMIRegionCopy{
|
||||||
|
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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
||||||
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 (
|
||||||
"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) {
|
||||||
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) {
|
||||||
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 (
|
||||||
"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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (
|
||||||
"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 {
|
||||||
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) {
|
||||||
|
|
||||||
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 {
|
if err := c.Prepare(nil); len(err) != 0 {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.SSHPort != 22 {
|
if c.Comm.SSHPort != 22 {
|
||||||
t.Fatalf("invalid value: %d", c.SSHPort)
|
t.Fatalf("invalid value: %d", c.Comm.SSHPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SSHPort = 44
|
c.Comm.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 != 44 {
|
||||||
t.Fatalf("invalid value: %d", c.SSHPort)
|
t.Fatalf("invalid value: %d", c.Comm.SSHPort)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunConfigPrepare_SSHTimeout(t *testing.T) {
|
|
||||||
c := testConfig()
|
|
||||||
c.RawSSHTimeout = ""
|
|
||||||
if err := c.Prepare(nil); len(err) != 0 {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.RawSSHTimeout = "bad"
|
|
||||||
if err := c.Prepare(nil); len(err) != 1 {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (
|
||||||
"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
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
|
||||||
// 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 {
|
||||||
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 (
|
||||||
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 {
|
||||||
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) {
|
||||||
|
|
||||||
// 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
|
||||||
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 {
|
||||||
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
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
|
"github.com/aws/aws-sdk-go/service/ec2"
|
||||||
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StepDeregisterAMI struct {
|
||||||
|
ForceDeregister bool
|
||||||
|
AMIName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepDeregisterAMI) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
|
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
|
// check for force deregister
|
||||||
|
if s.ForceDeregister {
|
||||||
|
resp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{
|
||||||
|
Filters: []*ec2.Filter{&ec2.Filter{
|
||||||
|
Name: aws.String("name"),
|
||||||
|
Values: []*string{aws.String(s.AMIName)},
|
||||||
|
}}})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("Error creating AMI: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
// deregister image(s) by that name
|
||||||
|
for _, i := range resp.Images {
|
||||||
|
_, err := ec2conn.DeregisterImage(&ec2.DeregisterImageInput{
|
||||||
|
ImageID: i.ImageID,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("Error deregistering existing AMI: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
ui.Say(fmt.Sprintf("Deregistered AMI %s, id: %s", s.AMIName, *i.ImageID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepDeregisterAMI) Cleanup(state multistep.StateBag) {
|
||||||
|
}
|
|
@ -0,0 +1,160 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go/service/ec2"
|
||||||
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/helper/communicator"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StepGetPassword reads the password from a Windows server and sets it
|
||||||
|
// on the WinRM config.
|
||||||
|
type StepGetPassword struct {
|
||||||
|
Comm *communicator.Config
|
||||||
|
Timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepGetPassword) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
image := state.Get("source_image").(*ec2.Image)
|
||||||
|
|
||||||
|
// Skip if we're not Windows...
|
||||||
|
if image.Platform == nil || *image.Platform != "windows" {
|
||||||
|
log.Printf("[INFO] Not Windows, skipping get password...")
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we already have a password, skip it
|
||||||
|
if s.Comm.WinRMPassword != "" {
|
||||||
|
ui.Say("Skipping waiting for password since WinRM password set...")
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the password
|
||||||
|
var password string
|
||||||
|
var err error
|
||||||
|
cancel := make(chan struct{})
|
||||||
|
waitDone := make(chan bool, 1)
|
||||||
|
go func() {
|
||||||
|
ui.Say("Waiting for auto-generated password for instance...")
|
||||||
|
ui.Message(
|
||||||
|
"It is normal for this process to take up to 15 minutes,\n" +
|
||||||
|
"but it usually takes around 5. Please wait.")
|
||||||
|
password, err = s.waitForPassword(state, cancel)
|
||||||
|
waitDone <- true
|
||||||
|
}()
|
||||||
|
|
||||||
|
timeout := time.After(s.Timeout)
|
||||||
|
WaitLoop:
|
||||||
|
for {
|
||||||
|
// Wait for either SSH to become available, a timeout to occur,
|
||||||
|
// or an interrupt to come through.
|
||||||
|
select {
|
||||||
|
case <-waitDone:
|
||||||
|
if err != nil {
|
||||||
|
ui.Error(fmt.Sprintf("Error waiting for password: %s", err))
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Message(fmt.Sprintf(" \nPassword retrieved!"))
|
||||||
|
s.Comm.WinRMPassword = password
|
||||||
|
break WaitLoop
|
||||||
|
case <-timeout:
|
||||||
|
err := fmt.Errorf("Timeout waiting for password.")
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
close(cancel)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
case <-time.After(1 * time.Second):
|
||||||
|
if _, ok := state.GetOk(multistep.StateCancelled); ok {
|
||||||
|
// The step sequence was cancelled, so cancel waiting for password
|
||||||
|
// and just start the halting process.
|
||||||
|
close(cancel)
|
||||||
|
log.Println("[WARN] Interrupt detected, quitting waiting for password.")
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepGetPassword) Cleanup(multistep.StateBag) {}
|
||||||
|
|
||||||
|
func (s *StepGetPassword) waitForPassword(state multistep.StateBag, cancel <-chan struct{}) (string, error) {
|
||||||
|
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||||
|
instance := state.Get("instance").(*ec2.Instance)
|
||||||
|
privateKey := state.Get("privateKey").(string)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-cancel:
|
||||||
|
log.Println("[INFO] Retrieve password wait cancelled. Exiting loop.")
|
||||||
|
return "", errors.New("Retrieve password wait cancelled")
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := ec2conn.GetPasswordData(&ec2.GetPasswordDataInput{
|
||||||
|
InstanceID: instance.InstanceID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("Error retrieving auto-generated instance password: %s", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.PasswordData != nil && *resp.PasswordData != "" {
|
||||||
|
decryptedPassword, err := decryptPasswordDataWithPrivateKey(
|
||||||
|
*resp.PasswordData, []byte(privateKey))
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("Error decrypting auto-generated instance password: %s", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return decryptedPassword, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] Password is blank, will retry...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decryptPasswordDataWithPrivateKey(passwordData string, pemBytes []byte) (string, error) {
|
||||||
|
encryptedPasswd, err := base64.StdEncoding.DecodeString(passwordData)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
block, _ := pem.Decode(pemBytes)
|
||||||
|
var asn1Bytes []byte
|
||||||
|
if _, ok := block.Headers["DEK-Info"]; ok {
|
||||||
|
return "", errors.New("encrypted private key isn't yet supported")
|
||||||
|
/*
|
||||||
|
asn1Bytes, err = x509.DecryptPEMBlock(block, password)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
} else {
|
||||||
|
asn1Bytes = block.Bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := x509.ParsePKCS1PrivateKey(asn1Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := rsa.DecryptPKCS1v15(nil, key, encryptedPasswd)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(out), nil
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
|
"github.com/aws/aws-sdk-go/service/ec2"
|
||||||
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StepPreValidate provides an opportunity to pre-validate any configuration for
|
||||||
|
// the build before actually doing any time consuming work
|
||||||
|
//
|
||||||
|
type StepPreValidate struct {
|
||||||
|
DestAmiName string
|
||||||
|
ForceDeregister bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepPreValidate) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
if s.ForceDeregister {
|
||||||
|
ui.Say("Force Deregister flag found, skipping prevalidating AMI Name")
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||||
|
|
||||||
|
ui.Say("Prevalidating AMI Name...")
|
||||||
|
resp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{
|
||||||
|
Filters: []*ec2.Filter{&ec2.Filter{
|
||||||
|
Name: aws.String("name"),
|
||||||
|
Values: []*string{aws.String(s.DestAmiName)},
|
||||||
|
}}})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("Error querying AMI: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resp.Images) > 0 {
|
||||||
|
err := fmt.Errorf("Error: name conflicts with an existing AMI: %s", *resp.Images[0].ImageID)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepPreValidate) Cleanup(multistep.StateBag) {}
|
|
@ -1,6 +1,7 @@
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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) {
|
||||||
}
|
}
|
||||||
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 (
|
||||||
"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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
|
|
||||||
// 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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
// 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
|
||||||
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
|
||||||
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,
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
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) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = `
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
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 {
|
||||||
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)
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
package ebs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
|
"github.com/aws/aws-sdk-go/service/ec2"
|
||||||
|
"github.com/mitchellh/packer/builder/amazon/common"
|
||||||
|
builderT "github.com/mitchellh/packer/helper/builder/testing"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuilderTagsAcc_basic(t *testing.T) {
|
||||||
|
builderT.Test(t, builderT.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Builder: &Builder{},
|
||||||
|
Template: testBuilderTagsAccBasic,
|
||||||
|
Check: checkTags(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkTags() builderT.TestCheckFunc {
|
||||||
|
return func(artifacts []packer.Artifact) error {
|
||||||
|
if len(artifacts) > 1 {
|
||||||
|
return fmt.Errorf("more than 1 artifact")
|
||||||
|
}
|
||||||
|
|
||||||
|
tags := make(map[string]string)
|
||||||
|
tags["OS_Version"] = "Ubuntu"
|
||||||
|
tags["Release"] = "Latest"
|
||||||
|
|
||||||
|
// Get the actual *Artifact pointer so we can access the AMIs directly
|
||||||
|
artifactRaw := artifacts[0]
|
||||||
|
artifact, ok := artifactRaw.(*common.Artifact)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unknown artifact: %#v", artifactRaw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// describe the image, get block devices with a snapshot
|
||||||
|
ec2conn, _ := testEC2Conn()
|
||||||
|
imageResp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{
|
||||||
|
ImageIDs: []*string{aws.String(artifact.Amis["us-east-1"])},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error retrieving details for AMI Artifcat (%#v) in Tags Test: %s", artifact, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(imageResp.Images) == 0 {
|
||||||
|
return fmt.Errorf("No images found for AMI Artifcat (%#v) in Tags Test: %s", artifact, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
image := imageResp.Images[0]
|
||||||
|
|
||||||
|
// Check only those with a Snapshot ID, i.e. not Ephemeral
|
||||||
|
var snapshots []*string
|
||||||
|
for _, device := range image.BlockDeviceMappings {
|
||||||
|
if device.EBS != nil && device.EBS.SnapshotID != nil {
|
||||||
|
snapshots = append(snapshots, device.EBS.SnapshotID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// grab matching snapshot info
|
||||||
|
resp, err := ec2conn.DescribeSnapshots(&ec2.DescribeSnapshotsInput{
|
||||||
|
SnapshotIDs: snapshots,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error retreiving Snapshots for AMI Artifcat (%#v) in Tags Test: %s", artifact, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resp.Snapshots) == 0 {
|
||||||
|
return fmt.Errorf("No Snapshots found for AMI Artifcat (%#v) in Tags Test", artifact)
|
||||||
|
}
|
||||||
|
|
||||||
|
// grab the snapshots, check the tags
|
||||||
|
for _, s := range resp.Snapshots {
|
||||||
|
expected := len(tags)
|
||||||
|
for _, t := range s.Tags {
|
||||||
|
for key, value := range tags {
|
||||||
|
if key == *t.Key && value == *t.Value {
|
||||||
|
expected--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected > 0 {
|
||||||
|
return fmt.Errorf("Not all tags found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const testBuilderTagsAccBasic = `
|
||||||
|
{
|
||||||
|
"builders": [
|
||||||
|
{
|
||||||
|
"type": "test",
|
||||||
|
"region": "us-east-1",
|
||||||
|
"source_ami": "ami-9eaa1cf6",
|
||||||
|
"instance_type": "t2.micro",
|
||||||
|
"ssh_username": "ubuntu",
|
||||||
|
"ami_name": "packer-tags-testing-{{timestamp}}",
|
||||||
|
"tags": {
|
||||||
|
"OS_Version": "Ubuntu",
|
||||||
|
"Release": "Latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`
|
|
@ -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,6 +74,15 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.config.BundleUploadCommand == "" {
|
if b.config.BundleUploadCommand == "" {
|
||||||
|
if b.config.IamInstanceProfile != "" {
|
||||||
|
b.config.BundleUploadCommand = "sudo -i -n ec2-upload-bundle " +
|
||||||
|
"-b {{.BucketName}} " +
|
||||||
|
"-m {{.ManifestPath}} " +
|
||||||
|
"-d {{.BundleDirectory}} " +
|
||||||
|
"--batch " +
|
||||||
|
"--region {{.Region}} " +
|
||||||
|
"--retry"
|
||||||
|
} else {
|
||||||
b.config.BundleUploadCommand = "sudo -i -n ec2-upload-bundle " +
|
b.config.BundleUploadCommand = "sudo -i -n ec2-upload-bundle " +
|
||||||
"-b {{.BucketName}} " +
|
"-b {{.BucketName}} " +
|
||||||
"-m {{.ManifestPath}} " +
|
"-m {{.ManifestPath}} " +
|
||||||
|
@ -80,9 +90,10 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||||
"-s {{.SecretKey}} " +
|
"-s {{.SecretKey}} " +
|
||||||
"-d {{.BundleDirectory}} " +
|
"-d {{.BundleDirectory}} " +
|
||||||
"--batch " +
|
"--batch " +
|
||||||
"--location {{.Region}} " +
|
"--region {{.Region}} " +
|
||||||
"--retry"
|
"--retry"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if b.config.BundleVolCommand == "" {
|
if b.config.BundleVolCommand == "" {
|
||||||
b.config.BundleVolCommand = "sudo -i -n ec2-bundle-vol " +
|
b.config.BundleVolCommand = "sudo -i -n ec2-bundle-vol " +
|
||||||
|
@ -157,6 +168,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||||
|
|
||||||
// Build the steps
|
// 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
|
||||||
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
|
||||||
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
|
||||||
&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,
|
||||||
|
|
|
@ -1,76 +0,0 @@
|
||||||
// All of the methods used to communicate with the digital_ocean API
|
|
||||||
// are here. Their API is on a path to V2, so just plain JSON is used
|
|
||||||
// in place of a proper client library for now.
|
|
||||||
|
|
||||||
package digitalocean
|
|
||||||
|
|
||||||
type Region struct {
|
|
||||||
Slug string `json:"slug"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
|
|
||||||
// v1 only
|
|
||||||
Id uint `json:"id,omitempty"`
|
|
||||||
|
|
||||||
// v2 only
|
|
||||||
Sizes []string `json:"sizes,omitempty"`
|
|
||||||
Available bool `json:"available,omitempty"`
|
|
||||||
Features []string `json:"features,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RegionsResp struct {
|
|
||||||
Regions []Region
|
|
||||||
}
|
|
||||||
|
|
||||||
type Size struct {
|
|
||||||
Slug string `json:"slug"`
|
|
||||||
|
|
||||||
// v1 only
|
|
||||||
Id uint `json:"id,omitempty"`
|
|
||||||
Name string `json:"name,omitempty"`
|
|
||||||
|
|
||||||
// v2 only
|
|
||||||
Memory uint `json:"memory,omitempty"`
|
|
||||||
VCPUS uint `json:"vcpus,omitempty"`
|
|
||||||
Disk uint `json:"disk,omitempty"`
|
|
||||||
Transfer float64 `json:"transfer,omitempty"`
|
|
||||||
PriceMonthly float64 `json:"price_monthly,omitempty"`
|
|
||||||
PriceHourly float64 `json:"price_hourly,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SizesResp struct {
|
|
||||||
Sizes []Size
|
|
||||||
}
|
|
||||||
|
|
||||||
type Image struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Slug string `json:"slug"`
|
|
||||||
Distribution string `json:"distribution"`
|
|
||||||
|
|
||||||
// v2 only
|
|
||||||
Public bool `json:"public,omitempty"`
|
|
||||||
ActionIds []string `json:"action_ids,omitempty"`
|
|
||||||
CreatedAt string `json:"created_at,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ImagesResp struct {
|
|
||||||
Images []Image
|
|
||||||
}
|
|
||||||
|
|
||||||
type DigitalOceanClient interface {
|
|
||||||
CreateKey(string, string) (uint, error)
|
|
||||||
DestroyKey(uint) error
|
|
||||||
CreateDroplet(string, string, string, string, uint, bool) (uint, error)
|
|
||||||
DestroyDroplet(uint) error
|
|
||||||
PowerOffDroplet(uint) error
|
|
||||||
ShutdownDroplet(uint) error
|
|
||||||
CreateSnapshot(uint, string) error
|
|
||||||
Images() ([]Image, error)
|
|
||||||
DestroyImage(uint) error
|
|
||||||
DropletStatus(uint) (string, string, error)
|
|
||||||
Image(string) (Image, error)
|
|
||||||
Regions() ([]Region, error)
|
|
||||||
Region(string) (Region, error)
|
|
||||||
Sizes() ([]Size, error)
|
|
||||||
Size(string) (Size, error)
|
|
||||||
}
|
|
|
@ -1,382 +0,0 @@
|
||||||
// All of the methods used to communicate with the digital_ocean API
|
|
||||||
// are here. Their API is on a path to V2, so just plain JSON is used
|
|
||||||
// in place of a proper client library for now.
|
|
||||||
|
|
||||||
package digitalocean
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mitchellh/mapstructure"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DigitalOceanClientV1 struct {
|
|
||||||
// The http client for communicating
|
|
||||||
client *http.Client
|
|
||||||
|
|
||||||
// Credentials
|
|
||||||
ClientID string
|
|
||||||
APIKey string
|
|
||||||
// The base URL of the API
|
|
||||||
APIURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a new client for communicating with DO
|
|
||||||
func DigitalOceanClientNewV1(client string, key string, url string) *DigitalOceanClientV1 {
|
|
||||||
c := &DigitalOceanClientV1{
|
|
||||||
client: &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
APIURL: url,
|
|
||||||
ClientID: client,
|
|
||||||
APIKey: key,
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates an SSH Key and returns it's id
|
|
||||||
func (d DigitalOceanClientV1) CreateKey(name string, pub string) (uint, error) {
|
|
||||||
params := url.Values{}
|
|
||||||
params.Set("name", name)
|
|
||||||
params.Set("ssh_pub_key", pub)
|
|
||||||
|
|
||||||
body, err := NewRequestV1(d, "ssh_keys/new", params)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the SSH key's ID we just created
|
|
||||||
key := body["ssh_key"].(map[string]interface{})
|
|
||||||
keyId := key["id"].(float64)
|
|
||||||
return uint(keyId), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroys an SSH key
|
|
||||||
func (d DigitalOceanClientV1) DestroyKey(id uint) error {
|
|
||||||
path := fmt.Sprintf("ssh_keys/%v/destroy", id)
|
|
||||||
_, err := NewRequestV1(d, path, url.Values{})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a droplet and returns it's id
|
|
||||||
func (d DigitalOceanClientV1) CreateDroplet(name string, size string, image string, region string, keyId uint, privateNetworking bool) (uint, error) {
|
|
||||||
params := url.Values{}
|
|
||||||
params.Set("name", name)
|
|
||||||
|
|
||||||
found_size, err := d.Size(size)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("Invalid size or lookup failure: '%s': %s", size, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
found_image, err := d.Image(image)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("Invalid image or lookup failure: '%s': %s", image, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
found_region, err := d.Region(region)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("Invalid region or lookup failure: '%s': %s", region, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
params.Set("size_slug", found_size.Slug)
|
|
||||||
params.Set("image_slug", found_image.Slug)
|
|
||||||
params.Set("region_slug", found_region.Slug)
|
|
||||||
params.Set("ssh_key_ids", fmt.Sprintf("%v", keyId))
|
|
||||||
params.Set("private_networking", fmt.Sprintf("%v", privateNetworking))
|
|
||||||
|
|
||||||
body, err := NewRequestV1(d, "droplets/new", params)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the Droplets ID
|
|
||||||
droplet := body["droplet"].(map[string]interface{})
|
|
||||||
dropletId := droplet["id"].(float64)
|
|
||||||
return uint(dropletId), err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroys a droplet
|
|
||||||
func (d DigitalOceanClientV1) DestroyDroplet(id uint) error {
|
|
||||||
path := fmt.Sprintf("droplets/%v/destroy", id)
|
|
||||||
_, err := NewRequestV1(d, path, url.Values{})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Powers off a droplet
|
|
||||||
func (d DigitalOceanClientV1) PowerOffDroplet(id uint) error {
|
|
||||||
path := fmt.Sprintf("droplets/%v/power_off", id)
|
|
||||||
_, err := NewRequestV1(d, path, url.Values{})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shutsdown a droplet. This is a "soft" shutdown.
|
|
||||||
func (d DigitalOceanClientV1) ShutdownDroplet(id uint) error {
|
|
||||||
path := fmt.Sprintf("droplets/%v/shutdown", id)
|
|
||||||
_, err := NewRequestV1(d, path, url.Values{})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a snaphot of a droplet by it's ID
|
|
||||||
func (d DigitalOceanClientV1) CreateSnapshot(id uint, name string) error {
|
|
||||||
path := fmt.Sprintf("droplets/%v/snapshot", id)
|
|
||||||
|
|
||||||
params := url.Values{}
|
|
||||||
params.Set("name", name)
|
|
||||||
|
|
||||||
_, err := NewRequestV1(d, path, params)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns all available images.
|
|
||||||
func (d DigitalOceanClientV1) Images() ([]Image, error) {
|
|
||||||
resp, err := NewRequestV1(d, "images", url.Values{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var result ImagesResp
|
|
||||||
if err := mapstructure.Decode(resp, &result); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.Images, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroys an image by its ID.
|
|
||||||
func (d DigitalOceanClientV1) DestroyImage(id uint) error {
|
|
||||||
path := fmt.Sprintf("images/%d/destroy", id)
|
|
||||||
_, err := NewRequestV1(d, path, url.Values{})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns DO's string representation of status "off" "new" "active" etc.
|
|
||||||
func (d DigitalOceanClientV1) DropletStatus(id uint) (string, string, error) {
|
|
||||||
path := fmt.Sprintf("droplets/%v", id)
|
|
||||||
|
|
||||||
body, err := NewRequestV1(d, path, url.Values{})
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
var ip string
|
|
||||||
|
|
||||||
// Read the droplet's "status"
|
|
||||||
droplet := body["droplet"].(map[string]interface{})
|
|
||||||
status := droplet["status"].(string)
|
|
||||||
|
|
||||||
if droplet["ip_address"] != nil {
|
|
||||||
ip = droplet["ip_address"].(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ip, status, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sends an api request and returns a generic map[string]interface of
|
|
||||||
// the response.
|
|
||||||
func NewRequestV1(d DigitalOceanClientV1, path string, params url.Values) (map[string]interface{}, error) {
|
|
||||||
client := d.client
|
|
||||||
|
|
||||||
// Add the authentication parameters
|
|
||||||
params.Set("client_id", d.ClientID)
|
|
||||||
params.Set("api_key", d.APIKey)
|
|
||||||
|
|
||||||
url := fmt.Sprintf("%s/%s?%s", d.APIURL, path, params.Encode())
|
|
||||||
|
|
||||||
// Do some basic scrubbing so sensitive information doesn't appear in logs
|
|
||||||
scrubbedUrl := strings.Replace(url, d.ClientID, "CLIENT_ID", -1)
|
|
||||||
scrubbedUrl = strings.Replace(scrubbedUrl, d.APIKey, "API_KEY", -1)
|
|
||||||
log.Printf("sending new request to digitalocean: %s", scrubbedUrl)
|
|
||||||
|
|
||||||
var lastErr error
|
|
||||||
for attempts := 1; attempts < 10; attempts++ {
|
|
||||||
resp, err := client.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
resp.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("response from digitalocean: %s", body)
|
|
||||||
|
|
||||||
var decodedResponse map[string]interface{}
|
|
||||||
err = json.Unmarshal(body, &decodedResponse)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.New(fmt.Sprintf("Failed to decode JSON response (HTTP %v) from DigitalOcean: %s",
|
|
||||||
resp.StatusCode, body))
|
|
||||||
return decodedResponse, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for errors sent by digitalocean
|
|
||||||
status := decodedResponse["status"].(string)
|
|
||||||
if status == "OK" {
|
|
||||||
return decodedResponse, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if status == "ERROR" {
|
|
||||||
statusRaw, ok := decodedResponse["error_message"]
|
|
||||||
if ok {
|
|
||||||
status = statusRaw.(string)
|
|
||||||
} else {
|
|
||||||
status = fmt.Sprintf(
|
|
||||||
"Unknown error. Full response body: %s", body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastErr = errors.New(fmt.Sprintf("Received error from DigitalOcean (%d): %s",
|
|
||||||
resp.StatusCode, status))
|
|
||||||
log.Println(lastErr)
|
|
||||||
if strings.Contains(status, "a pending event") {
|
|
||||||
// Retry, DigitalOcean sends these dumb "pending event"
|
|
||||||
// errors all the time.
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some other kind of error. Just return.
|
|
||||||
return decodedResponse, lastErr
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, lastErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d DigitalOceanClientV1) Image(slug_or_name_or_id string) (Image, error) {
|
|
||||||
images, err := d.Images()
|
|
||||||
if err != nil {
|
|
||||||
return Image{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, image := range images {
|
|
||||||
if strings.EqualFold(image.Slug, slug_or_name_or_id) {
|
|
||||||
return image, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, image := range images {
|
|
||||||
if strings.EqualFold(image.Name, slug_or_name_or_id) {
|
|
||||||
return image, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, image := range images {
|
|
||||||
id, err := strconv.Atoi(slug_or_name_or_id)
|
|
||||||
if err == nil {
|
|
||||||
if image.Id == uint(id) {
|
|
||||||
return image, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = errors.New(fmt.Sprintf("Unknown image '%v'", slug_or_name_or_id))
|
|
||||||
|
|
||||||
return Image{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns all available regions.
|
|
||||||
func (d DigitalOceanClientV1) Regions() ([]Region, error) {
|
|
||||||
resp, err := NewRequestV1(d, "regions", url.Values{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var result RegionsResp
|
|
||||||
if err := mapstructure.Decode(resp, &result); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.Regions, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d DigitalOceanClientV1) Region(slug_or_name_or_id string) (Region, error) {
|
|
||||||
regions, err := d.Regions()
|
|
||||||
if err != nil {
|
|
||||||
return Region{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, region := range regions {
|
|
||||||
if strings.EqualFold(region.Slug, slug_or_name_or_id) {
|
|
||||||
return region, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, region := range regions {
|
|
||||||
if strings.EqualFold(region.Name, slug_or_name_or_id) {
|
|
||||||
return region, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, region := range regions {
|
|
||||||
id, err := strconv.Atoi(slug_or_name_or_id)
|
|
||||||
if err == nil {
|
|
||||||
if region.Id == uint(id) {
|
|
||||||
return region, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = errors.New(fmt.Sprintf("Unknown region '%v'", slug_or_name_or_id))
|
|
||||||
|
|
||||||
return Region{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns all available sizes.
|
|
||||||
func (d DigitalOceanClientV1) Sizes() ([]Size, error) {
|
|
||||||
resp, err := NewRequestV1(d, "sizes", url.Values{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var result SizesResp
|
|
||||||
if err := mapstructure.Decode(resp, &result); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.Sizes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d DigitalOceanClientV1) Size(slug_or_name_or_id string) (Size, error) {
|
|
||||||
sizes, err := d.Sizes()
|
|
||||||
if err != nil {
|
|
||||||
return Size{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, size := range sizes {
|
|
||||||
if strings.EqualFold(size.Slug, slug_or_name_or_id) {
|
|
||||||
return size, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, size := range sizes {
|
|
||||||
if strings.EqualFold(size.Name, slug_or_name_or_id) {
|
|
||||||
return size, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, size := range sizes {
|
|
||||||
id, err := strconv.Atoi(slug_or_name_or_id)
|
|
||||||
if err == nil {
|
|
||||||
if size.Id == uint(id) {
|
|
||||||
return size, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = errors.New(fmt.Sprintf("Unknown size '%v'", slug_or_name_or_id))
|
|
||||||
|
|
||||||
return Size{}, err
|
|
||||||
}
|
|
|
@ -1,457 +0,0 @@
|
||||||
// are here. Their API is on a path to V2, so just plain JSON is used
|
|
||||||
// in place of a proper client library for now.
|
|
||||||
|
|
||||||
package digitalocean
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DigitalOceanClientV2 struct {
|
|
||||||
// The http client for communicating
|
|
||||||
client *http.Client
|
|
||||||
|
|
||||||
// Credentials
|
|
||||||
APIToken string
|
|
||||||
|
|
||||||
// The base URL of the API
|
|
||||||
APIURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a new client for communicating with DO
|
|
||||||
func DigitalOceanClientNewV2(token string, url string) *DigitalOceanClientV2 {
|
|
||||||
c := &DigitalOceanClientV2{
|
|
||||||
client: &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
APIURL: url,
|
|
||||||
APIToken: token,
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates an SSH Key and returns it's id
|
|
||||||
func (d DigitalOceanClientV2) CreateKey(name string, pub string) (uint, error) {
|
|
||||||
type KeyReq struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
PublicKey string `json:"public_key"`
|
|
||||||
}
|
|
||||||
type KeyRes struct {
|
|
||||||
SSHKey struct {
|
|
||||||
Id uint
|
|
||||||
Name string
|
|
||||||
Fingerprint string
|
|
||||||
PublicKey string `json:"public_key"`
|
|
||||||
} `json:"ssh_key"`
|
|
||||||
}
|
|
||||||
req := &KeyReq{Name: name, PublicKey: pub}
|
|
||||||
res := KeyRes{}
|
|
||||||
err := NewRequestV2(d, "v2/account/keys", "POST", req, &res)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.SSHKey.Id, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroys an SSH key
|
|
||||||
func (d DigitalOceanClientV2) DestroyKey(id uint) error {
|
|
||||||
path := fmt.Sprintf("v2/account/keys/%v", id)
|
|
||||||
return NewRequestV2(d, path, "DELETE", nil, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a droplet and returns it's id
|
|
||||||
func (d DigitalOceanClientV2) CreateDroplet(name string, size string, image string, region string, keyId uint, privateNetworking bool) (uint, error) {
|
|
||||||
type DropletReq struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Region string `json:"region"`
|
|
||||||
Size string `json:"size"`
|
|
||||||
Image string `json:"image"`
|
|
||||||
SSHKeys []string `json:"ssh_keys,omitempty"`
|
|
||||||
Backups bool `json:"backups,omitempty"`
|
|
||||||
IPv6 bool `json:"ipv6,omitempty"`
|
|
||||||
PrivateNetworking bool `json:"private_networking,omitempty"`
|
|
||||||
}
|
|
||||||
type DropletRes struct {
|
|
||||||
Droplet struct {
|
|
||||||
Id uint
|
|
||||||
Name string
|
|
||||||
Memory uint
|
|
||||||
VCPUS uint `json:"vcpus"`
|
|
||||||
Disk uint
|
|
||||||
Region Region
|
|
||||||
Image Image
|
|
||||||
Size Size
|
|
||||||
Locked bool
|
|
||||||
CreateAt string `json:"created_at"`
|
|
||||||
Status string
|
|
||||||
Networks struct {
|
|
||||||
V4 []struct {
|
|
||||||
IPAddr string `json:"ip_address"`
|
|
||||||
Netmask string
|
|
||||||
Gateway string
|
|
||||||
Type string
|
|
||||||
} `json:"v4,omitempty"`
|
|
||||||
V6 []struct {
|
|
||||||
IPAddr string `json:"ip_address"`
|
|
||||||
CIDR uint `json:"cidr"`
|
|
||||||
Gateway string
|
|
||||||
Type string
|
|
||||||
} `json:"v6,omitempty"`
|
|
||||||
}
|
|
||||||
Kernel struct {
|
|
||||||
Id uint
|
|
||||||
Name string
|
|
||||||
Version string
|
|
||||||
}
|
|
||||||
BackupIds []uint
|
|
||||||
SnapshotIds []uint
|
|
||||||
ActionIds []uint
|
|
||||||
Features []string `json:"features,omitempty"`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
req := &DropletReq{Name: name}
|
|
||||||
res := DropletRes{}
|
|
||||||
|
|
||||||
found_size, err := d.Size(size)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("Invalid size or lookup failure: '%s': %s", size, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
found_image, err := d.Image(image)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("Invalid image or lookup failure: '%s': %s", image, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
found_region, err := d.Region(region)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("Invalid region or lookup failure: '%s': %s", region, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Size = found_size.Slug
|
|
||||||
req.Image = found_image.Slug
|
|
||||||
req.Region = found_region.Slug
|
|
||||||
req.SSHKeys = []string{fmt.Sprintf("%v", keyId)}
|
|
||||||
req.PrivateNetworking = privateNetworking
|
|
||||||
|
|
||||||
err = NewRequestV2(d, "v2/droplets", "POST", req, &res)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.Droplet.Id, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroys a droplet
|
|
||||||
func (d DigitalOceanClientV2) DestroyDroplet(id uint) error {
|
|
||||||
path := fmt.Sprintf("v2/droplets/%v", id)
|
|
||||||
return NewRequestV2(d, path, "DELETE", nil, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Powers off a droplet
|
|
||||||
func (d DigitalOceanClientV2) PowerOffDroplet(id uint) error {
|
|
||||||
type ActionReq struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
type ActionRes struct {
|
|
||||||
}
|
|
||||||
req := &ActionReq{Type: "power_off"}
|
|
||||||
path := fmt.Sprintf("v2/droplets/%v/actions", id)
|
|
||||||
return NewRequestV2(d, path, "POST", req, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shutsdown a droplet. This is a "soft" shutdown.
|
|
||||||
func (d DigitalOceanClientV2) ShutdownDroplet(id uint) error {
|
|
||||||
type ActionReq struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
type ActionRes struct {
|
|
||||||
}
|
|
||||||
req := &ActionReq{Type: "shutdown"}
|
|
||||||
|
|
||||||
path := fmt.Sprintf("v2/droplets/%v/actions", id)
|
|
||||||
return NewRequestV2(d, path, "POST", req, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a snaphot of a droplet by it's ID
|
|
||||||
func (d DigitalOceanClientV2) CreateSnapshot(id uint, name string) error {
|
|
||||||
type ActionReq struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
type ActionRes struct {
|
|
||||||
}
|
|
||||||
req := &ActionReq{Type: "snapshot", Name: name}
|
|
||||||
path := fmt.Sprintf("v2/droplets/%v/actions", id)
|
|
||||||
return NewRequestV2(d, path, "POST", req, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns all available images.
|
|
||||||
func (d DigitalOceanClientV2) Images() ([]Image, error) {
|
|
||||||
res := ImagesResp{}
|
|
||||||
|
|
||||||
err := NewRequestV2(d, "v2/images?per_page=200", "GET", nil, &res)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.Images, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroys an image by its ID.
|
|
||||||
func (d DigitalOceanClientV2) DestroyImage(id uint) error {
|
|
||||||
path := fmt.Sprintf("v2/images/%d", id)
|
|
||||||
return NewRequestV2(d, path, "DELETE", nil, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns DO's string representation of status "off" "new" "active" etc.
|
|
||||||
func (d DigitalOceanClientV2) DropletStatus(id uint) (string, string, error) {
|
|
||||||
path := fmt.Sprintf("v2/droplets/%v", id)
|
|
||||||
type DropletRes struct {
|
|
||||||
Droplet struct {
|
|
||||||
Id uint
|
|
||||||
Name string
|
|
||||||
Memory uint
|
|
||||||
VCPUS uint `json:"vcpus"`
|
|
||||||
Disk uint
|
|
||||||
Region Region
|
|
||||||
Image Image
|
|
||||||
Size Size
|
|
||||||
Locked bool
|
|
||||||
CreateAt string `json:"created_at"`
|
|
||||||
Status string
|
|
||||||
Networks struct {
|
|
||||||
V4 []struct {
|
|
||||||
IPAddr string `json:"ip_address"`
|
|
||||||
Netmask string
|
|
||||||
Gateway string
|
|
||||||
Type string
|
|
||||||
} `json:"v4,omitempty"`
|
|
||||||
V6 []struct {
|
|
||||||
IPAddr string `json:"ip_address"`
|
|
||||||
CIDR uint `json:"cidr"`
|
|
||||||
Gateway string
|
|
||||||
Type string
|
|
||||||
} `json:"v6,omitempty"`
|
|
||||||
}
|
|
||||||
Kernel struct {
|
|
||||||
Id uint
|
|
||||||
Name string
|
|
||||||
Version string
|
|
||||||
}
|
|
||||||
BackupIds []uint
|
|
||||||
SnapshotIds []uint
|
|
||||||
ActionIds []uint
|
|
||||||
Features []string `json:"features,omitempty"`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res := DropletRes{}
|
|
||||||
err := NewRequestV2(d, path, "GET", nil, &res)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
var ip string
|
|
||||||
|
|
||||||
for _, n := range res.Droplet.Networks.V4 {
|
|
||||||
if n.Type == "public" {
|
|
||||||
ip = n.IPAddr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ip, res.Droplet.Status, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sends an api request and returns a generic map[string]interface of
|
|
||||||
// the response.
|
|
||||||
func NewRequestV2(d DigitalOceanClientV2, path string, method string, req interface{}, res interface{}) error {
|
|
||||||
var err error
|
|
||||||
var request *http.Request
|
|
||||||
|
|
||||||
client := d.client
|
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
// Add the authentication parameters
|
|
||||||
url := fmt.Sprintf("%s/%s", d.APIURL, path)
|
|
||||||
if req != nil {
|
|
||||||
enc := json.NewEncoder(buf)
|
|
||||||
enc.Encode(req)
|
|
||||||
defer buf.Reset()
|
|
||||||
request, err = http.NewRequest(method, url, buf)
|
|
||||||
request.Header.Add("Content-Type", "application/json")
|
|
||||||
} else {
|
|
||||||
request, err = http.NewRequest(method, url, nil)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the authentication parameters
|
|
||||||
request.Header.Add("Authorization", "Bearer "+d.APIToken)
|
|
||||||
if buf != nil {
|
|
||||||
log.Printf("sending new request to digitalocean: %s buffer: %s", url, buf)
|
|
||||||
} else {
|
|
||||||
log.Printf("sending new request to digitalocean: %s", url)
|
|
||||||
}
|
|
||||||
resp, err := client.Do(request)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if method == "DELETE" && resp.StatusCode == 204 {
|
|
||||||
if resp.Body != nil {
|
|
||||||
resp.Body.Close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Body == nil {
|
|
||||||
return errors.New("Request returned empty body")
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
resp.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("response from digitalocean: %s", body)
|
|
||||||
|
|
||||||
err = json.Unmarshal(body, &res)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New(fmt.Sprintf("Failed to decode JSON response %s (HTTP %v) from DigitalOcean: %s", err.Error(),
|
|
||||||
resp.StatusCode, body))
|
|
||||||
}
|
|
||||||
switch resp.StatusCode {
|
|
||||||
case 403, 401, 429, 422, 404, 503, 500:
|
|
||||||
return errors.New(fmt.Sprintf("digitalocean request error: %+v", res))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d DigitalOceanClientV2) Image(slug_or_name_or_id string) (Image, error) {
|
|
||||||
images, err := d.Images()
|
|
||||||
if err != nil {
|
|
||||||
return Image{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, image := range images {
|
|
||||||
if strings.EqualFold(image.Slug, slug_or_name_or_id) {
|
|
||||||
return image, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, image := range images {
|
|
||||||
if strings.EqualFold(image.Name, slug_or_name_or_id) {
|
|
||||||
return image, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, image := range images {
|
|
||||||
id, err := strconv.Atoi(slug_or_name_or_id)
|
|
||||||
if err == nil {
|
|
||||||
if image.Id == uint(id) {
|
|
||||||
return image, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = errors.New(fmt.Sprintf("Unknown image '%v'", slug_or_name_or_id))
|
|
||||||
|
|
||||||
return Image{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns all available regions.
|
|
||||||
func (d DigitalOceanClientV2) Regions() ([]Region, error) {
|
|
||||||
res := RegionsResp{}
|
|
||||||
err := NewRequestV2(d, "v2/regions?per_page=200", "GET", nil, &res)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.Regions, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d DigitalOceanClientV2) Region(slug_or_name_or_id string) (Region, error) {
|
|
||||||
regions, err := d.Regions()
|
|
||||||
if err != nil {
|
|
||||||
return Region{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, region := range regions {
|
|
||||||
if strings.EqualFold(region.Slug, slug_or_name_or_id) {
|
|
||||||
return region, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, region := range regions {
|
|
||||||
if strings.EqualFold(region.Name, slug_or_name_or_id) {
|
|
||||||
return region, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, region := range regions {
|
|
||||||
id, err := strconv.Atoi(slug_or_name_or_id)
|
|
||||||
if err == nil {
|
|
||||||
if region.Id == uint(id) {
|
|
||||||
return region, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = errors.New(fmt.Sprintf("Unknown region '%v'", slug_or_name_or_id))
|
|
||||||
|
|
||||||
return Region{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns all available sizes.
|
|
||||||
func (d DigitalOceanClientV2) Sizes() ([]Size, error) {
|
|
||||||
res := SizesResp{}
|
|
||||||
err := NewRequestV2(d, "v2/sizes?per_page=200", "GET", nil, &res)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.Sizes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d DigitalOceanClientV2) Size(slug_or_name_or_id string) (Size, error) {
|
|
||||||
sizes, err := d.Sizes()
|
|
||||||
if err != nil {
|
|
||||||
return Size{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, size := range sizes {
|
|
||||||
if strings.EqualFold(size.Slug, slug_or_name_or_id) {
|
|
||||||
return size, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, size := range sizes {
|
|
||||||
if strings.EqualFold(size.Name, slug_or_name_or_id) {
|
|
||||||
return size, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, size := range sizes {
|
|
||||||
id, err := strconv.Atoi(slug_or_name_or_id)
|
|
||||||
if err == nil {
|
|
||||||
if size.Id == uint(id) {
|
|
||||||
return size, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = errors.New(fmt.Sprintf("Unknown size '%v'", slug_or_name_or_id))
|
|
||||||
|
|
||||||
return Size{}, err
|
|
||||||
}
|
|
|
@ -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 {
|
||||||
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{} {
|
||||||
|
|
||||||
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 @@
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
b.config = *c
|
||||||
|
|
||||||
// Optional configuration with defaults
|
|
||||||
if b.config.APIKey == "" {
|
|
||||||
// Default to environment variable for api_key, if it exists
|
|
||||||
b.config.APIKey = os.Getenv("DIGITALOCEAN_API_KEY")
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.config.ClientID == "" {
|
|
||||||
// Default to environment variable for client_id, if it exists
|
|
||||||
b.config.ClientID = os.Getenv("DIGITALOCEAN_CLIENT_ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.config.APIURL == "" {
|
|
||||||
// Default to environment variable for api_url, if it exists
|
|
||||||
b.config.APIURL = os.Getenv("DIGITALOCEAN_API_URL")
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.config.APIToken == "" {
|
|
||||||
// Default to environment variable for api_token, if it exists
|
|
||||||
b.config.APIToken = os.Getenv("DIGITALOCEAN_API_TOKEN")
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.config.Region == "" {
|
|
||||||
if b.config.RegionID != 0 {
|
|
||||||
b.config.Region = fmt.Sprintf("%v", b.config.RegionID)
|
|
||||||
} else {
|
|
||||||
b.config.Region = DefaultRegion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.config.Size == "" {
|
|
||||||
if b.config.SizeID != 0 {
|
|
||||||
b.config.Size = fmt.Sprintf("%v", b.config.SizeID)
|
|
||||||
} else {
|
|
||||||
b.config.Size = DefaultSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.config.Image == "" {
|
|
||||||
if b.config.ImageID != 0 {
|
|
||||||
b.config.Image = fmt.Sprintf("%v", b.config.ImageID)
|
|
||||||
} else {
|
|
||||||
b.config.Image = DefaultImage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.config.SnapshotName == "" {
|
|
||||||
// Default to packer-{{ unix timestamp (utc) }}
|
|
||||||
b.config.SnapshotName = "packer-{{timestamp}}"
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.config.DropletName == "" {
|
|
||||||
// Default to packer-[time-ordered-uuid]
|
|
||||||
b.config.DropletName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.config.SSHUsername == "" {
|
|
||||||
// Default to "root". You can override this if your
|
|
||||||
// SourceImage has a different user account then the DO default
|
|
||||||
b.config.SSHUsername = "root"
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.config.SSHPort == 0 {
|
|
||||||
// Default to port 22 per DO default
|
|
||||||
b.config.SSHPort = 22
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.config.RawSSHTimeout == "" {
|
|
||||||
// Default to 1 minute timeouts
|
|
||||||
b.config.RawSSHTimeout = "1m"
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.config.RawStateTimeout == "" {
|
|
||||||
// Default to 6 minute timeouts waiting for
|
|
||||||
// desired state. i.e waiting for droplet to become active
|
|
||||||
b.config.RawStateTimeout = "6m"
|
|
||||||
}
|
|
||||||
|
|
||||||
var errs *packer.MultiError
|
|
||||||
if b.config.APIToken == "" {
|
|
||||||
// Required configurations that will display errors if not set
|
|
||||||
if b.config.ClientID == "" {
|
|
||||||
errs = packer.MultiErrorAppend(
|
|
||||||
errs, errors.New("a client_id for v1 auth or api_token for v2 auth must be specified"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.config.APIKey == "" {
|
|
||||||
errs = packer.MultiErrorAppend(
|
|
||||||
errs, errors.New("a api_key for v1 auth or api_token for v2 auth must be specified"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.config.APIURL == "" {
|
|
||||||
b.config.APIURL = "https://api.digitalocean.com"
|
|
||||||
}
|
|
||||||
|
|
||||||
sshTimeout, err := time.ParseDuration(b.config.RawSSHTimeout)
|
|
||||||
if err != nil {
|
|
||||||
errs = packer.MultiErrorAppend(
|
|
||||||
errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
|
|
||||||
}
|
|
||||||
b.config.sshTimeout = sshTimeout
|
|
||||||
|
|
||||||
stateTimeout, err := time.ParseDuration(b.config.RawStateTimeout)
|
|
||||||
if err != nil {
|
|
||||||
errs = packer.MultiErrorAppend(
|
|
||||||
errs, fmt.Errorf("Failed parsing state_timeout: %s", err))
|
|
||||||
}
|
|
||||||
b.config.stateTimeout = stateTimeout
|
|
||||||
|
|
||||||
if errs != nil && len(errs.Errors) > 0 {
|
|
||||||
return nil, errs
|
|
||||||
}
|
|
||||||
|
|
||||||
common.ScrubConfig(b.config, b.config.ClientID, b.config.APIKey)
|
|
||||||
return nil, nil
|
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
|
||||||
|
|
||||||
// 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,
|
||||||
|
Host: commHost,
|
||||||
SSHConfig: sshConfig,
|
SSHConfig: sshConfig,
|
||||||
SSHWaitTimeout: 5 * time.Minute,
|
|
||||||
},
|
},
|
||||||
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
|
||||||
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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
package digitalocean
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
builderT "github.com/mitchellh/packer/helper/builder/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuilderAcc_basic(t *testing.T) {
|
||||||
|
builderT.Test(t, builderT.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Builder: &Builder{},
|
||||||
|
Template: testBuilderAccBasic,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccPreCheck(t *testing.T) {
|
||||||
|
if v := os.Getenv("DIGITALOCEAN_API_TOKEN"); v == "" {
|
||||||
|
t.Fatal("DIGITALOCEAN_API_TOKEN must be set for acceptance tests")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const testBuilderAccBasic = `
|
||||||
|
{
|
||||||
|
"builders": [{
|
||||||
|
"type": "test",
|
||||||
|
"region": "nyc2",
|
||||||
|
"size": "512mb",
|
||||||
|
"image": "ubuntu-12-04-x64"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
`
|
|
@ -1,22 +1,19 @@
|
||||||
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) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
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) {
|
||||||
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) {
|
||||||
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) {
|
||||||
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,52 +179,11 @@ 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) {
|
||||||
var b Builder
|
var b Builder
|
||||||
config := testConfig()
|
config := testConfig()
|
||||||
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
package digitalocean
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
|
"github.com/mitchellh/packer/common"
|
||||||
|
"github.com/mitchellh/packer/common/uuid"
|
||||||
|
"github.com/mitchellh/packer/helper/communicator"
|
||||||
|
"github.com/mitchellh/packer/helper/config"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
|
"github.com/mitchellh/packer/template/interpolate"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
common.PackerConfig `mapstructure:",squash"`
|
||||||
|
Comm communicator.Config `mapstructure:",squash"`
|
||||||
|
|
||||||
|
APIToken string `mapstructure:"api_token"`
|
||||||
|
|
||||||
|
Region string `mapstructure:"region"`
|
||||||
|
Size string `mapstructure:"size"`
|
||||||
|
Image string `mapstructure:"image"`
|
||||||
|
|
||||||
|
PrivateNetworking bool `mapstructure:"private_networking"`
|
||||||
|
SnapshotName string `mapstructure:"snapshot_name"`
|
||||||
|
StateTimeout time.Duration `mapstructure:"state_timeout"`
|
||||||
|
DropletName string `mapstructure:"droplet_name"`
|
||||||
|
UserData string `mapstructure:"user_data"`
|
||||||
|
|
||||||
|
ctx *interpolate.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
||||||
|
c := new(Config)
|
||||||
|
|
||||||
|
var md mapstructure.Metadata
|
||||||
|
err := config.Decode(c, &config.DecodeOpts{
|
||||||
|
Metadata: &md,
|
||||||
|
Interpolate: true,
|
||||||
|
InterpolateFilter: &interpolate.RenderFilter{
|
||||||
|
Exclude: []string{
|
||||||
|
"run_command",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, raws...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defaults
|
||||||
|
if c.APIToken == "" {
|
||||||
|
// Default to environment variable for api_token, if it exists
|
||||||
|
c.APIToken = os.Getenv("DIGITALOCEAN_API_TOKEN")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.SnapshotName == "" {
|
||||||
|
def, err := interpolate.Render("packer-{{timestamp}}", nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to packer-{{ unix timestamp (utc) }}
|
||||||
|
c.SnapshotName = def
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.DropletName == "" {
|
||||||
|
// Default to packer-[time-ordered-uuid]
|
||||||
|
c.DropletName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Comm.SSHUsername == "" {
|
||||||
|
// Default to "root". You can override this if your
|
||||||
|
// SourceImage has a different user account then the DO default
|
||||||
|
c.Comm.SSHUsername = "root"
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.StateTimeout == 0 {
|
||||||
|
// Default to 6 minute timeouts waiting for
|
||||||
|
// desired state. i.e waiting for droplet to become active
|
||||||
|
c.StateTimeout = 6 * time.Minute
|
||||||
|
}
|
||||||
|
|
||||||
|
var errs *packer.MultiError
|
||||||
|
if es := c.Comm.Prepare(c.ctx); len(es) > 0 {
|
||||||
|
errs = packer.MultiErrorAppend(errs, es...)
|
||||||
|
}
|
||||||
|
if c.APIToken == "" {
|
||||||
|
// Required configurations that will display errors if not set
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, errors.New("api_token for auth must be specified"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Region == "" {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, errors.New("region is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Size == "" {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, errors.New("size is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Image == "" {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, errors.New("image is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if errs != nil && len(errs.Errors) > 0 {
|
||||||
|
return nil, nil, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
common.ScrubConfig(c, c.APIToken)
|
||||||
|
return c, nil, nil
|
||||||
|
}
|
|
@ -1,15 +1,15 @@
|
||||||
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) {
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
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 (
|
||||||
"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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
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
|
||||||
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 (
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
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 (
|
||||||
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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (
|
||||||
"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 (
|
||||||
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 {
|
||||||
|
|
||||||
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 {
|
||||||
|
|
||||||
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 (
|
||||||
"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 (
|
||||||
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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
package digitalocean
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type apiTokenSource struct {
|
||||||
|
AccessToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *apiTokenSource) Token() (*oauth2.Token, error) {
|
||||||
|
return &oauth2.Token{
|
||||||
|
AccessToken: t.AccessToken,
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -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
|
||||||
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 (
|
||||||
|
|
||||||
"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
|
||||||
&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 {
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/communicator/ssh"
|
||||||
|
"github.com/mitchellh/packer/helper/communicator"
|
||||||
|
gossh "golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
func commHost(state multistep.StateBag) (string, error) {
|
||||||
|
containerId := state.Get("container_id").(string)
|
||||||
|
driver := state.Get("driver").(Driver)
|
||||||
|
return driver.IPAddress(containerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sshConfig(comm *communicator.Config) func(state multistep.StateBag) (*gossh.ClientConfig, error) {
|
||||||
|
return func(state multistep.StateBag) (*gossh.ClientConfig, error) {
|
||||||
|
if comm.SSHPrivateKey != "" {
|
||||||
|
// key based auth
|
||||||
|
bytes, err := ioutil.ReadFile(comm.SSHPrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
|
||||||
|
}
|
||||||
|
privateKey := string(bytes)
|
||||||
|
|
||||||
|
signer, err := gossh.ParsePrivateKey([]byte(privateKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &gossh.ClientConfig{
|
||||||
|
User: comm.SSHUsername,
|
||||||
|
Auth: []gossh.AuthMethod{
|
||||||
|
gossh.PublicKeys(signer),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
} else {
|
||||||
|
// password based auth
|
||||||
|
return &gossh.ClientConfig{
|
||||||
|
User: comm.SSHUsername,
|
||||||
|
Auth: []gossh.AuthMethod{
|
||||||
|
gossh.Password(comm.SSHPassword),
|
||||||
|
gossh.KeyboardInteractive(
|
||||||
|
ssh.PasswordKeyboardInteractive(comm.SSHPassword)),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
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) {
|
||||||
return nil, nil, errs
|
return nil, nil, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
return &c, nil, nil
|
return c, nil, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
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 {
|
||||||
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) {
|
||||||
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
|
||||||
|
|
||||||
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 {
|
||||||
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
|
||||||
|
|
||||||
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
|
||||||
&StepInstanceInfo{
|
&StepInstanceInfo{
|
||||||
Debug: b.config.PackerDebug,
|
Debug: b.config.PackerDebug,
|
||||||
},
|
},
|
||||||
&common.StepConnectSSH{
|
&communicator.StepConnect{
|
||||||
SSHAddress: sshAddress,
|
Config: &b.config.Comm,
|
||||||
|
Host: commHost,
|
||||||
SSHConfig: sshConfig,
|
SSHConfig: sshConfig,
|
||||||
SSHWaitTimeout: 5 * time.Minute,
|
|
||||||
},
|
},
|
||||||
new(common.StepProvision),
|
new(common.StepProvision),
|
||||||
new(StepTeardownInstance),
|
new(StepTeardownInstance),
|
||||||
|
|
|
@ -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 (
|
||||||
// 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 {
|
||||||
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) {
|
||||||
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) {
|
||||||
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(
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
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) {
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
// 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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
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) {
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
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 (
|
||||||
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) {
|
||||||
return nil, nil, errs
|
return nil, nil, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
return c, nil, nil
|
return &c, nil, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
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) {
|
||||||
// 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) {
|
||||||
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) {
|
||||||
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)
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
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 (
|
||||||
"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"`
|
||||||
|
UserID string `mapstructure:"user_id"`
|
||||||
Password string `mapstructure:"password"`
|
Password string `mapstructure:"password"`
|
||||||
ApiKey string `mapstructure:"api_key"`
|
APIKey string `mapstructure:"api_key"`
|
||||||
Project string `mapstructure:"project"`
|
IdentityEndpoint string `mapstructure:"identity_endpoint"`
|
||||||
Provider string `mapstructure:"provider"`
|
TenantID string `mapstructure:"tenant_id"`
|
||||||
RawRegion string `mapstructure:"region"`
|
TenantName string `mapstructure:"tenant_name"`
|
||||||
ProxyUrl string `mapstructure:"proxy_url"`
|
DomainID string `mapstructure:"domain_id"`
|
||||||
TenantId string `mapstructure:"tenant_id"`
|
DomainName string `mapstructure:"domain_name"`
|
||||||
Insecure bool `mapstructure:"insecure"`
|
Insecure bool `mapstructure:"insecure"`
|
||||||
}
|
Region string `mapstructure:"region"`
|
||||||
|
EndpointType string `mapstructure:"endpoint_type"`
|
||||||
|
|
||||||
// Auth returns a valid Auth object for access to openstack services, or
|
osClient *gophercloud.ProviderClient
|
||||||
// an error if the authentication couldn't be resolved.
|
|
||||||
func (c *AccessConfig) Auth() (gophercloud.AccessProvider, error) {
|
|
||||||
c.Username = common.ChooseString(c.Username, os.Getenv("SDK_USERNAME"), os.Getenv("OS_USERNAME"))
|
|
||||||
c.Password = common.ChooseString(c.Password, os.Getenv("SDK_PASSWORD"), os.Getenv("OS_PASSWORD"))
|
|
||||||
c.ApiKey = common.ChooseString(c.ApiKey, os.Getenv("SDK_API_KEY"))
|
|
||||||
c.Project = common.ChooseString(c.Project, os.Getenv("SDK_PROJECT"), os.Getenv("OS_TENANT_NAME"))
|
|
||||||
c.Provider = common.ChooseString(c.Provider, os.Getenv("SDK_PROVIDER"), os.Getenv("OS_AUTH_URL"))
|
|
||||||
c.RawRegion = common.ChooseString(c.RawRegion, os.Getenv("SDK_REGION"), os.Getenv("OS_REGION_NAME"))
|
|
||||||
c.TenantId = common.ChooseString(c.TenantId, os.Getenv("OS_TENANT_ID"))
|
|
||||||
|
|
||||||
// OpenStack's auto-generated openrc.sh files do not append the suffix
|
|
||||||
// /tokens to the authentication URL. This ensures it is present when
|
|
||||||
// specifying the URL.
|
|
||||||
if strings.Contains(c.Provider, "://") && !strings.HasSuffix(c.Provider, "/tokens") {
|
|
||||||
c.Provider += "/tokens"
|
|
||||||
}
|
|
||||||
|
|
||||||
authoptions := gophercloud.AuthOptions{
|
|
||||||
AllowReauth: true,
|
|
||||||
|
|
||||||
ApiKey: c.ApiKey,
|
|
||||||
TenantId: c.TenantId,
|
|
||||||
TenantName: c.Project,
|
|
||||||
Username: c.Username,
|
|
||||||
Password: c.Password,
|
|
||||||
}
|
|
||||||
|
|
||||||
default_transport := &http.Transport{}
|
|
||||||
|
|
||||||
if c.Insecure {
|
|
||||||
cfg := new(tls.Config)
|
|
||||||
cfg.InsecureSkipVerify = true
|
|
||||||
default_transport.TLSClientConfig = cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
// For corporate networks it may be the case where we want our API calls
|
|
||||||
// to be sent through a separate HTTP proxy than external traffic.
|
|
||||||
if c.ProxyUrl != "" {
|
|
||||||
url, err := url.Parse(c.ProxyUrl)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// The gophercloud.Context has a UseCustomClient method which
|
|
||||||
// would allow us to override with a new instance of http.Client.
|
|
||||||
default_transport.Proxy = http.ProxyURL(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Insecure || c.ProxyUrl != "" {
|
|
||||||
http.DefaultTransport = default_transport
|
|
||||||
}
|
|
||||||
|
|
||||||
return gophercloud.Authenticate(c.Provider, authoptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *AccessConfig) Region() string {
|
|
||||||
return common.ChooseString(c.RawRegion, os.Getenv("SDK_REGION"), os.Getenv("OS_REGION_NAME"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
|
func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
|
||||||
errs := make([]error, 0)
|
if c.EndpointType != "internal" && c.EndpointType != "internalURL" &&
|
||||||
if strings.HasPrefix(c.Provider, "rackspace") {
|
c.EndpointType != "admin" && c.EndpointType != "adminURL" &&
|
||||||
if c.Region() == "" {
|
c.EndpointType != "public" && c.EndpointType != "publicURL" &&
|
||||||
errs = append(errs, fmt.Errorf("region must be specified when using rackspace"))
|
c.EndpointType != "" {
|
||||||
|
return []error{fmt.Errorf("Invalid endpoint type provided")}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Region == "" {
|
||||||
|
c.Region = os.Getenv("OS_REGION_NAME")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy RackSpace stuff. We're keeping this around to keep things BC.
|
||||||
|
if c.APIKey == "" {
|
||||||
|
c.APIKey = os.Getenv("SDK_API_KEY")
|
||||||
|
}
|
||||||
|
if c.Password == "" {
|
||||||
|
c.Password = os.Getenv("SDK_PASSWORD")
|
||||||
|
}
|
||||||
|
if c.Region == "" {
|
||||||
|
c.Region = os.Getenv("SDK_REGION")
|
||||||
|
}
|
||||||
|
if c.TenantName == "" {
|
||||||
|
c.TenantName = os.Getenv("SDK_PROJECT")
|
||||||
|
}
|
||||||
|
if c.Username == "" {
|
||||||
|
c.Username = os.Getenv("SDK_USERNAME")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get as much as possible from the end
|
||||||
|
ao, _ := openstack.AuthOptionsFromEnv()
|
||||||
|
|
||||||
|
// Override values if we have them in our config
|
||||||
|
overrides := []struct {
|
||||||
|
From, To *string
|
||||||
|
}{
|
||||||
|
{&c.Username, &ao.Username},
|
||||||
|
{&c.UserID, &ao.UserID},
|
||||||
|
{&c.Password, &ao.Password},
|
||||||
|
{&c.APIKey, &ao.APIKey},
|
||||||
|
{&c.IdentityEndpoint, &ao.IdentityEndpoint},
|
||||||
|
{&c.TenantID, &ao.TenantID},
|
||||||
|
{&c.TenantName, &ao.TenantName},
|
||||||
|
{&c.DomainID, &ao.DomainID},
|
||||||
|
{&c.DomainName, &ao.DomainName},
|
||||||
|
}
|
||||||
|
for _, s := range overrides {
|
||||||
|
if *s.From != "" {
|
||||||
|
*s.To = *s.From
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(errs) > 0 {
|
// Build the client itself
|
||||||
return errs
|
client, err := openstack.NewClient(ao.IdentityEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
return []error{err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we have insecure set, then create a custom HTTP client that
|
||||||
|
// ignores SSL errors.
|
||||||
|
if c.Insecure {
|
||||||
|
config := &tls.Config{InsecureSkipVerify: true}
|
||||||
|
transport := &http.Transport{TLSClientConfig: config}
|
||||||
|
client.HTTPClient.Transport = transport
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth
|
||||||
|
err = openstack.Authenticate(client, ao)
|
||||||
|
if err != nil {
|
||||||
|
return []error{err}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.osClient = client
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *AccessConfig) computeV2Client() (*gophercloud.ServiceClient, error) {
|
||||||
|
return openstack.NewComputeV2(c.osClient, gophercloud.EndpointOpts{
|
||||||
|
Region: c.Region,
|
||||||
|
Availability: c.getEndpointType(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AccessConfig) getEndpointType() gophercloud.Availability {
|
||||||
|
if c.EndpointType == "internal" || c.EndpointType == "internalURL" {
|
||||||
|
return gophercloud.AvailabilityInternal
|
||||||
|
}
|
||||||
|
if c.EndpointType == "admin" || c.EndpointType == "adminURL" {
|
||||||
|
return gophercloud.AvailabilityAdmin
|
||||||
|
}
|
||||||
|
return gophercloud.AvailabilityPublic
|
||||||
|
}
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
package openstack
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func testAccessConfig() *AccessConfig {
|
|
||||||
return &AccessConfig{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAccessConfigPrepare_NoRegion_Rackspace(t *testing.T) {
|
|
||||||
c := testAccessConfig()
|
|
||||||
c.Provider = "rackspace-us"
|
|
||||||
if err := c.Prepare(nil); err == nil {
|
|
||||||
t.Fatalf("shouldn't have err: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAccessConfigPrepare_NoRegion_PrivateCloud(t *testing.T) {
|
|
||||||
c := testAccessConfig()
|
|
||||||
c.Provider = "http://some-keystone-server:5000/v2.0"
|
|
||||||
if err := c.Prepare(nil); err != nil {
|
|
||||||
t.Fatalf("shouldn't have err: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAccessConfigPrepare_Region(t *testing.T) {
|
|
||||||
dfw := "DFW"
|
|
||||||
c := testAccessConfig()
|
|
||||||
c.RawRegion = dfw
|
|
||||||
if err := c.Prepare(nil); err != nil {
|
|
||||||
t.Fatalf("shouldn't have err: %s", err)
|
|
||||||
}
|
|
||||||
if dfw != c.Region() {
|
|
||||||
t.Fatalf("Regions do not match: %s %s", dfw, c.Region())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 {
|
||||||
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{} {
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
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,6 +20,7 @@ const BuilderId = "mitchellh.openstack"
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
common.PackerConfig `mapstructure:",squash"`
|
common.PackerConfig `mapstructure:",squash"`
|
||||||
|
|
||||||
AccessConfig `mapstructure:",squash"`
|
AccessConfig `mapstructure:",squash"`
|
||||||
ImageConfig `mapstructure:",squash"`
|
ImageConfig `mapstructure:",squash"`
|
||||||
RunConfig `mapstructure:",squash"`
|
RunConfig `mapstructure:",squash"`
|
||||||
|
@ -55,43 +56,35 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
||||||
auth, err := b.config.AccessConfig.Auth()
|
computeClient, err := b.config.computeV2Client()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("Error initializing compute client: %s", err)
|
||||||
}
|
|
||||||
//fetches the api requisites from gophercloud for the appropriate
|
|
||||||
//openstack variant
|
|
||||||
api, err := gophercloud.PopulateApi(b.config.RunConfig.OpenstackProvider)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
api.Region = b.config.AccessConfig.Region()
|
|
||||||
|
|
||||||
csp, err := gophercloud.ServersApi(auth, api)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Region: %s", b.config.AccessConfig.Region())
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup the state bag and initial state for the steps
|
// Setup the state bag and initial state for the steps
|
||||||
state := new(multistep.BasicStateBag)
|
state := new(multistep.BasicStateBag)
|
||||||
state.Put("config", b.config)
|
state.Put("config", b.config)
|
||||||
state.Put("csp", csp)
|
|
||||||
state.Put("hook", hook)
|
state.Put("hook", hook)
|
||||||
state.Put("ui", ui)
|
state.Put("ui", ui)
|
||||||
|
|
||||||
// Build the steps
|
// Build the steps
|
||||||
steps := []multistep.Step{
|
steps := []multistep.Step{
|
||||||
|
&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
|
||||||
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
|
||||||
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{} {
|
||||||
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) {
|
||||||
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
|
||||||
|
|
||||||
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 {
|
||||||
|
Comm communicator.Config `mapstructure:",squash"`
|
||||||
|
SSHInterface string `mapstructure:"ssh_interface"`
|
||||||
|
|
||||||
SourceImage string `mapstructure:"source_image"`
|
SourceImage string `mapstructure:"source_image"`
|
||||||
Flavor string `mapstructure:"flavor"`
|
Flavor string `mapstructure:"flavor"`
|
||||||
RawSSHTimeout string `mapstructure:"ssh_timeout"`
|
AvailabilityZone string `mapstructure:"availability_zone"`
|
||||||
SSHUsername string `mapstructure:"ssh_username"`
|
|
||||||
SSHPort int `mapstructure:"ssh_port"`
|
|
||||||
SSHInterface string `mapstructure:"ssh_interface"`
|
|
||||||
OpenstackProvider string `mapstructure:"openstack_provider"`
|
|
||||||
UseFloatingIp bool `mapstructure:"use_floating_ip"`
|
|
||||||
RackconnectWait bool `mapstructure:"rackconnect_wait"`
|
RackconnectWait bool `mapstructure:"rackconnect_wait"`
|
||||||
FloatingIpPool string `mapstructure:"floating_ip_pool"`
|
FloatingIpPool string `mapstructure:"floating_ip_pool"`
|
||||||
FloatingIp string `mapstructure:"floating_ip"`
|
FloatingIp string `mapstructure:"floating_ip"`
|
||||||
SecurityGroups []string `mapstructure:"security_groups"`
|
SecurityGroups []string `mapstructure:"security_groups"`
|
||||||
Networks []string `mapstructure:"networks"`
|
Networks []string `mapstructure:"networks"`
|
||||||
|
UserData string `mapstructure:"user_data"`
|
||||||
|
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
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
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mitchellh/packer/helper/communicator"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -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) {
|
||||||
|
|
||||||
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 {
|
if err := c.Prepare(nil); len(err) != 0 {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.SSHPort != 22 {
|
if c.Comm.SSHPort != 22 {
|
||||||
t.Fatalf("invalid value: %d", c.SSHPort)
|
t.Fatalf("invalid value: %d", c.Comm.SSHPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SSHPort = 44
|
c.Comm.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 != 44 {
|
||||||
t.Fatalf("invalid value: %d", c.SSHPort)
|
t.Fatalf("invalid value: %d", c.Comm.SSHPort)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunConfigPrepare_SSHTimeout(t *testing.T) {
|
|
||||||
c := testRunConfig()
|
|
||||||
c.RawSSHTimeout = ""
|
|
||||||
if err := c.Prepare(nil); len(err) != 0 {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.RawSSHTimeout = "bad"
|
|
||||||
if err := c.Prepare(nil); len(err) != 1 {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
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 {
|
||||||
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,9 +65,11 @@ func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentState == conf.Target {
|
for _, t := range conf.Target {
|
||||||
|
if currentState == t {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if conf.StepState != nil {
|
if conf.StepState != nil {
|
||||||
if _, ok := conf.StepState.GetOk(multistep.StateCancelled); ok {
|
if _, ok := conf.StepState.GetOk(multistep.StateCancelled); ok {
|
||||||
|
|
|
@ -1,51 +1,53 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
ip_pools, err := s.AllAddressPools()
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.New("Error parsing SSH addresses")
|
|
||||||
}
|
|
||||||
for pool, addresses := range ip_pools {
|
|
||||||
if sshinterface != "" {
|
if sshinterface != "" {
|
||||||
if pool != sshinterface {
|
if addr := sshAddrFromPool(s, sshinterface); addr != "" {
|
||||||
continue
|
return addr, nil
|
||||||
}
|
|
||||||
}
|
|
||||||
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)
|
// If we have a floating IP, use that
|
||||||
|
ip := state.Get("access_ip").(*floatingip.FloatingIP)
|
||||||
|
if ip != nil && ip.IP != "" {
|
||||||
|
return ip.IP, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.AccessIPv4 != "" {
|
||||||
|
return s.AccessIPv4, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get it from the requested interface
|
||||||
|
if addr := sshAddrFromPool(s, sshinterface); addr != "" {
|
||||||
|
return addr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := servers.Get(client, s.ID).Extract()
|
||||||
if err != nil {
|
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
|
||||||
}, 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
|
||||||
|
|
||||||
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 {
|
||||||
|
|
||||||
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
|
return
|
||||||
}
|
}
|
||||||
ui.Say(fmt.Sprintf("Deleted temporary floating IP %s", instanceIp.Ip))
|
|
||||||
|
if s.FloatingIpPool != "" && instanceIp.ID != "" {
|
||||||
|
if err := floatingip.Delete(client, instanceIp.ID).ExtractErr(); err != nil {
|
||||||
|
ui.Error(fmt.Sprintf(
|
||||||
|
"Error deleting temporary floating IP %s", instanceIp.IP))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Say(fmt.Sprintf("Deleted temporary floating IP %s", instanceIp.IP))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,28 +2,36 @@ package openstack
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/mitchellh/multistep"
|
|
||||||
"github.com/mitchellh/packer/packer"
|
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
|
"github.com/rackspace/gophercloud"
|
||||||
|
"github.com/rackspace/gophercloud/openstack/compute/v2/images"
|
||||||
|
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
|
||||||
)
|
)
|
||||||
|
|
||||||
type stepCreateImage struct{}
|
type stepCreateImage struct{}
|
||||||
|
|
||||||
func (s *stepCreateImage) Run(state multistep.StateBag) multistep.StepAction {
|
func (s *stepCreateImage) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
csp := state.Get("csp").(gophercloud.CloudServersProvider)
|
|
||||||
config := state.Get("config").(Config)
|
config := state.Get("config").(Config)
|
||||||
server := state.Get("server").(*gophercloud.Server)
|
server := state.Get("server").(*servers.Server)
|
||||||
ui := state.Get("ui").(packer.Ui)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
|
// We need the v2 compute client
|
||||||
|
client, err := config.computeV2Client()
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error initializing compute client: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
// Create the image
|
// Create the image
|
||||||
ui.Say(fmt.Sprintf("Creating the image: %s", config.ImageName))
|
ui.Say(fmt.Sprintf("Creating the image: %s", config.ImageName))
|
||||||
createOpts := gophercloud.CreateImage{
|
imageId, err := servers.CreateImage(client, server.ID, servers.CreateImageOpts{
|
||||||
Name: config.ImageName,
|
Name: config.ImageName,
|
||||||
}
|
}).ExtractImageID()
|
||||||
imageId, err := csp.CreateImage(server.Id, createOpts)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := fmt.Errorf("Error creating image: %s", err)
|
err := fmt.Errorf("Error creating image: %s", err)
|
||||||
state.Put("error", err)
|
state.Put("error", err)
|
||||||
|
@ -32,12 +40,12 @@ func (s *stepCreateImage) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the Image ID in the state
|
// Set the Image ID in the state
|
||||||
ui.Say(fmt.Sprintf("Image: %s", imageId))
|
ui.Message(fmt.Sprintf("Image: %s", imageId))
|
||||||
state.Put("image", imageId)
|
state.Put("image", imageId)
|
||||||
|
|
||||||
// Wait for the image to become ready
|
// Wait for the image to become ready
|
||||||
ui.Say("Waiting for image to become ready...")
|
ui.Say("Waiting for image to become ready...")
|
||||||
if err := WaitForImage(csp, imageId); err != nil {
|
if err := WaitForImage(client, imageId); err != nil {
|
||||||
err := fmt.Errorf("Error waiting for image: %s", err)
|
err := fmt.Errorf("Error waiting for image: %s", err)
|
||||||
state.Put("error", err)
|
state.Put("error", err)
|
||||||
ui.Error(err.Error())
|
ui.Error(err.Error())
|
||||||
|
@ -52,10 +60,17 @@ func (s *stepCreateImage) Cleanup(multistep.StateBag) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// WaitForImage waits for the given Image ID to become ready.
|
// WaitForImage waits for the given Image ID to become ready.
|
||||||
func WaitForImage(csp gophercloud.CloudServersProvider, imageId string) error {
|
func WaitForImage(client *gophercloud.ServiceClient, imageId string) error {
|
||||||
for {
|
for {
|
||||||
image, err := csp.ImageById(imageId)
|
image, err := images.Get(client, imageId).Extract()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError)
|
||||||
|
if ok && errCode.Actual == 500 {
|
||||||
|
log.Printf("[ERROR] 500 error received, will ignore and retry: %s", err)
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,13 @@ package openstack
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/mitchellh/multistep"
|
|
||||||
"github.com/mitchellh/packer/common/uuid"
|
|
||||||
"github.com/mitchellh/packer/packer"
|
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/common/uuid"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
|
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StepKeyPair struct {
|
type StepKeyPair struct {
|
||||||
|
@ -19,18 +18,28 @@ type StepKeyPair struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction {
|
func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
csp := state.Get("csp").(gophercloud.CloudServersProvider)
|
config := state.Get("config").(Config)
|
||||||
ui := state.Get("ui").(packer.Ui)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
|
// We need the v2 compute client
|
||||||
|
computeClient, err := config.computeV2Client()
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error initializing compute client: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
ui.Say("Creating temporary keypair for this instance...")
|
ui.Say("Creating temporary keypair for this instance...")
|
||||||
keyName := fmt.Sprintf("packer %s", uuid.TimeOrderedUUID())
|
keyName := fmt.Sprintf("packer %s", uuid.TimeOrderedUUID())
|
||||||
log.Printf("temporary keypair name: %s", keyName)
|
keypair, err := keypairs.Create(computeClient, keypairs.CreateOpts{
|
||||||
keyResp, err := csp.CreateKeyPair(gophercloud.NewKeyPair{Name: keyName})
|
Name: keyName,
|
||||||
|
}).Extract()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.Put("error", fmt.Errorf("Error creating temporary keypair: %s", err))
|
state.Put("error", fmt.Errorf("Error creating temporary keypair: %s", err))
|
||||||
return multistep.ActionHalt
|
return multistep.ActionHalt
|
||||||
}
|
}
|
||||||
if keyResp.PrivateKey == "" {
|
|
||||||
|
if keypair.PrivateKey == "" {
|
||||||
state.Put("error", fmt.Errorf("The temporary keypair returned was blank"))
|
state.Put("error", fmt.Errorf("The temporary keypair returned was blank"))
|
||||||
return multistep.ActionHalt
|
return multistep.ActionHalt
|
||||||
}
|
}
|
||||||
|
@ -47,7 +56,7 @@ func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
// Write the key out
|
// Write the key out
|
||||||
if _, err := f.Write([]byte(keyResp.PrivateKey)); err != nil {
|
if _, err := f.Write([]byte(keypair.PrivateKey)); err != nil {
|
||||||
state.Put("error", fmt.Errorf("Error saving debug key: %s", err))
|
state.Put("error", fmt.Errorf("Error saving debug key: %s", err))
|
||||||
return multistep.ActionHalt
|
return multistep.ActionHalt
|
||||||
}
|
}
|
||||||
|
@ -66,7 +75,7 @@ func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
|
|
||||||
// Set some state data for use in future steps
|
// Set some state data for use in future steps
|
||||||
state.Put("keyPair", keyName)
|
state.Put("keyPair", keyName)
|
||||||
state.Put("privateKey", keyResp.PrivateKey)
|
state.Put("privateKey", keypair.PrivateKey)
|
||||||
|
|
||||||
return multistep.ActionContinue
|
return multistep.ActionContinue
|
||||||
}
|
}
|
||||||
|
@ -77,11 +86,19 @@ func (s *StepKeyPair) Cleanup(state multistep.StateBag) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
csp := state.Get("csp").(gophercloud.CloudServersProvider)
|
config := state.Get("config").(Config)
|
||||||
ui := state.Get("ui").(packer.Ui)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
|
// We need the v2 compute client
|
||||||
|
computeClient, err := config.computeV2Client()
|
||||||
|
if err != nil {
|
||||||
|
ui.Error(fmt.Sprintf(
|
||||||
|
"Error cleaning up keypair. Please delete the key manually: %s", s.keyName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ui.Say("Deleting temporary keypair...")
|
ui.Say("Deleting temporary keypair...")
|
||||||
err := csp.DeleteKeyPair(s.keyName)
|
err = keypairs.Delete(computeClient, s.keyName).ExtractErr()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ui.Error(fmt.Sprintf(
|
ui.Error(fmt.Sprintf(
|
||||||
"Error cleaning up keypair. Please delete the key manually: %s", s.keyName))
|
"Error cleaning up keypair. Please delete the key manually: %s", s.keyName))
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
package openstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
|
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions"
|
||||||
|
"github.com/rackspace/gophercloud/pagination"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StepLoadExtensions gets the FlavorRef from a Flavor. It first assumes
|
||||||
|
// that the Flavor is a ref and verifies it. Otherwise, it tries to find
|
||||||
|
// the flavor by name.
|
||||||
|
type StepLoadExtensions struct{}
|
||||||
|
|
||||||
|
func (s *StepLoadExtensions) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
|
config := state.Get("config").(Config)
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
|
// We need the v2 compute client
|
||||||
|
client, err := config.computeV2Client()
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error initializing compute client: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Say("Discovering enabled extensions...")
|
||||||
|
result := make(map[string]struct{}, 15)
|
||||||
|
pager := extensions.List(client)
|
||||||
|
err = pager.EachPage(func(p pagination.Page) (bool, error) {
|
||||||
|
// Extract the extensions from this page
|
||||||
|
exts, err := extensions.ExtractExtensions(p)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ext := range exts {
|
||||||
|
log.Printf("[DEBUG] Discovered extension: %s", ext.Alias)
|
||||||
|
result[ext.Alias] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error loading extensions: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Put("extensions", result)
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepLoadExtensions) Cleanup(state multistep.StateBag) {
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package openstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
|
"github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StepLoadFlavor gets the FlavorRef from a Flavor. It first assumes
|
||||||
|
// that the Flavor is a ref and verifies it. Otherwise, it tries to find
|
||||||
|
// the flavor by name.
|
||||||
|
type StepLoadFlavor struct {
|
||||||
|
Flavor string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepLoadFlavor) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
|
config := state.Get("config").(Config)
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
|
// We need the v2 compute client
|
||||||
|
client, err := config.computeV2Client()
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error initializing compute client: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Say(fmt.Sprintf("Loading flavor: %s", s.Flavor))
|
||||||
|
log.Printf("[INFO] Loading flavor by ID: %s", s.Flavor)
|
||||||
|
flavor, err := flavors.Get(client, s.Flavor).Extract()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] Failed to find flavor by ID: %s", err)
|
||||||
|
geterr := err
|
||||||
|
|
||||||
|
log.Printf("[INFO] Loading flavor by name: %s", s.Flavor)
|
||||||
|
id, err := flavors.IDFromName(client, s.Flavor)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] Failed to find flavor by name: %s", err)
|
||||||
|
err = fmt.Errorf(
|
||||||
|
"Unable to find specified flavor by ID or name!\n\n"+
|
||||||
|
"Error from ID lookup: %s\n\n"+
|
||||||
|
"Error from name lookup: %s",
|
||||||
|
geterr,
|
||||||
|
err)
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
flavor = &flavors.Flavor{ID: id}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Message(fmt.Sprintf("Verified flavor. ID: %s", flavor.ID))
|
||||||
|
state.Put("flavor_id", flavor.ID)
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepLoadFlavor) Cleanup(state multistep.StateBag) {
|
||||||
|
}
|
|
@ -2,51 +2,70 @@ package openstack
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/mitchellh/multistep"
|
"io/ioutil"
|
||||||
"github.com/mitchellh/packer/packer"
|
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
|
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
|
||||||
|
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StepRunSourceServer struct {
|
type StepRunSourceServer struct {
|
||||||
Flavor string
|
|
||||||
Name string
|
Name string
|
||||||
SourceImage string
|
SourceImage string
|
||||||
SecurityGroups []string
|
SecurityGroups []string
|
||||||
Networks []string
|
Networks []string
|
||||||
|
AvailabilityZone string
|
||||||
|
UserData string
|
||||||
|
UserDataFile string
|
||||||
|
|
||||||
server *gophercloud.Server
|
server *servers.Server
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StepRunSourceServer) Run(state multistep.StateBag) multistep.StepAction {
|
func (s *StepRunSourceServer) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
csp := state.Get("csp").(gophercloud.CloudServersProvider)
|
config := state.Get("config").(Config)
|
||||||
|
flavor := state.Get("flavor_id").(string)
|
||||||
keyName := state.Get("keyPair").(string)
|
keyName := state.Get("keyPair").(string)
|
||||||
ui := state.Get("ui").(packer.Ui)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
// XXX - validate image and flavor is available
|
// We need the v2 compute client
|
||||||
|
computeClient, err := config.computeV2Client()
|
||||||
securityGroups := make([]map[string]interface{}, len(s.SecurityGroups))
|
if err != nil {
|
||||||
for i, groupName := range s.SecurityGroups {
|
err = fmt.Errorf("Error initializing compute client: %s", err)
|
||||||
securityGroups[i] = make(map[string]interface{})
|
state.Put("error", err)
|
||||||
securityGroups[i]["name"] = groupName
|
return multistep.ActionHalt
|
||||||
}
|
}
|
||||||
|
|
||||||
networks := make([]gophercloud.NetworkConfig, len(s.Networks))
|
networks := make([]servers.Network, len(s.Networks))
|
||||||
for i, networkUuid := range s.Networks {
|
for i, networkUuid := range s.Networks {
|
||||||
networks[i].Uuid = networkUuid
|
networks[i].UUID = networkUuid
|
||||||
}
|
}
|
||||||
|
|
||||||
server := gophercloud.NewServer{
|
userData := []byte(s.UserData)
|
||||||
|
if s.UserDataFile != "" {
|
||||||
|
userData, err = ioutil.ReadFile(s.UserDataFile)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error reading user data file: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Say("Launching server...")
|
||||||
|
s.server, err = servers.Create(computeClient, keypairs.CreateOptsExt{
|
||||||
|
CreateOptsBuilder: servers.CreateOpts{
|
||||||
Name: s.Name,
|
Name: s.Name,
|
||||||
ImageRef: s.SourceImage,
|
ImageRef: s.SourceImage,
|
||||||
FlavorRef: s.Flavor,
|
FlavorRef: flavor,
|
||||||
KeyPairName: keyName,
|
SecurityGroups: s.SecurityGroups,
|
||||||
SecurityGroup: securityGroups,
|
|
||||||
Networks: networks,
|
Networks: networks,
|
||||||
}
|
AvailabilityZone: s.AvailabilityZone,
|
||||||
|
UserData: userData,
|
||||||
|
},
|
||||||
|
|
||||||
serverResp, err := csp.CreateServer(server)
|
KeyName: keyName,
|
||||||
|
}).Extract()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := fmt.Errorf("Error launching source server: %s", err)
|
err := fmt.Errorf("Error launching source server: %s", err)
|
||||||
state.Put("error", err)
|
state.Put("error", err)
|
||||||
|
@ -54,25 +73,25 @@ func (s *StepRunSourceServer) Run(state multistep.StateBag) multistep.StepAction
|
||||||
return multistep.ActionHalt
|
return multistep.ActionHalt
|
||||||
}
|
}
|
||||||
|
|
||||||
s.server, err = csp.ServerById(serverResp.Id)
|
ui.Message(fmt.Sprintf("Server ID: %s", s.server.ID))
|
||||||
log.Printf("server id: %s", s.server.Id)
|
log.Printf("server id: %s", s.server.ID)
|
||||||
|
|
||||||
ui.Say(fmt.Sprintf("Waiting for server (%s) to become ready...", s.server.Id))
|
ui.Say("Waiting for server to become ready...")
|
||||||
stateChange := StateChangeConf{
|
stateChange := StateChangeConf{
|
||||||
Pending: []string{"BUILD"},
|
Pending: []string{"BUILD"},
|
||||||
Target: "ACTIVE",
|
Target: []string{"ACTIVE"},
|
||||||
Refresh: ServerStateRefreshFunc(csp, s.server),
|
Refresh: ServerStateRefreshFunc(computeClient, s.server),
|
||||||
StepState: state,
|
StepState: state,
|
||||||
}
|
}
|
||||||
latestServer, err := WaitForState(&stateChange)
|
latestServer, err := WaitForState(&stateChange)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := fmt.Errorf("Error waiting for server (%s) to become ready: %s", s.server.Id, err)
|
err := fmt.Errorf("Error waiting for server (%s) to become ready: %s", s.server.ID, err)
|
||||||
state.Put("error", err)
|
state.Put("error", err)
|
||||||
ui.Error(err.Error())
|
ui.Error(err.Error())
|
||||||
return multistep.ActionHalt
|
return multistep.ActionHalt
|
||||||
}
|
}
|
||||||
|
|
||||||
s.server = latestServer.(*gophercloud.Server)
|
s.server = latestServer.(*servers.Server)
|
||||||
state.Put("server", s.server)
|
state.Put("server", s.server)
|
||||||
|
|
||||||
return multistep.ActionContinue
|
return multistep.ActionContinue
|
||||||
|
@ -83,19 +102,26 @@ func (s *StepRunSourceServer) Cleanup(state multistep.StateBag) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
csp := state.Get("csp").(gophercloud.CloudServersProvider)
|
config := state.Get("config").(Config)
|
||||||
ui := state.Get("ui").(packer.Ui)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
|
// We need the v2 compute client
|
||||||
|
computeClient, err := config.computeV2Client()
|
||||||
|
if err != nil {
|
||||||
|
ui.Error(fmt.Sprintf("Error terminating server, may still be around: %s", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ui.Say("Terminating the source server...")
|
ui.Say("Terminating the source server...")
|
||||||
if err := csp.DeleteServerById(s.server.Id); err != nil {
|
if err := servers.Delete(computeClient, s.server.ID).ExtractErr(); err != nil {
|
||||||
ui.Error(fmt.Sprintf("Error terminating server, may still be around: %s", err))
|
ui.Error(fmt.Sprintf("Error terminating server, may still be around: %s", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stateChange := StateChangeConf{
|
stateChange := StateChangeConf{
|
||||||
Pending: []string{"ACTIVE", "BUILD", "REBUILD", "SUSPENDED"},
|
Pending: []string{"ACTIVE", "BUILD", "REBUILD", "SUSPENDED", "SHUTOFF", "STOPPED"},
|
||||||
Refresh: ServerStateRefreshFunc(csp, s.server),
|
Refresh: ServerStateRefreshFunc(computeClient, s.server),
|
||||||
Target: "DELETED",
|
Target: []string{"DELETED"},
|
||||||
}
|
}
|
||||||
|
|
||||||
WaitForState(&stateChange)
|
WaitForState(&stateChange)
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
package openstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
|
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop"
|
||||||
|
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StepStopServer struct{}
|
||||||
|
|
||||||
|
func (s *StepStopServer) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
config := state.Get("config").(Config)
|
||||||
|
extensions := state.Get("extensions").(map[string]struct{})
|
||||||
|
server := state.Get("server").(*servers.Server)
|
||||||
|
|
||||||
|
// Verify we have the extension
|
||||||
|
if _, ok := extensions["os-server-start-stop"]; !ok {
|
||||||
|
ui.Say("OpenStack cluster doesn't support stop, skipping...")
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need the v2 compute client
|
||||||
|
client, err := config.computeV2Client()
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error initializing compute client: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Say("Stopping server...")
|
||||||
|
if err := startstop.Stop(client, server.ID).ExtractErr(); err != nil {
|
||||||
|
err = fmt.Errorf("Error stopping server: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Message("Waiting for server to stop...")
|
||||||
|
stateChange := StateChangeConf{
|
||||||
|
Pending: []string{"ACTIVE"},
|
||||||
|
Target: []string{"SHUTOFF", "STOPPED"},
|
||||||
|
Refresh: ServerStateRefreshFunc(client, server),
|
||||||
|
StepState: state,
|
||||||
|
}
|
||||||
|
if _, err := WaitForState(&stateChange); err != nil {
|
||||||
|
err := fmt.Errorf("Error waiting for server (%s) to stop: %s", server.ID, err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepStopServer) Cleanup(state multistep.StateBag) {}
|
|
@ -2,11 +2,11 @@ package openstack
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/mitchellh/multistep"
|
|
||||||
"github.com/mitchellh/packer/packer"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
|
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StepWaitForRackConnect struct {
|
type StepWaitForRackConnect struct {
|
||||||
|
@ -18,14 +18,22 @@ func (s *StepWaitForRackConnect) Run(state multistep.StateBag) multistep.StepAct
|
||||||
return multistep.ActionContinue
|
return multistep.ActionContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
csp := state.Get("csp").(gophercloud.CloudServersProvider)
|
config := state.Get("config").(Config)
|
||||||
server := state.Get("server").(*gophercloud.Server)
|
server := state.Get("server").(*servers.Server)
|
||||||
ui := state.Get("ui").(packer.Ui)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
ui.Say(fmt.Sprintf("Waiting for server (%s) to become RackConnect ready...", server.Id))
|
// We need the v2 compute client
|
||||||
|
computeClient, err := config.computeV2Client()
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error initializing compute client: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Say(fmt.Sprintf(
|
||||||
|
"Waiting for server (%s) to become RackConnect ready...", server.ID))
|
||||||
for {
|
for {
|
||||||
server, err := csp.ServerById(server.Id)
|
server, err = servers.Get(computeClient, server.ID).Extract()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return multistep.ActionHalt
|
return multistep.ActionHalt
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,6 +73,12 @@ func NewDriver() (Driver, error) {
|
||||||
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,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh"
|
|
||||||
"github.com/mitchellh/multistep"
|
"github.com/mitchellh/multistep"
|
||||||
commonssh "github.com/mitchellh/packer/common/ssh"
|
commonssh "github.com/mitchellh/packer/common/ssh"
|
||||||
packerssh "github.com/mitchellh/packer/communicator/ssh"
|
packerssh "github.com/mitchellh/packer/communicator/ssh"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SSHAddress(state multistep.StateBag) (string, error) {
|
func CommHost(state multistep.StateBag) (string, error) {
|
||||||
vmName := state.Get("vmName").(string)
|
vmName := state.Get("vmName").(string)
|
||||||
driver := state.Get("driver").(Driver)
|
driver := state.Get("driver").(Driver)
|
||||||
|
|
||||||
|
@ -23,19 +21,19 @@ func SSHAddress(state multistep.StateBag) (string, error) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%s:22", ip), nil
|
return ip, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*ssh.ClientConfig, error) {
|
func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*ssh.ClientConfig, error) {
|
||||||
return func(state multistep.StateBag) (*ssh.ClientConfig, error) {
|
return func(state multistep.StateBag) (*ssh.ClientConfig, error) {
|
||||||
auth := []ssh.AuthMethod{
|
auth := []ssh.AuthMethod{
|
||||||
ssh.Password(config.SSHPassword),
|
ssh.Password(config.Comm.SSHPassword),
|
||||||
ssh.KeyboardInteractive(
|
ssh.KeyboardInteractive(
|
||||||
packerssh.PasswordKeyboardInteractive(config.SSHPassword)),
|
packerssh.PasswordKeyboardInteractive(config.Comm.SSHPassword)),
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.SSHKeyPath != "" {
|
if config.SSHKeyPath != "" {
|
||||||
signer, err := commonssh.FileSigner(config.SSHKeyPath)
|
signer, err := commonssh.FileSigner(config.Comm.SSHPrivateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -44,7 +42,7 @@ func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*ssh.ClientConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ssh.ClientConfig{
|
return &ssh.ClientConfig{
|
||||||
User: config.SSHUser,
|
User: config.Comm.SSHUsername,
|
||||||
Auth: auth,
|
Auth: auth,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,52 +1,29 @@
|
||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
commonssh "github.com/mitchellh/packer/common/ssh"
|
"github.com/mitchellh/packer/helper/communicator"
|
||||||
"github.com/mitchellh/packer/template/interpolate"
|
"github.com/mitchellh/packer/template/interpolate"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SSHConfig struct {
|
type SSHConfig struct {
|
||||||
SSHKeyPath string `mapstructure:"ssh_key_path"`
|
Comm communicator.Config `mapstructure:",squash"`
|
||||||
SSHPassword string `mapstructure:"ssh_password"`
|
|
||||||
SSHPort uint `mapstructure:"ssh_port"`
|
|
||||||
SSHUser string `mapstructure:"ssh_username"`
|
|
||||||
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
|
|
||||||
|
|
||||||
SSHWaitTimeout time.Duration
|
// These are deprecated, but we keep them around for BC
|
||||||
|
// TODO(@mitchellh): remove
|
||||||
|
SSHKeyPath string `mapstructure:"ssh_key_path"`
|
||||||
|
SSHWaitTimeout time.Duration `mapstructure:"ssh_wait_timeout"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *SSHConfig) Prepare(ctx *interpolate.Context) []error {
|
func (c *SSHConfig) Prepare(ctx *interpolate.Context) []error {
|
||||||
if c.SSHPort == 0 {
|
// TODO: backwards compatibility, write fixer instead
|
||||||
c.SSHPort = 22
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.RawSSHWaitTimeout == "" {
|
|
||||||
c.RawSSHWaitTimeout = "20m"
|
|
||||||
}
|
|
||||||
|
|
||||||
var errs []error
|
|
||||||
if c.SSHKeyPath != "" {
|
if c.SSHKeyPath != "" {
|
||||||
if _, err := os.Stat(c.SSHKeyPath); err != nil {
|
c.Comm.SSHPrivateKey = c.SSHKeyPath
|
||||||
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
|
|
||||||
} else if _, err := commonssh.FileSigner(c.SSHKeyPath); err != nil {
|
|
||||||
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
|
|
||||||
}
|
}
|
||||||
|
if c.SSHWaitTimeout != 0 {
|
||||||
|
c.Comm.SSHTimeout = c.SSHWaitTimeout
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.SSHUser == "" {
|
return c.Comm.Prepare(ctx)
|
||||||
errs = append(errs, errors.New("An ssh_username must be specified."))
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
c.SSHWaitTimeout, err = time.ParseDuration(c.RawSSHWaitTimeout)
|
|
||||||
if err != nil {
|
|
||||||
errs = append(errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return errs
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,15 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mitchellh/packer/helper/communicator"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testSSHConfig() *SSHConfig {
|
func testSSHConfig() *SSHConfig {
|
||||||
return &SSHConfig{
|
return &SSHConfig{
|
||||||
SSHUser: "foo",
|
Comm: communicator.Config{
|
||||||
|
SSHUsername: "foo",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,8 +23,8 @@ func TestSSHConfigPrepare(t *testing.T) {
|
||||||
t.Fatalf("err: %#v", errs)
|
t.Fatalf("err: %#v", errs)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.SSHPort != 22 {
|
if c.Comm.SSHPort != 22 {
|
||||||
t.Errorf("bad ssh port: %d", c.SSHPort)
|
t.Errorf("bad ssh port: %d", c.Comm.SSHPort)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,46 +82,14 @@ func TestSSHConfigPrepare_SSHUser(t *testing.T) {
|
||||||
var errs []error
|
var errs []error
|
||||||
|
|
||||||
c = testSSHConfig()
|
c = testSSHConfig()
|
||||||
c.SSHUser = ""
|
c.Comm.SSHUsername = ""
|
||||||
errs = c.Prepare(testConfigTemplate(t))
|
errs = c.Prepare(testConfigTemplate(t))
|
||||||
if len(errs) == 0 {
|
if len(errs) == 0 {
|
||||||
t.Fatalf("should have error")
|
t.Fatalf("should have error")
|
||||||
}
|
}
|
||||||
|
|
||||||
c = testSSHConfig()
|
c = testSSHConfig()
|
||||||
c.SSHUser = "exists"
|
c.Comm.SSHUsername = "exists"
|
||||||
errs = c.Prepare(testConfigTemplate(t))
|
|
||||||
if len(errs) > 0 {
|
|
||||||
t.Fatalf("should not have error: %#v", errs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSSHConfigPrepare_SSHWaitTimeout(t *testing.T) {
|
|
||||||
var c *SSHConfig
|
|
||||||
var errs []error
|
|
||||||
|
|
||||||
// Defaults
|
|
||||||
c = testSSHConfig()
|
|
||||||
c.RawSSHWaitTimeout = ""
|
|
||||||
errs = c.Prepare(testConfigTemplate(t))
|
|
||||||
if len(errs) > 0 {
|
|
||||||
t.Fatalf("should not have error: %#v", errs)
|
|
||||||
}
|
|
||||||
if c.RawSSHWaitTimeout != "20m" {
|
|
||||||
t.Fatalf("bad value: %s", c.RawSSHWaitTimeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with a bad value
|
|
||||||
c = testSSHConfig()
|
|
||||||
c.RawSSHWaitTimeout = "this is not good"
|
|
||||||
errs = c.Prepare(testConfigTemplate(t))
|
|
||||||
if len(errs) == 0 {
|
|
||||||
t.Fatal("should have error")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with a good one
|
|
||||||
c = testSSHConfig()
|
|
||||||
c.RawSSHWaitTimeout = "5s"
|
|
||||||
errs = c.Prepare(testConfigTemplate(t))
|
errs = c.Prepare(testConfigTemplate(t))
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
t.Fatalf("should not have error: %#v", errs)
|
t.Fatalf("should not have error: %#v", errs)
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/mitchellh/multistep"
|
"github.com/mitchellh/multistep"
|
||||||
parallelscommon "github.com/mitchellh/packer/builder/parallels/common"
|
parallelscommon "github.com/mitchellh/packer/builder/parallels/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"
|
||||||
|
@ -245,10 +246,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||||
VMName: b.config.VMName,
|
VMName: b.config.VMName,
|
||||||
Ctx: b.config.ctx,
|
Ctx: b.config.ctx,
|
||||||
},
|
},
|
||||||
&common.StepConnectSSH{
|
&communicator.StepConnect{
|
||||||
SSHAddress: parallelscommon.SSHAddress,
|
Config: &b.config.SSHConfig.Comm,
|
||||||
|
Host: parallelscommon.CommHost,
|
||||||
SSHConfig: parallelscommon.SSHConfigFunc(b.config.SSHConfig),
|
SSHConfig: parallelscommon.SSHConfigFunc(b.config.SSHConfig),
|
||||||
SSHWaitTimeout: b.config.SSHWaitTimeout,
|
|
||||||
},
|
},
|
||||||
¶llelscommon.StepUploadVersion{
|
¶llelscommon.StepUploadVersion{
|
||||||
Path: b.config.PrlctlVersionFile,
|
Path: b.config.PrlctlVersionFile,
|
||||||
|
|
|
@ -3,11 +3,13 @@ package pvm
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
"github.com/mitchellh/multistep"
|
"github.com/mitchellh/multistep"
|
||||||
parallelscommon "github.com/mitchellh/packer/builder/parallels/common"
|
parallelscommon "github.com/mitchellh/packer/builder/parallels/common"
|
||||||
"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"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Builder implements packer.Builder and builds the actual Parallels
|
// Builder implements packer.Builder and builds the actual Parallels
|
||||||
|
@ -80,10 +82,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||||
VMName: b.config.VMName,
|
VMName: b.config.VMName,
|
||||||
Ctx: b.config.ctx,
|
Ctx: b.config.ctx,
|
||||||
},
|
},
|
||||||
&common.StepConnectSSH{
|
&communicator.StepConnect{
|
||||||
SSHAddress: parallelscommon.SSHAddress,
|
Config: &b.config.SSHConfig.Comm,
|
||||||
|
Host: parallelscommon.CommHost,
|
||||||
SSHConfig: parallelscommon.SSHConfigFunc(b.config.SSHConfig),
|
SSHConfig: parallelscommon.SSHConfigFunc(b.config.SSHConfig),
|
||||||
SSHWaitTimeout: b.config.SSHWaitTimeout,
|
|
||||||
},
|
},
|
||||||
¶llelscommon.StepUploadVersion{
|
¶llelscommon.StepUploadVersion{
|
||||||
Path: b.config.PrlctlVersionFile,
|
Path: b.config.PrlctlVersionFile,
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -12,7 +12,7 @@ import (
|
||||||
|
|
||||||
"github.com/mitchellh/multistep"
|
"github.com/mitchellh/multistep"
|
||||||
"github.com/mitchellh/packer/common"
|
"github.com/mitchellh/packer/common"
|
||||||
commonssh "github.com/mitchellh/packer/common/ssh"
|
"github.com/mitchellh/packer/helper/communicator"
|
||||||
"github.com/mitchellh/packer/helper/config"
|
"github.com/mitchellh/packer/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 +78,7 @@ type Builder struct {
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
common.PackerConfig `mapstructure:",squash"`
|
common.PackerConfig `mapstructure:",squash"`
|
||||||
|
Comm communicator.Config `mapstructure:",squash"`
|
||||||
|
|
||||||
Accelerator string `mapstructure:"accelerator"`
|
Accelerator string `mapstructure:"accelerator"`
|
||||||
BootCommand []string `mapstructure:"boot_command"`
|
BootCommand []string `mapstructure:"boot_command"`
|
||||||
|
@ -103,25 +104,24 @@ type Config struct {
|
||||||
ShutdownCommand string `mapstructure:"shutdown_command"`
|
ShutdownCommand string `mapstructure:"shutdown_command"`
|
||||||
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
|
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
|
||||||
SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
|
SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
|
||||||
SSHPassword string `mapstructure:"ssh_password"`
|
|
||||||
SSHPort uint `mapstructure:"ssh_port"`
|
|
||||||
SSHUser string `mapstructure:"ssh_username"`
|
|
||||||
SSHKeyPath string `mapstructure:"ssh_key_path"`
|
|
||||||
VNCPortMin uint `mapstructure:"vnc_port_min"`
|
VNCPortMin uint `mapstructure:"vnc_port_min"`
|
||||||
VNCPortMax uint `mapstructure:"vnc_port_max"`
|
VNCPortMax uint `mapstructure:"vnc_port_max"`
|
||||||
VMName string `mapstructure:"vm_name"`
|
VMName string `mapstructure:"vm_name"`
|
||||||
|
|
||||||
|
// These are deprecated, but we keep them around for BC
|
||||||
|
// TODO(@mitchellh): remove
|
||||||
|
SSHKeyPath string `mapstructure:"ssh_key_path"`
|
||||||
|
SSHWaitTimeout time.Duration `mapstructure:"ssh_wait_timeout"`
|
||||||
|
|
||||||
// TODO(mitchellh): deprecate
|
// TODO(mitchellh): deprecate
|
||||||
RunOnce bool `mapstructure:"run_once"`
|
RunOnce bool `mapstructure:"run_once"`
|
||||||
|
|
||||||
RawBootWait string `mapstructure:"boot_wait"`
|
RawBootWait string `mapstructure:"boot_wait"`
|
||||||
RawSingleISOUrl string `mapstructure:"iso_url"`
|
RawSingleISOUrl string `mapstructure:"iso_url"`
|
||||||
RawShutdownTimeout string `mapstructure:"shutdown_timeout"`
|
RawShutdownTimeout string `mapstructure:"shutdown_timeout"`
|
||||||
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
|
|
||||||
|
|
||||||
bootWait time.Duration ``
|
bootWait time.Duration ``
|
||||||
shutdownTimeout time.Duration ``
|
shutdownTimeout time.Duration ``
|
||||||
sshWaitTimeout time.Duration ``
|
|
||||||
ctx interpolate.Context
|
ctx interpolate.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,9 +139,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var errs *packer.MultiError
|
|
||||||
warnings := make([]string, 0)
|
|
||||||
|
|
||||||
if b.config.DiskSize == 0 {
|
if b.config.DiskSize == 0 {
|
||||||
b.config.DiskSize = 40000
|
b.config.DiskSize = 40000
|
||||||
}
|
}
|
||||||
|
@ -190,10 +187,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||||
b.config.SSHHostPortMax = 4444
|
b.config.SSHHostPortMax = 4444
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.config.SSHPort == 0 {
|
|
||||||
b.config.SSHPort = 22
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.config.VNCPortMin == 0 {
|
if b.config.VNCPortMin == 0 {
|
||||||
b.config.VNCPortMin = 5900
|
b.config.VNCPortMin = 5900
|
||||||
}
|
}
|
||||||
|
@ -222,6 +215,21 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||||
b.config.DiskInterface = "virtio"
|
b.config.DiskInterface = "virtio"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: backwards compatibility, write fixer instead
|
||||||
|
if b.config.SSHKeyPath != "" {
|
||||||
|
b.config.Comm.SSHPrivateKey = b.config.SSHKeyPath
|
||||||
|
}
|
||||||
|
if b.config.SSHWaitTimeout != 0 {
|
||||||
|
b.config.Comm.SSHTimeout = b.config.SSHWaitTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
var errs *packer.MultiError
|
||||||
|
warnings := make([]string, 0)
|
||||||
|
|
||||||
|
if es := b.config.Comm.Prepare(&b.config.ctx); len(es) > 0 {
|
||||||
|
errs = packer.MultiErrorAppend(errs, es...)
|
||||||
|
}
|
||||||
|
|
||||||
if !(b.config.Format == "qcow2" || b.config.Format == "raw") {
|
if !(b.config.Format == "qcow2" || b.config.Format == "raw") {
|
||||||
errs = packer.MultiErrorAppend(
|
errs = packer.MultiErrorAppend(
|
||||||
errs, errors.New("invalid format, only 'qcow2' or 'raw' are allowed"))
|
errs, errors.New("invalid format, only 'qcow2' or 'raw' are allowed"))
|
||||||
|
@ -314,42 +322,17 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||||
b.config.RawShutdownTimeout = "5m"
|
b.config.RawShutdownTimeout = "5m"
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.config.RawSSHWaitTimeout == "" {
|
|
||||||
b.config.RawSSHWaitTimeout = "20m"
|
|
||||||
}
|
|
||||||
|
|
||||||
b.config.shutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout)
|
b.config.shutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = packer.MultiErrorAppend(
|
errs = packer.MultiErrorAppend(
|
||||||
errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
|
errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.config.SSHKeyPath != "" {
|
|
||||||
if _, err := os.Stat(b.config.SSHKeyPath); err != nil {
|
|
||||||
errs = packer.MultiErrorAppend(
|
|
||||||
errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
|
|
||||||
} else if _, err := commonssh.FileSigner(b.config.SSHKeyPath); err != nil {
|
|
||||||
errs = packer.MultiErrorAppend(
|
|
||||||
errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.config.SSHHostPortMin > b.config.SSHHostPortMax {
|
if b.config.SSHHostPortMin > b.config.SSHHostPortMax {
|
||||||
errs = packer.MultiErrorAppend(
|
errs = packer.MultiErrorAppend(
|
||||||
errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
|
errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.config.SSHUser == "" {
|
|
||||||
errs = packer.MultiErrorAppend(
|
|
||||||
errs, errors.New("An ssh_username must be specified."))
|
|
||||||
}
|
|
||||||
|
|
||||||
b.config.sshWaitTimeout, err = time.ParseDuration(b.config.RawSSHWaitTimeout)
|
|
||||||
if err != nil {
|
|
||||||
errs = packer.MultiErrorAppend(
|
|
||||||
errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.config.VNCPortMin > b.config.VNCPortMax {
|
if b.config.VNCPortMin > b.config.VNCPortMax {
|
||||||
errs = packer.MultiErrorAppend(
|
errs = packer.MultiErrorAppend(
|
||||||
errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
|
errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
|
||||||
|
@ -409,10 +392,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||||
steprun,
|
steprun,
|
||||||
&stepBootWait{},
|
&stepBootWait{},
|
||||||
&stepTypeBootCommand{},
|
&stepTypeBootCommand{},
|
||||||
&common.StepConnectSSH{
|
&communicator.StepConnect{
|
||||||
SSHAddress: sshAddress,
|
Config: &b.config.Comm,
|
||||||
|
Host: commHost,
|
||||||
SSHConfig: sshConfig,
|
SSHConfig: sshConfig,
|
||||||
SSHWaitTimeout: b.config.sshWaitTimeout,
|
SSHPort: commPort,
|
||||||
},
|
},
|
||||||
new(common.StepProvision),
|
new(common.StepProvision),
|
||||||
new(stepShutdown),
|
new(stepShutdown),
|
||||||
|
|
|
@ -79,8 +79,8 @@ func TestBuilderPrepare_Defaults(t *testing.T) {
|
||||||
t.Errorf("bad max ssh host port: %d", b.config.SSHHostPortMax)
|
t.Errorf("bad max ssh host port: %d", b.config.SSHHostPortMax)
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.config.SSHPort != 22 {
|
if b.config.Comm.SSHPort != 22 {
|
||||||
t.Errorf("bad ssh port: %d", b.config.SSHPort)
|
t.Errorf("bad ssh port: %d", b.config.Comm.SSHPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.config.VMName != "packer-foo" {
|
if b.config.VMName != "packer-foo" {
|
||||||
|
@ -595,10 +595,6 @@ func TestBuilderPrepare_SSHWaitTimeout(t *testing.T) {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.config.RawSSHWaitTimeout != "20m" {
|
|
||||||
t.Fatalf("bad value: %s", b.config.RawSSHWaitTimeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with a bad value
|
// Test with a bad value
|
||||||
config["ssh_wait_timeout"] = "this is not good"
|
config["ssh_wait_timeout"] = "this is not good"
|
||||||
b = Builder{}
|
b = Builder{}
|
||||||
|
|
|
@ -1,30 +1,32 @@
|
||||||
package qemu
|
package qemu
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
gossh "golang.org/x/crypto/ssh"
|
|
||||||
"github.com/mitchellh/multistep"
|
"github.com/mitchellh/multistep"
|
||||||
commonssh "github.com/mitchellh/packer/common/ssh"
|
commonssh "github.com/mitchellh/packer/common/ssh"
|
||||||
"github.com/mitchellh/packer/communicator/ssh"
|
"github.com/mitchellh/packer/communicator/ssh"
|
||||||
|
gossh "golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
func sshAddress(state multistep.StateBag) (string, error) {
|
func commHost(state multistep.StateBag) (string, error) {
|
||||||
|
return "127.0.0.1", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func commPort(state multistep.StateBag) (int, error) {
|
||||||
sshHostPort := state.Get("sshHostPort").(uint)
|
sshHostPort := state.Get("sshHostPort").(uint)
|
||||||
return fmt.Sprintf("127.0.0.1:%d", sshHostPort), nil
|
return int(sshHostPort), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func sshConfig(state multistep.StateBag) (*gossh.ClientConfig, error) {
|
func sshConfig(state multistep.StateBag) (*gossh.ClientConfig, error) {
|
||||||
config := state.Get("config").(*Config)
|
config := state.Get("config").(*Config)
|
||||||
|
|
||||||
auth := []gossh.AuthMethod{
|
auth := []gossh.AuthMethod{
|
||||||
gossh.Password(config.SSHPassword),
|
gossh.Password(config.Comm.SSHPassword),
|
||||||
gossh.KeyboardInteractive(
|
gossh.KeyboardInteractive(
|
||||||
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
|
ssh.PasswordKeyboardInteractive(config.Comm.SSHPassword)),
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.SSHKeyPath != "" {
|
if config.Comm.SSHPrivateKey != "" {
|
||||||
signer, err := commonssh.FileSigner(config.SSHKeyPath)
|
signer, err := commonssh.FileSigner(config.Comm.SSHPrivateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -33,7 +35,7 @@ func sshConfig(state multistep.StateBag) (*gossh.ClientConfig, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return &gossh.ClientConfig{
|
return &gossh.ClientConfig{
|
||||||
User: config.SSHUser,
|
User: config.Comm.SSHUsername,
|
||||||
Auth: auth,
|
Auth: auth,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -2,7 +2,6 @@ package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/mitchellh/packer/common"
|
"github.com/mitchellh/packer/common"
|
||||||
"github.com/mitchellh/packer/template/interpolate"
|
"github.com/mitchellh/packer/template/interpolate"
|
||||||
|
@ -17,13 +16,5 @@ func (c *OutputConfig) Prepare(ctx *interpolate.Context, pc *common.PackerConfig
|
||||||
c.OutputDir = fmt.Sprintf("output-%s", pc.PackerBuildName)
|
c.OutputDir = fmt.Sprintf("output-%s", pc.PackerBuildName)
|
||||||
}
|
}
|
||||||
|
|
||||||
var errs []error
|
return nil
|
||||||
if !pc.PackerForce {
|
|
||||||
if _, err := os.Stat(c.OutputDir); err == nil {
|
|
||||||
errs = append(errs, fmt.Errorf(
|
|
||||||
"Output directory '%s' already exists. It must not exist.", c.OutputDir))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errs
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,27 +39,7 @@ func TestOutputConfigPrepare_exists(t *testing.T) {
|
||||||
PackerForce: false,
|
PackerForce: false,
|
||||||
}
|
}
|
||||||
errs := c.Prepare(testConfigTemplate(t), pc)
|
errs := c.Prepare(testConfigTemplate(t), pc)
|
||||||
if len(errs) == 0 {
|
if len(errs) != 0 {
|
||||||
t.Fatal("should have errors")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOutputConfigPrepare_forceExists(t *testing.T) {
|
|
||||||
td, err := ioutil.TempDir("", "packer")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(td)
|
|
||||||
|
|
||||||
c := new(OutputConfig)
|
|
||||||
c.OutputDir = td
|
|
||||||
|
|
||||||
pc := &common.PackerConfig{
|
|
||||||
PackerBuildName: "foo",
|
|
||||||
PackerForce: true,
|
|
||||||
}
|
|
||||||
errs := c.Prepare(testConfigTemplate(t), pc)
|
|
||||||
if len(errs) > 0 {
|
|
||||||
t.Fatal("should not have errors")
|
t.Fatal("should not have errors")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,31 @@
|
||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
gossh "golang.org/x/crypto/ssh"
|
|
||||||
"github.com/mitchellh/multistep"
|
"github.com/mitchellh/multistep"
|
||||||
commonssh "github.com/mitchellh/packer/common/ssh"
|
commonssh "github.com/mitchellh/packer/common/ssh"
|
||||||
"github.com/mitchellh/packer/communicator/ssh"
|
"github.com/mitchellh/packer/communicator/ssh"
|
||||||
|
gossh "golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SSHAddress(state multistep.StateBag) (string, error) {
|
func CommHost(state multistep.StateBag) (string, error) {
|
||||||
|
return "127.0.0.1", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SSHPort(state multistep.StateBag) (int, error) {
|
||||||
sshHostPort := state.Get("sshHostPort").(uint)
|
sshHostPort := state.Get("sshHostPort").(uint)
|
||||||
return fmt.Sprintf("127.0.0.1:%d", sshHostPort), nil
|
return int(sshHostPort), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*gossh.ClientConfig, error) {
|
func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*gossh.ClientConfig, error) {
|
||||||
return func(state multistep.StateBag) (*gossh.ClientConfig, error) {
|
return func(state multistep.StateBag) (*gossh.ClientConfig, error) {
|
||||||
auth := []gossh.AuthMethod{
|
auth := []gossh.AuthMethod{
|
||||||
gossh.Password(config.SSHPassword),
|
gossh.Password(config.Comm.SSHPassword),
|
||||||
gossh.KeyboardInteractive(
|
gossh.KeyboardInteractive(
|
||||||
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
|
ssh.PasswordKeyboardInteractive(config.Comm.SSHPassword)),
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.SSHKeyPath != "" {
|
if config.SSHKeyPath != "" {
|
||||||
signer, err := commonssh.FileSigner(config.SSHKeyPath)
|
signer, err := commonssh.FileSigner(config.Comm.SSHPrivateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -32,7 +34,7 @@ func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*gossh.ClientConf
|
||||||
}
|
}
|
||||||
|
|
||||||
return &gossh.ClientConfig{
|
return &gossh.ClientConfig{
|
||||||
User: config.SSHUser,
|
User: config.Comm.SSHUsername,
|
||||||
Auth: auth,
|
Auth: auth,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,24 +2,23 @@ package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
commonssh "github.com/mitchellh/packer/common/ssh"
|
"github.com/mitchellh/packer/helper/communicator"
|
||||||
"github.com/mitchellh/packer/template/interpolate"
|
"github.com/mitchellh/packer/template/interpolate"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SSHConfig struct {
|
type SSHConfig struct {
|
||||||
|
Comm communicator.Config `mapstructure:",squash"`
|
||||||
|
|
||||||
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
|
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
|
||||||
SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
|
SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
|
||||||
SSHKeyPath string `mapstructure:"ssh_key_path"`
|
SSHSkipNatMapping bool `mapstructure:"ssh_skip_nat_mapping"`
|
||||||
SSHPassword string `mapstructure:"ssh_password"`
|
|
||||||
SSHPort uint `mapstructure:"ssh_port"`
|
|
||||||
SSHUser string `mapstructure:"ssh_username"`
|
|
||||||
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
|
|
||||||
|
|
||||||
SSHWaitTimeout time.Duration
|
// These are deprecated, but we keep them around for BC
|
||||||
|
// TODO(@mitchellh): remove
|
||||||
|
SSHKeyPath string `mapstructure:"ssh_key_path"`
|
||||||
|
SSHWaitTimeout time.Duration `mapstructure:"ssh_wait_timeout"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *SSHConfig) Prepare(ctx *interpolate.Context) []error {
|
func (c *SSHConfig) Prepare(ctx *interpolate.Context) []error {
|
||||||
|
@ -31,37 +30,19 @@ func (c *SSHConfig) Prepare(ctx *interpolate.Context) []error {
|
||||||
c.SSHHostPortMax = 4444
|
c.SSHHostPortMax = 4444
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.SSHPort == 0 {
|
// TODO: backwards compatibility, write fixer instead
|
||||||
c.SSHPort = 22
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.RawSSHWaitTimeout == "" {
|
|
||||||
c.RawSSHWaitTimeout = "20m"
|
|
||||||
}
|
|
||||||
|
|
||||||
var errs []error
|
|
||||||
if c.SSHKeyPath != "" {
|
if c.SSHKeyPath != "" {
|
||||||
if _, err := os.Stat(c.SSHKeyPath); err != nil {
|
c.Comm.SSHPrivateKey = c.SSHKeyPath
|
||||||
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
|
|
||||||
} else if _, err := commonssh.FileSigner(c.SSHKeyPath); err != nil {
|
|
||||||
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
|
|
||||||
}
|
}
|
||||||
|
if c.SSHWaitTimeout != 0 {
|
||||||
|
c.Comm.SSHTimeout = c.SSHWaitTimeout
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errs := c.Comm.Prepare(ctx)
|
||||||
if c.SSHHostPortMin > c.SSHHostPortMax {
|
if c.SSHHostPortMin > c.SSHHostPortMax {
|
||||||
errs = append(errs,
|
errs = append(errs,
|
||||||
errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
|
errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.SSHUser == "" {
|
|
||||||
errs = append(errs, errors.New("An ssh_username must be specified."))
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
c.SSHWaitTimeout, err = time.ParseDuration(c.RawSSHWaitTimeout)
|
|
||||||
if err != nil {
|
|
||||||
errs = append(errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return errs
|
return errs
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,15 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mitchellh/packer/helper/communicator"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testSSHConfig() *SSHConfig {
|
func testSSHConfig() *SSHConfig {
|
||||||
return &SSHConfig{
|
return &SSHConfig{
|
||||||
SSHUser: "foo",
|
Comm: communicator.Config{
|
||||||
|
SSHUsername: "foo",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,8 +31,8 @@ func TestSSHConfigPrepare(t *testing.T) {
|
||||||
t.Errorf("bad max ssh host port: %d", c.SSHHostPortMax)
|
t.Errorf("bad max ssh host port: %d", c.SSHHostPortMax)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.SSHPort != 22 {
|
if c.Comm.SSHPort != 22 {
|
||||||
t.Errorf("bad ssh port: %d", c.SSHPort)
|
t.Errorf("bad ssh port: %d", c.Comm.SSHPort)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,46 +113,14 @@ func TestSSHConfigPrepare_SSHUser(t *testing.T) {
|
||||||
var errs []error
|
var errs []error
|
||||||
|
|
||||||
c = testSSHConfig()
|
c = testSSHConfig()
|
||||||
c.SSHUser = ""
|
c.Comm.SSHUsername = ""
|
||||||
errs = c.Prepare(testConfigTemplate(t))
|
errs = c.Prepare(testConfigTemplate(t))
|
||||||
if len(errs) == 0 {
|
if len(errs) == 0 {
|
||||||
t.Fatalf("should have error")
|
t.Fatalf("should have error")
|
||||||
}
|
}
|
||||||
|
|
||||||
c = testSSHConfig()
|
c = testSSHConfig()
|
||||||
c.SSHUser = "exists"
|
c.Comm.SSHUsername = "exists"
|
||||||
errs = c.Prepare(testConfigTemplate(t))
|
|
||||||
if len(errs) > 0 {
|
|
||||||
t.Fatalf("should not have error: %#v", errs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSSHConfigPrepare_SSHWaitTimeout(t *testing.T) {
|
|
||||||
var c *SSHConfig
|
|
||||||
var errs []error
|
|
||||||
|
|
||||||
// Defaults
|
|
||||||
c = testSSHConfig()
|
|
||||||
c.RawSSHWaitTimeout = ""
|
|
||||||
errs = c.Prepare(testConfigTemplate(t))
|
|
||||||
if len(errs) > 0 {
|
|
||||||
t.Fatalf("should not have error: %#v", errs)
|
|
||||||
}
|
|
||||||
if c.RawSSHWaitTimeout != "20m" {
|
|
||||||
t.Fatalf("bad value: %s", c.RawSSHWaitTimeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with a bad value
|
|
||||||
c = testSSHConfig()
|
|
||||||
c.RawSSHWaitTimeout = "this is not good"
|
|
||||||
errs = c.Prepare(testConfigTemplate(t))
|
|
||||||
if len(errs) == 0 {
|
|
||||||
t.Fatal("should have error")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with a good one
|
|
||||||
c = testSSHConfig()
|
|
||||||
c.RawSSHWaitTimeout = "5s"
|
|
||||||
errs = c.Prepare(testConfigTemplate(t))
|
errs = c.Prepare(testConfigTemplate(t))
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
t.Fatalf("should not have error: %#v", errs)
|
t.Fatalf("should not have error: %#v", errs)
|
||||||
|
|
|
@ -20,6 +20,7 @@ type StepExport struct {
|
||||||
Format string
|
Format string
|
||||||
OutputDir string
|
OutputDir string
|
||||||
ExportOpts []string
|
ExportOpts []string
|
||||||
|
SkipNatMapping bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StepExport) Run(state multistep.StateBag) multistep.StepAction {
|
func (s *StepExport) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
|
@ -30,9 +31,10 @@ func (s *StepExport) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
// Wait a second to ensure VM is really shutdown.
|
// Wait a second to ensure VM is really shutdown.
|
||||||
log.Println("1 second timeout to ensure VM is really shutdown")
|
log.Println("1 second timeout to ensure VM is really shutdown")
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
|
ui.Say("Preparing to export machine...")
|
||||||
|
|
||||||
// Clear out the Packer-created forwarding rule
|
// Clear out the Packer-created forwarding rule
|
||||||
ui.Say("Preparing to export machine...")
|
if !s.SkipNatMapping {
|
||||||
ui.Message(fmt.Sprintf(
|
ui.Message(fmt.Sprintf(
|
||||||
"Deleting forwarded port mapping for SSH (host port %d)",
|
"Deleting forwarded port mapping for SSH (host port %d)",
|
||||||
state.Get("sshHostPort")))
|
state.Get("sshHostPort")))
|
||||||
|
@ -43,17 +45,17 @@ func (s *StepExport) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
ui.Error(err.Error())
|
ui.Error(err.Error())
|
||||||
return multistep.ActionHalt
|
return multistep.ActionHalt
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Export the VM to an OVF
|
// Export the VM to an OVF
|
||||||
outputPath := filepath.Join(s.OutputDir, vmName+"."+s.Format)
|
outputPath := filepath.Join(s.OutputDir, vmName+"."+s.Format)
|
||||||
|
|
||||||
command = []string{
|
command := []string{
|
||||||
"export",
|
"export",
|
||||||
vmName,
|
vmName,
|
||||||
"--output",
|
"--output",
|
||||||
outputPath,
|
outputPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
command = append(command, s.ExportOpts...)
|
command = append(command, s.ExportOpts...)
|
||||||
|
|
||||||
ui.Say("Exporting virtual machine...")
|
ui.Say("Exporting virtual machine...")
|
||||||
|
|
|
@ -2,11 +2,13 @@ package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/mitchellh/multistep"
|
|
||||||
"github.com/mitchellh/packer/packer"
|
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/helper/communicator"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This step adds a NAT port forwarding definition so that SSH is available
|
// This step adds a NAT port forwarding definition so that SSH is available
|
||||||
|
@ -19,9 +21,10 @@ import (
|
||||||
//
|
//
|
||||||
// Produces:
|
// Produces:
|
||||||
type StepForwardSSH struct {
|
type StepForwardSSH struct {
|
||||||
GuestPort uint
|
CommConfig *communicator.Config
|
||||||
HostPortMin uint
|
HostPortMin uint
|
||||||
HostPortMax uint
|
HostPortMax uint
|
||||||
|
SkipNatMapping bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StepForwardSSH) Run(state multistep.StateBag) multistep.StepAction {
|
func (s *StepForwardSSH) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
|
@ -29,19 +32,21 @@ func (s *StepForwardSSH) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
ui := state.Get("ui").(packer.Ui)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
vmName := state.Get("vmName").(string)
|
vmName := state.Get("vmName").(string)
|
||||||
|
|
||||||
|
guestPort := s.CommConfig.Port()
|
||||||
|
sshHostPort := guestPort
|
||||||
|
if !s.SkipNatMapping {
|
||||||
log.Printf("Looking for available SSH port between %d and %d",
|
log.Printf("Looking for available SSH port between %d and %d",
|
||||||
s.HostPortMin, s.HostPortMax)
|
s.HostPortMin, s.HostPortMax)
|
||||||
var sshHostPort uint
|
offset := 0
|
||||||
var offset uint = 0
|
|
||||||
|
|
||||||
portRange := int(s.HostPortMax - s.HostPortMin)
|
portRange := int(s.HostPortMax - s.HostPortMin)
|
||||||
if portRange > 0 {
|
if portRange > 0 {
|
||||||
// Have to check if > 0 to avoid a panic
|
// Have to check if > 0 to avoid a panic
|
||||||
offset = uint(rand.Intn(portRange))
|
offset = rand.Intn(portRange)
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
sshHostPort = offset + s.HostPortMin
|
sshHostPort = offset + int(s.HostPortMin)
|
||||||
log.Printf("Trying port: %d", sshHostPort)
|
log.Printf("Trying port: %d", sshHostPort)
|
||||||
l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", sshHostPort))
|
l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", sshHostPort))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
@ -55,7 +60,7 @@ func (s *StepForwardSSH) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
command := []string{
|
command := []string{
|
||||||
"modifyvm", vmName,
|
"modifyvm", vmName,
|
||||||
"--natpf1",
|
"--natpf1",
|
||||||
fmt.Sprintf("packerssh,tcp,127.0.0.1,%d,,%d", sshHostPort, s.GuestPort),
|
fmt.Sprintf("packerssh,tcp,127.0.0.1,%d,,%d", sshHostPort, guestPort),
|
||||||
}
|
}
|
||||||
if err := driver.VBoxManage(command...); err != nil {
|
if err := driver.VBoxManage(command...); err != nil {
|
||||||
err := fmt.Errorf("Error creating port forwarding rule: %s", err)
|
err := fmt.Errorf("Error creating port forwarding rule: %s", err)
|
||||||
|
@ -63,6 +68,7 @@ func (s *StepForwardSSH) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
ui.Error(err.Error())
|
ui.Error(err.Error())
|
||||||
return multistep.ActionHalt
|
return multistep.ActionHalt
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Save the port we're using so that future steps can use it
|
// Save the port we're using so that future steps can use it
|
||||||
state.Put("sshHostPort", sshHostPort)
|
state.Put("sshHostPort", sshHostPort)
|
||||||
|
|
|
@ -49,7 +49,7 @@ func (s *StepHTTPServer) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
httpPort = offset + s.HTTPPortMin
|
httpPort = offset + s.HTTPPortMin
|
||||||
httpAddr = fmt.Sprintf(":%d", httpPort)
|
httpAddr = fmt.Sprintf("0.0.0.0:%d", httpPort)
|
||||||
log.Printf("Trying port: %d", httpPort)
|
log.Printf("Trying port: %d", httpPort)
|
||||||
s.l, err = net.Listen("tcp", httpAddr)
|
s.l, err = net.Listen("tcp", httpAddr)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
|
@ -22,7 +22,16 @@ type StepOutputDir struct {
|
||||||
func (s *StepOutputDir) Run(state multistep.StateBag) multistep.StepAction {
|
func (s *StepOutputDir) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
ui := state.Get("ui").(packer.Ui)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
if _, err := os.Stat(s.Path); err == nil && s.Force {
|
if _, err := os.Stat(s.Path); err == nil {
|
||||||
|
if !s.Force {
|
||||||
|
err := fmt.Errorf(
|
||||||
|
"Output directory exists: %s\n\n"+
|
||||||
|
"Use the force flag to delete it prior to building.",
|
||||||
|
s.Path)
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
ui.Say("Deleting previous output directory...")
|
ui.Say("Deleting previous output directory...")
|
||||||
os.RemoveAll(s.Path)
|
os.RemoveAll(s.Path)
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,30 @@ func TestStepOutputDir(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStepOutputDir_exists(t *testing.T) {
|
||||||
|
state := testState(t)
|
||||||
|
step := testStepOutputDir(t)
|
||||||
|
|
||||||
|
// Make the dir
|
||||||
|
if err := os.MkdirAll(step.Path, 0755); err != nil {
|
||||||
|
t.Fatalf("bad: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the run
|
||||||
|
if action := step.Run(state); action != multistep.ActionHalt {
|
||||||
|
t.Fatalf("bad action: %#v", action)
|
||||||
|
}
|
||||||
|
if _, ok := state.GetOk("error"); !ok {
|
||||||
|
t.Fatal("should have error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the cleanup
|
||||||
|
step.Cleanup(state)
|
||||||
|
if _, err := os.Stat(step.Path); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestStepOutputDir_cancelled(t *testing.T) {
|
func TestStepOutputDir_cancelled(t *testing.T) {
|
||||||
state := testState(t)
|
state := testState(t)
|
||||||
step := testStepOutputDir(t)
|
step := testStepOutputDir(t)
|
||||||
|
|
|
@ -38,6 +38,19 @@ func (s *StepRemoveDevices) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
ui.Error(err.Error())
|
ui.Error(err.Error())
|
||||||
return multistep.ActionHalt
|
return multistep.ActionHalt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't forget to remove the floppy controller as well
|
||||||
|
command = []string{
|
||||||
|
"storagectl", vmName,
|
||||||
|
"--name", "Floppy Controller",
|
||||||
|
"--remove",
|
||||||
|
}
|
||||||
|
if err := driver.VBoxManage(command...); err != nil {
|
||||||
|
err := fmt.Errorf("Error removing floppy controller: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := state.GetOk("attachedIso"); ok {
|
if _, ok := state.GetOk("attachedIso"); ok {
|
||||||
|
|
|
@ -102,10 +102,13 @@ func TestStepRemoveDevices_floppyPath(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that both were removed
|
// Test that both were removed
|
||||||
if len(driver.VBoxManageCalls) != 1 {
|
if len(driver.VBoxManageCalls) != 2 {
|
||||||
t.Fatalf("bad: %#v", driver.VBoxManageCalls)
|
t.Fatalf("bad: %#v", driver.VBoxManageCalls)
|
||||||
}
|
}
|
||||||
if driver.VBoxManageCalls[0][3] != "Floppy Controller" {
|
if driver.VBoxManageCalls[0][3] != "Floppy Controller" {
|
||||||
t.Fatalf("bad: %#v", driver.VBoxManageCalls)
|
t.Fatalf("bad: %#v", driver.VBoxManageCalls)
|
||||||
}
|
}
|
||||||
|
if driver.VBoxManageCalls[1][3] != "Floppy Controller" {
|
||||||
|
t.Fatalf("bad: %#v", driver.VBoxManageCalls)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/mitchellh/multistep"
|
"github.com/mitchellh/multistep"
|
||||||
vboxcommon "github.com/mitchellh/packer/builder/virtualbox/common"
|
vboxcommon "github.com/mitchellh/packer/builder/virtualbox/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"
|
||||||
|
@ -230,6 +231,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||||
Description: "ISO",
|
Description: "ISO",
|
||||||
ResultKey: "iso_path",
|
ResultKey: "iso_path",
|
||||||
Url: b.config.ISOUrls,
|
Url: b.config.ISOUrls,
|
||||||
|
Extension: "iso",
|
||||||
},
|
},
|
||||||
&vboxcommon.StepOutputDir{
|
&vboxcommon.StepOutputDir{
|
||||||
Force: b.config.PackerForce,
|
Force: b.config.PackerForce,
|
||||||
|
@ -252,9 +254,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||||
},
|
},
|
||||||
new(vboxcommon.StepAttachFloppy),
|
new(vboxcommon.StepAttachFloppy),
|
||||||
&vboxcommon.StepForwardSSH{
|
&vboxcommon.StepForwardSSH{
|
||||||
GuestPort: b.config.SSHPort,
|
CommConfig: &b.config.SSHConfig.Comm,
|
||||||
HostPortMin: b.config.SSHHostPortMin,
|
HostPortMin: b.config.SSHHostPortMin,
|
||||||
HostPortMax: b.config.SSHHostPortMax,
|
HostPortMax: b.config.SSHHostPortMax,
|
||||||
|
SkipNatMapping: b.config.SSHSkipNatMapping,
|
||||||
},
|
},
|
||||||
&vboxcommon.StepVBoxManage{
|
&vboxcommon.StepVBoxManage{
|
||||||
Commands: b.config.VBoxManage,
|
Commands: b.config.VBoxManage,
|
||||||
|
@ -269,10 +272,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||||
VMName: b.config.VMName,
|
VMName: b.config.VMName,
|
||||||
Ctx: b.config.ctx,
|
Ctx: b.config.ctx,
|
||||||
},
|
},
|
||||||
&common.StepConnectSSH{
|
&communicator.StepConnect{
|
||||||
SSHAddress: vboxcommon.SSHAddress,
|
Config: &b.config.SSHConfig.Comm,
|
||||||
|
Host: vboxcommon.CommHost,
|
||||||
SSHConfig: vboxcommon.SSHConfigFunc(b.config.SSHConfig),
|
SSHConfig: vboxcommon.SSHConfigFunc(b.config.SSHConfig),
|
||||||
SSHWaitTimeout: b.config.SSHWaitTimeout,
|
SSHPort: vboxcommon.SSHPort,
|
||||||
},
|
},
|
||||||
&vboxcommon.StepUploadVersion{
|
&vboxcommon.StepUploadVersion{
|
||||||
Path: b.config.VBoxVersionFile,
|
Path: b.config.VBoxVersionFile,
|
||||||
|
@ -296,6 +300,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||||
Format: b.config.Format,
|
Format: b.config.Format,
|
||||||
OutputDir: b.config.OutputDir,
|
OutputDir: b.config.OutputDir,
|
||||||
ExportOpts: b.config.ExportOpts.ExportOpts,
|
ExportOpts: b.config.ExportOpts.ExportOpts,
|
||||||
|
SkipNatMapping: b.config.SSHSkipNatMapping,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
package iso
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
builderT "github.com/mitchellh/packer/helper/builder/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuilderAcc_basic(t *testing.T) {
|
||||||
|
builderT.Test(t, builderT.TestCase{
|
||||||
|
Builder: &Builder{},
|
||||||
|
Template: testBuilderAccBasic,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const testBuilderAccBasic = `
|
||||||
|
{
|
||||||
|
"builders": [{
|
||||||
|
"type": "test",
|
||||||
|
"guest_os_type": "Ubuntu_64",
|
||||||
|
"iso_url": "http://releases.ubuntu.com/12.04/ubuntu-12.04.5-server-amd64.iso",
|
||||||
|
"iso_checksum": "769474248a3897f4865817446f9a4a53",
|
||||||
|
"iso_checksum_type": "md5",
|
||||||
|
"ssh_username": "packer",
|
||||||
|
"ssh_password": "packer",
|
||||||
|
"ssh_wait_timeout": "30s",
|
||||||
|
"shutdown_command": "echo 'packer' | sudo -S shutdown -P now"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
`
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/mitchellh/multistep"
|
"github.com/mitchellh/multistep"
|
||||||
vboxcommon "github.com/mitchellh/packer/builder/virtualbox/common"
|
vboxcommon "github.com/mitchellh/packer/builder/virtualbox/common"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -82,9 +83,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||||
},
|
},
|
||||||
new(vboxcommon.StepAttachFloppy),
|
new(vboxcommon.StepAttachFloppy),
|
||||||
&vboxcommon.StepForwardSSH{
|
&vboxcommon.StepForwardSSH{
|
||||||
GuestPort: b.config.SSHPort,
|
CommConfig: &b.config.SSHConfig.Comm,
|
||||||
HostPortMin: b.config.SSHHostPortMin,
|
HostPortMin: b.config.SSHHostPortMin,
|
||||||
HostPortMax: b.config.SSHHostPortMax,
|
HostPortMax: b.config.SSHHostPortMax,
|
||||||
|
SkipNatMapping: b.config.SSHSkipNatMapping,
|
||||||
},
|
},
|
||||||
&vboxcommon.StepVBoxManage{
|
&vboxcommon.StepVBoxManage{
|
||||||
Commands: b.config.VBoxManage,
|
Commands: b.config.VBoxManage,
|
||||||
|
@ -99,10 +101,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||||
VMName: b.config.VMName,
|
VMName: b.config.VMName,
|
||||||
Ctx: b.config.ctx,
|
Ctx: b.config.ctx,
|
||||||
},
|
},
|
||||||
&common.StepConnectSSH{
|
&communicator.StepConnect{
|
||||||
SSHAddress: vboxcommon.SSHAddress,
|
Config: &b.config.SSHConfig.Comm,
|
||||||
|
Host: vboxcommon.CommHost,
|
||||||
SSHConfig: vboxcommon.SSHConfigFunc(b.config.SSHConfig),
|
SSHConfig: vboxcommon.SSHConfigFunc(b.config.SSHConfig),
|
||||||
SSHWaitTimeout: b.config.SSHWaitTimeout,
|
SSHPort: vboxcommon.SSHPort,
|
||||||
},
|
},
|
||||||
&vboxcommon.StepUploadVersion{
|
&vboxcommon.StepUploadVersion{
|
||||||
Path: b.config.VBoxVersionFile,
|
Path: b.config.VBoxVersionFile,
|
||||||
|
@ -126,6 +129,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||||
Format: b.config.Format,
|
Format: b.config.Format,
|
||||||
OutputDir: b.config.OutputDir,
|
OutputDir: b.config.OutputDir,
|
||||||
ExportOpts: b.config.ExportOpts.ExportOpts,
|
ExportOpts: b.config.ExportOpts.ExportOpts,
|
||||||
|
SkipNatMapping: b.config.SSHSkipNatMapping,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,8 +40,8 @@ type Config struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
||||||
var c Config
|
c := new(Config)
|
||||||
err := config.Decode(&c, &config.DecodeOpts{
|
err := config.Decode(c, &config.DecodeOpts{
|
||||||
Interpolate: true,
|
Interpolate: true,
|
||||||
InterpolateFilter: &interpolate.RenderFilter{
|
InterpolateFilter: &interpolate.RenderFilter{
|
||||||
Exclude: []string{
|
Exclude: []string{
|
||||||
|
@ -132,5 +132,5 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
||||||
c.ImportFlags = append(c.ImportFlags, "--options", c.ImportOpts)
|
c.ImportFlags = append(c.ImportFlags, "--options", c.ImportOpts)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &c, warnings, nil
|
return c, warnings, nil
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue