commit
899cd7b845
87
CHANGELOG.md
87
CHANGELOG.md
|
@ -1,48 +1,131 @@
|
|||
## 0.8.0 (unreleased)
|
||||
|
||||
BACKWARDS INCOMPATIBILITIES:
|
||||
|
||||
* core: SSH connection will no longer request a PTY by default. This
|
||||
can be enabled per builder.
|
||||
* builder/digitalocean: no longer supports the v1 API which has been
|
||||
deprecated for some time. Most configurations should continue to
|
||||
work as long as you use the `api_token` field for auth.
|
||||
* builder/digitalocean: `image`, `region`, and `size` are now required.
|
||||
* builder/openstack: auth parameters have been changed to better
|
||||
reflect OS terminology. Existing environment variables still work.
|
||||
|
||||
FEATURES:
|
||||
|
||||
* **WinRM:** You can now connect via WinRM with almost every builder.
|
||||
See the docs for more info. [GH-2239]
|
||||
* **Windows AWS Support:** Windows AMIs can now be built without any
|
||||
external plugins: Packer will start a Windows instance, get the
|
||||
admin password, and can use WinRM (above) to connect through. [GH-2240]
|
||||
* **Disable SSH:** Set `communicator` to "none" in any builder to disable SSH
|
||||
connections. Note that provisioners won't work if this is done. [GH-1591]
|
||||
* **SSH Agent Forwarding:** SSH Agent Forwarding will now be enabled
|
||||
to allow access to remote servers such as private git repos. [GH-1066]
|
||||
* **Docker builder supports SSH**: The Docker builder now supports containers
|
||||
with SSH, just set `communicator` to "ssh" [GH-2244]
|
||||
* **New config function: `build_name`**: The name of the currently running
|
||||
build. [GH-2232]
|
||||
* **New config function: `build_type`**: The type of the currently running
|
||||
builder. This is useful for provisioners. [GH-2232]
|
||||
* **New config function: `template_dir`**: The directory to the template
|
||||
being built. This should be used for template-relative paths. [GH-54]
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* core: Interrupt handling for SIGTERM signal as well. [GH-1858]
|
||||
* builder/*: Add `ssh_handshake_attempts` to configure the number of
|
||||
handshake attempts done before failure [GH-2237]
|
||||
* builder/amazon: Add `force_deregister` option for automatic AMI
|
||||
deregistration [GH-2221]
|
||||
* builder/amazon: Now applies tags to EBS snapshots [GH-2212]
|
||||
* builder/digitalocean: Save SSH key to pwd if debug mode is on. [GH-1829]
|
||||
* builder/digitalocean: User data support [GH-2113]
|
||||
* builder/parallels: Support Parallels Desktop 11 [GH-2199]
|
||||
* builder/openstack: Add `rackconnect_wait` for Rackspace customers to wait for
|
||||
RackConnect data to appear
|
||||
* buidler/openstakc: Add `ssh_interface` option for rackconnect for users that
|
||||
* buidler/openstack: Add `ssh_interface` option for rackconnect for users that
|
||||
have prohibitive firewalls
|
||||
* builder/openstack: Flavor names can be used as well as refs
|
||||
* builder/openstack: Add `availability_zone` [GH-2016]
|
||||
* builder/openstack: Machine will be stopped prior to imaging if the
|
||||
cluster supports the `startstop` extension. [GH-2223]
|
||||
* builder/openstack: Support for user data [GH-2224]
|
||||
* builder/virtualbox: Added option: `ssh_skip_nat_mapping` to skip the
|
||||
automatic port forward for SSH and to use the guest port directly. [GH-1078]
|
||||
* builder/virtualbox: Added SCSI support
|
||||
* builder/vmware: Support for additional disks [GH-1382]
|
||||
* command/fix: After fixing, the template is validated [GH-2228]
|
||||
* command/push: Add `-name` flag for specifying name from CLI [GH-2042]
|
||||
* command/push: Push configuration in templates supports variables [GH-1861]
|
||||
* post-processor/docker-save: Can be chained [GH-2179]
|
||||
* post-processor/docker-tag: Support `force` option [GH-2055]
|
||||
* post-processor/docker-tag: Can be chained [GH-2179]
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* core: Fix potential panic for post-processor plugin exits [GH-2098]
|
||||
* core: `PACKER_CONFIG` may point to a non-existent file [GH-2226]
|
||||
* builder/amazon: Allow spaces in AMI names when using `clean_ami_name` [GH-2182]
|
||||
* builder/amazon: Remove deprecated ec2-upload-bundle paramger [GH-1931]
|
||||
* builder/amazon: Use IAM Profile to upload bundle if provided [GH-1985]
|
||||
* builder/amazon: Use correct exit code after SSH authentication failed [GH-2004]
|
||||
* builder/amazon: Retry finding created instance for eventual
|
||||
consistency. [GH-2129]
|
||||
* builder/amazon: If no AZ is specified, use AZ chosen automatically by
|
||||
AWS for spot instance. [GH-2017]
|
||||
* builder/amazon: Private key file (only available in debug mode)
|
||||
is deleted on cleanup. [GH-1801]
|
||||
* builder/amazon: AMI copy won't copy to the source region [GH-2123]
|
||||
* builder/amazon: Validate AMI doesn't exist with name prior to build [GH-1774]
|
||||
* builder/amazon: Improved retry logic around waiting for instances. [GH-1764]
|
||||
* builder/amazon: Fix issues with creating Block Devices. [GH-2195]
|
||||
* builder/amazon/chroot: Retry waiting for disk attachments [GH-2046]
|
||||
* builder/amazon/instance: Use `-i` in sudo commands so PATH is inherited. [GH-1930]
|
||||
* builder/amazon/instance: Use `--region` flag for bundle upload command. [GH-1931]
|
||||
* builder/digitalocean: Wait for droplet to unlock before changing state,
|
||||
should lower the "pending event" errors.
|
||||
* builder/digitalocean: Ignore invalid fields from the ever-changing v2 API
|
||||
* builder/digitalocean: Private images can be used as a source [GH-1792]
|
||||
* builder/docker: Fixed hang on prompt while copying script
|
||||
* builder/docker: Use `docker exec` for newer versions of Docker for
|
||||
running scripts [GH-1993]
|
||||
* builder/docker: Fix crash that could occur at certain timed ctrl-c [GH-1838]
|
||||
* builder/docker: validate that `export_path` is not a directory [GH-2105]
|
||||
* builder/google: `ssh_timeout` is respected [GH-1781]
|
||||
* builder/openstack: `ssh_interface` can be used to specify the interface
|
||||
to retrieve the SSH IP from. [GH-2220]
|
||||
* builder/qemu: Add `disk_discard` option [GH-2120]
|
||||
* builder/virtualbox: Added SCSI support
|
||||
* builder/qemu: Use proper SSH port, not hardcoded to 22. [GH-2236]
|
||||
* builder/virtualbox: Bind HTTP server to IPv4, which is more compatible with
|
||||
OS installers. [GH-1709]
|
||||
* builder/virtualbox: Remove the floppy controller in addition to the
|
||||
floppy disk. [GH-1879]
|
||||
* builder/virtualbox: Fixed regression where downloading ISO without a
|
||||
".iso" extension didn't work. [GH-1839]
|
||||
* builder/virtualbox: Output dir is verified at runtime, not template
|
||||
validation time. [GH-2233]
|
||||
* builder/vmware: Add 100ms delay between keystrokes to avoid subtle
|
||||
timing issues in most cases. [GH-1663]
|
||||
* builder/vmware: Bind HTTP server to IPv4, which is more compatible with
|
||||
OS installers. [GH-1709]
|
||||
* builder/vmware: Case-insensitive match of MAC address to find IP [GH-1989]
|
||||
* builder/vmware: More robust IP parsing from ifconfig output [GH-1999]
|
||||
* builder/vmware: Nested output directories for ESXi work [GH-2174]
|
||||
* builder/vmware: Output dir is verified at runtime, not template
|
||||
validation time. [GH-2233]
|
||||
* command/fix: For the `virtualbox` to `virtualbox-iso` builder rename,
|
||||
provisioner overrides are now also fixed [GH-2231]
|
||||
* command/validate: don't crash for invalid builds [GH-2139]
|
||||
* post-processor/atlas: Find common archive prefix for Windows [GH-1874]
|
||||
* post-processor/atlas: Fix index out of range panic [GH-1959]
|
||||
* post-processor/vagrant-cloud: Fixed failing on response
|
||||
* post-processor/vagrant-cloud: Don't delete version on error [GH-2014]
|
||||
* provisioner/puppet-masterless: Allow manifest_file to be a directory
|
||||
* provisioner/salt-masterless: Add `--retcode-passthrough` to salt-call
|
||||
* provisioner/shell: chmod executable script to 0755, not 0777 [GH-1708]
|
||||
* provisioner/shell: inline commands failing will fail the provisioner [GH-2069]
|
||||
* provisioner/shell: single quotes in env vars are escaped [GH-2229]
|
||||
|
||||
## 0.7.5 (December 9, 2014)
|
||||
|
||||
|
|
2
Makefile
2
Makefile
|
@ -31,7 +31,7 @@ testrace:
|
|||
go test -race $(TEST) $(TESTARGS)
|
||||
|
||||
updatedeps:
|
||||
go get -d -v -p 2 ./...
|
||||
go get -u -d -v -p 2 ./...
|
||||
|
||||
vet:
|
||||
@go tool vet 2>/dev/null ; if [ $$? -eq 3 ]; then \
|
||||
|
|
|
@ -147,6 +147,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
|
||||
// Build the steps
|
||||
steps := []multistep.Step{
|
||||
&awscommon.StepPreValidate{
|
||||
DestAmiName: b.config.AMIName,
|
||||
ForceDeregister: b.config.AMIForceDeregister,
|
||||
},
|
||||
&StepInstanceInfo{},
|
||||
&awscommon.StepSourceAMIInfo{
|
||||
SourceAmi: b.config.SourceAmi,
|
||||
|
@ -164,9 +168,15 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
&StepChrootProvision{},
|
||||
&StepEarlyCleanup{},
|
||||
&StepSnapshot{},
|
||||
&awscommon.StepDeregisterAMI{
|
||||
ForceDeregister: b.config.AMIForceDeregister,
|
||||
AMIName: b.config.AMIName,
|
||||
},
|
||||
&StepRegisterAMI{},
|
||||
&awscommon.StepAMIRegionCopy{
|
||||
Regions: b.config.AMIRegions,
|
||||
AccessConfig: &b.config.AccessConfig,
|
||||
Regions: b.config.AMIRegions,
|
||||
Name: b.config.AMIName,
|
||||
},
|
||||
&awscommon.StepModifyAMIAttributes{
|
||||
Description: b.config.AMIDescription,
|
||||
|
|
|
@ -17,6 +17,7 @@ type AMIConfig struct {
|
|||
AMIRegions []string `mapstructure:"ami_regions"`
|
||||
AMITags map[string]string `mapstructure:"tags"`
|
||||
AMIEnhancedNetworking bool `mapstructure:"enhanced_networking"`
|
||||
AMIForceDeregister bool `mapstructure:"force_deregister"`
|
||||
}
|
||||
|
||||
func (c *AMIConfig) Prepare(ctx *interpolate.Context) []error {
|
||||
|
|
|
@ -29,13 +29,23 @@ func buildBlockDevices(b []BlockDevice) []*ec2.BlockDeviceMapping {
|
|||
|
||||
for _, blockDevice := range b {
|
||||
ebsBlockDevice := &ec2.EBSBlockDevice{
|
||||
SnapshotID: &blockDevice.SnapshotId,
|
||||
Encrypted: &blockDevice.Encrypted,
|
||||
IOPS: &blockDevice.IOPS,
|
||||
VolumeType: &blockDevice.VolumeType,
|
||||
VolumeSize: &blockDevice.VolumeSize,
|
||||
DeleteOnTermination: &blockDevice.DeleteOnTermination,
|
||||
}
|
||||
|
||||
// IOPS is only valid for SSD Volumes
|
||||
if blockDevice.VolumeType != "" && blockDevice.VolumeType != "standard" && blockDevice.VolumeType != "gp2" {
|
||||
ebsBlockDevice.IOPS = &blockDevice.IOPS
|
||||
}
|
||||
|
||||
// You cannot specify Encrypted if you specify a Snapshot ID
|
||||
if blockDevice.SnapshotId != "" {
|
||||
ebsBlockDevice.SnapshotID = &blockDevice.SnapshotId
|
||||
} else {
|
||||
ebsBlockDevice.Encrypted = &blockDevice.Encrypted
|
||||
}
|
||||
|
||||
mapping := &ec2.BlockDeviceMapping{
|
||||
EBS: ebsBlockDevice,
|
||||
DeviceName: &blockDevice.DeviceName,
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awsutil"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
)
|
||||
|
||||
|
@ -28,11 +29,48 @@ func TestBlockDevice(t *testing.T) {
|
|||
DeviceName: aws.String("/dev/sdb"),
|
||||
VirtualName: aws.String("ephemeral0"),
|
||||
EBS: &ec2.EBSBlockDevice{
|
||||
Encrypted: aws.Boolean(false),
|
||||
SnapshotID: aws.String("snap-1234"),
|
||||
VolumeType: aws.String("standard"),
|
||||
VolumeSize: aws.Long(8),
|
||||
DeleteOnTermination: aws.Boolean(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Config: &BlockDevice{
|
||||
DeviceName: "/dev/sdb",
|
||||
VolumeSize: 8,
|
||||
},
|
||||
|
||||
Result: &ec2.BlockDeviceMapping{
|
||||
DeviceName: aws.String("/dev/sdb"),
|
||||
VirtualName: aws.String(""),
|
||||
EBS: &ec2.EBSBlockDevice{
|
||||
Encrypted: aws.Boolean(false),
|
||||
VolumeType: aws.String(""),
|
||||
VolumeSize: aws.Long(8),
|
||||
DeleteOnTermination: aws.Boolean(false),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Config: &BlockDevice{
|
||||
DeviceName: "/dev/sdb",
|
||||
VirtualName: "ephemeral0",
|
||||
VolumeType: "io1",
|
||||
VolumeSize: 8,
|
||||
DeleteOnTermination: true,
|
||||
IOPS: 1000,
|
||||
},
|
||||
|
||||
Result: &ec2.BlockDeviceMapping{
|
||||
DeviceName: aws.String("/dev/sdb"),
|
||||
VirtualName: aws.String("ephemeral0"),
|
||||
EBS: &ec2.EBSBlockDevice{
|
||||
Encrypted: aws.Boolean(false),
|
||||
VolumeType: aws.String("io1"),
|
||||
VolumeSize: aws.Long(8),
|
||||
DeleteOnTermination: aws.Boolean(true),
|
||||
IOPS: aws.Long(1000),
|
||||
},
|
||||
},
|
||||
|
@ -48,11 +86,11 @@ func TestBlockDevice(t *testing.T) {
|
|||
expected := []*ec2.BlockDeviceMapping{tc.Result}
|
||||
got := blockDevices.BuildAMIDevices()
|
||||
if !reflect.DeepEqual(expected, got) {
|
||||
t.Fatalf("bad: %#v", expected)
|
||||
t.Fatalf("Bad block device, \nexpected: %s\n\ngot: %s", awsutil.StringValue(expected), awsutil.StringValue(got))
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expected, blockDevices.BuildLaunchDevices()) {
|
||||
t.Fatalf("bad: %#v", expected)
|
||||
t.Fatalf("Bad block device, \nexpected: %s\n\ngot: %s", awsutil.StringValue(expected), awsutil.StringValue(blockDevices.BuildLaunchDevices()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/mitchellh/packer/common/uuid"
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
)
|
||||
|
||||
|
@ -21,40 +22,32 @@ type RunConfig struct {
|
|||
SourceAmi string `mapstructure:"source_ami"`
|
||||
SpotPrice string `mapstructure:"spot_price"`
|
||||
SpotPriceAutoProduct string `mapstructure:"spot_price_auto_product"`
|
||||
RawSSHTimeout string `mapstructure:"ssh_timeout"`
|
||||
SSHUsername string `mapstructure:"ssh_username"`
|
||||
SSHPrivateKeyFile string `mapstructure:"ssh_private_key_file"`
|
||||
SSHPrivateIp bool `mapstructure:"ssh_private_ip"`
|
||||
SSHPort int `mapstructure:"ssh_port"`
|
||||
SecurityGroupId string `mapstructure:"security_group_id"`
|
||||
SecurityGroupIds []string `mapstructure:"security_group_ids"`
|
||||
SubnetId string `mapstructure:"subnet_id"`
|
||||
TemporaryKeyPairName string `mapstructure:"temporary_key_pair_name"`
|
||||
UserData string `mapstructure:"user_data"`
|
||||
UserDataFile string `mapstructure:"user_data_file"`
|
||||
WindowsPasswordTimeout time.Duration `mapstructure:"windows_password_timeout"`
|
||||
VpcId string `mapstructure:"vpc_id"`
|
||||
|
||||
// Unexported fields that are calculated from others
|
||||
sshTimeout time.Duration
|
||||
// Communicator settings
|
||||
Comm communicator.Config `mapstructure:",squash"`
|
||||
SSHPrivateIp bool `mapstructure:"ssh_private_ip"`
|
||||
}
|
||||
|
||||
func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
|
||||
// Defaults
|
||||
if c.SSHPort == 0 {
|
||||
c.SSHPort = 22
|
||||
}
|
||||
|
||||
if c.RawSSHTimeout == "" {
|
||||
c.RawSSHTimeout = "5m"
|
||||
}
|
||||
|
||||
if c.TemporaryKeyPairName == "" {
|
||||
c.TemporaryKeyPairName = fmt.Sprintf(
|
||||
"packer %s", uuid.TimeOrderedUUID())
|
||||
}
|
||||
|
||||
if c.WindowsPasswordTimeout == 0 {
|
||||
c.WindowsPasswordTimeout = 10 * time.Minute
|
||||
}
|
||||
|
||||
// Validation
|
||||
var errs []error
|
||||
errs := c.Comm.Prepare(ctx)
|
||||
if c.SourceAmi == "" {
|
||||
errs = append(errs, errors.New("A source_ami must be specified"))
|
||||
}
|
||||
|
@ -70,10 +63,6 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
|
|||
}
|
||||
}
|
||||
|
||||
if c.SSHUsername == "" {
|
||||
errs = append(errs, errors.New("An ssh_username must be specified"))
|
||||
}
|
||||
|
||||
if c.UserData != "" && c.UserDataFile != "" {
|
||||
errs = append(errs, fmt.Errorf("Only one of user_data or user_data_file can be specified."))
|
||||
} else if c.UserDataFile != "" {
|
||||
|
@ -91,15 +80,5 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
|
|||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
c.sshTimeout, err = time.ParseDuration(c.RawSSHTimeout)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (c *RunConfig) SSHTimeout() time.Duration {
|
||||
return c.sshTimeout
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ import (
|
|||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -19,7 +21,10 @@ func testConfig() *RunConfig {
|
|||
return &RunConfig{
|
||||
SourceAmi: "abcd",
|
||||
InstanceType: "m1.small",
|
||||
SSHUsername: "root",
|
||||
|
||||
Comm: communicator.Config{
|
||||
SSHUsername: "foo",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,41 +67,28 @@ func TestRunConfigPrepare_SpotAuto(t *testing.T) {
|
|||
|
||||
func TestRunConfigPrepare_SSHPort(t *testing.T) {
|
||||
c := testConfig()
|
||||
c.SSHPort = 0
|
||||
c.Comm.SSHPort = 0
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if c.SSHPort != 22 {
|
||||
t.Fatalf("invalid value: %d", c.SSHPort)
|
||||
if c.Comm.SSHPort != 22 {
|
||||
t.Fatalf("invalid value: %d", c.Comm.SSHPort)
|
||||
}
|
||||
|
||||
c.SSHPort = 44
|
||||
c.Comm.SSHPort = 44
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if c.SSHPort != 44 {
|
||||
t.Fatalf("invalid value: %d", c.SSHPort)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigPrepare_SSHTimeout(t *testing.T) {
|
||||
c := testConfig()
|
||||
c.RawSSHTimeout = ""
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
c.RawSSHTimeout = "bad"
|
||||
if err := c.Prepare(nil); len(err) != 1 {
|
||||
t.Fatalf("err: %s", err)
|
||||
if c.Comm.SSHPort != 44 {
|
||||
t.Fatalf("invalid value: %d", c.Comm.SSHPort)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigPrepare_SSHUsername(t *testing.T) {
|
||||
c := testConfig()
|
||||
c.SSHUsername = ""
|
||||
c.Comm.SSHUsername = ""
|
||||
if err := c.Prepare(nil); len(err) != 1 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
|
|
@ -10,9 +10,9 @@ import (
|
|||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// SSHAddress returns a function that can be given to the SSH communicator
|
||||
// SSHHost returns a function that can be given to the SSH communicator
|
||||
// for determining the SSH address based on the instance DNS name.
|
||||
func SSHAddress(e *ec2.EC2, port int, private bool) func(multistep.StateBag) (string, error) {
|
||||
func SSHHost(e *ec2.EC2, private bool) func(multistep.StateBag) (string, error) {
|
||||
return func(state multistep.StateBag) (string, error) {
|
||||
for j := 0; j < 2; j++ {
|
||||
var host string
|
||||
|
@ -28,7 +28,7 @@ func SSHAddress(e *ec2.EC2, port int, private bool) func(multistep.StateBag) (st
|
|||
}
|
||||
|
||||
if host != "" {
|
||||
return fmt.Sprintf("%s:%d", host, port), nil
|
||||
return host, nil
|
||||
}
|
||||
|
||||
r, err := e.DescribeInstances(&ec2.DescribeInstancesInput{
|
||||
|
|
|
@ -67,10 +67,10 @@ func AMIStateRefreshFunc(conn *ec2.EC2, imageId string) StateRefreshFunc {
|
|||
|
||||
// InstanceStateRefreshFunc returns a StateRefreshFunc that is used to watch
|
||||
// an EC2 instance.
|
||||
func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) StateRefreshFunc {
|
||||
func InstanceStateRefreshFunc(conn *ec2.EC2, instanceId string) StateRefreshFunc {
|
||||
return func() (interface{}, string, error) {
|
||||
resp, err := conn.DescribeInstances(&ec2.DescribeInstancesInput{
|
||||
InstanceIDs: []*string{i.InstanceID},
|
||||
InstanceIDs: []*string{&instanceId},
|
||||
})
|
||||
if err != nil {
|
||||
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidInstanceID.NotFound" {
|
||||
|
@ -91,7 +91,7 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) StateRefreshFunc {
|
|||
return nil, "", nil
|
||||
}
|
||||
|
||||
i = resp.Reservations[0].Instances[0]
|
||||
i := resp.Reservations[0].Instances[0]
|
||||
return i, *i.State.Name, nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
type StepAMIRegionCopy struct {
|
||||
AccessConfig *AccessConfig
|
||||
Regions []string
|
||||
Name string
|
||||
}
|
||||
|
||||
func (s *StepAMIRegionCopy) Run(state multistep.StateBag) multistep.StepAction {
|
||||
|
@ -32,12 +33,18 @@ func (s *StepAMIRegionCopy) Run(state multistep.StateBag) multistep.StepAction {
|
|||
var wg sync.WaitGroup
|
||||
errs := new(packer.MultiError)
|
||||
for _, region := range s.Regions {
|
||||
if region == ec2conn.Config.Region {
|
||||
ui.Message(fmt.Sprintf(
|
||||
"Avoiding copying AMI to duplicate region %s", region))
|
||||
continue
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
ui.Message(fmt.Sprintf("Copying to: %s", region))
|
||||
|
||||
go func(region string) {
|
||||
defer wg.Done()
|
||||
id, err := amiRegionCopy(state, s.AccessConfig, ami, region, ec2conn.Config.Region)
|
||||
id, err := amiRegionCopy(state, s.AccessConfig, s.Name, ami, region, ec2conn.Config.Region)
|
||||
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
@ -69,7 +76,7 @@ func (s *StepAMIRegionCopy) Cleanup(state multistep.StateBag) {
|
|||
|
||||
// amiRegionCopy does a copy for the given AMI to the target region and
|
||||
// returns the resulting ID or error.
|
||||
func amiRegionCopy(state multistep.StateBag, config *AccessConfig, imageId string,
|
||||
func amiRegionCopy(state multistep.StateBag, config *AccessConfig, name string, imageId string,
|
||||
target string, source string) (string, error) {
|
||||
|
||||
// Connect to the region where the AMI will be copied to
|
||||
|
@ -83,6 +90,7 @@ func amiRegionCopy(state multistep.StateBag, config *AccessConfig, imageId strin
|
|||
resp, err := regionconn.CopyImage(&ec2.CopyImageInput{
|
||||
SourceRegion: &source,
|
||||
SourceImageID: &imageId,
|
||||
Name: &name,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
|
|
@ -25,19 +25,56 @@ func (s *StepCreateTags) Run(state multistep.StateBag) multistep.StepAction {
|
|||
var ec2Tags []*ec2.Tag
|
||||
for key, value := range s.Tags {
|
||||
ui.Message(fmt.Sprintf("Adding tag: \"%s\": \"%s\"", key, value))
|
||||
ec2Tags = append(ec2Tags, &ec2.Tag{Key: &key, Value: &value})
|
||||
ec2Tags = append(ec2Tags, &ec2.Tag{
|
||||
Key: aws.String(key),
|
||||
Value: aws.String(value),
|
||||
})
|
||||
}
|
||||
|
||||
// Declare list of resources to tag
|
||||
resourceIds := []*string{&ami}
|
||||
|
||||
// Retrieve image list for given AMI
|
||||
imageResp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{
|
||||
ImageIDs: resourceIds,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error retrieving details for AMI (%s): %s", ami, err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if len(imageResp.Images) == 0 {
|
||||
err := fmt.Errorf("Error retrieving details for AMI (%s), no images found", ami)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
image := imageResp.Images[0]
|
||||
|
||||
// Add only those with a Snapshot ID, i.e. not Ephemeral
|
||||
for _, device := range image.BlockDeviceMappings {
|
||||
if device.EBS != nil && device.EBS.SnapshotID != nil {
|
||||
ui.Say(fmt.Sprintf("Tagging snapshot: %s", *device.EBS.SnapshotID))
|
||||
resourceIds = append(resourceIds, device.EBS.SnapshotID)
|
||||
}
|
||||
}
|
||||
|
||||
regionconn := ec2.New(&aws.Config{
|
||||
Credentials: ec2conn.Config.Credentials,
|
||||
Region: region,
|
||||
})
|
||||
_, err := regionconn.CreateTags(&ec2.CreateTagsInput{
|
||||
Resources: []*string{&ami},
|
||||
|
||||
_, err = regionconn.CreateTags(&ec2.CreateTagsInput{
|
||||
Resources: resourceIds,
|
||||
Tags: ec2Tags,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error adding tags to AMI (%s): %s", ami, err)
|
||||
err := fmt.Errorf("Error adding tags to Resources (%#v): %s", resourceIds, err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
|
|
|
@ -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
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
|
@ -53,7 +54,14 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi
|
|||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Test if it is encoded already, and if not, encode it
|
||||
if _, err := base64.StdEncoding.DecodeString(string(contents)); err != nil {
|
||||
log.Printf("[DEBUG] base64 encoding user data...")
|
||||
contents = []byte(base64.StdEncoding.EncodeToString(contents))
|
||||
}
|
||||
|
||||
userData = string(contents)
|
||||
|
||||
}
|
||||
|
||||
ui.Say("Launching a source AWS instance...")
|
||||
|
@ -174,11 +182,15 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi
|
|||
ImageID: &s.SourceAMI,
|
||||
InstanceType: &s.InstanceType,
|
||||
UserData: &userData,
|
||||
SecurityGroupIDs: securityGroupIds,
|
||||
IAMInstanceProfile: &ec2.IAMInstanceProfileSpecification{Name: &s.IamInstanceProfile},
|
||||
SubnetID: &s.SubnetId,
|
||||
NetworkInterfaces: []*ec2.InstanceNetworkInterfaceSpecification{
|
||||
&ec2.InstanceNetworkInterfaceSpecification{AssociatePublicIPAddress: &s.AssociatePublicIpAddress},
|
||||
&ec2.InstanceNetworkInterfaceSpecification{
|
||||
DeviceIndex: aws.Long(0),
|
||||
AssociatePublicIPAddress: &s.AssociatePublicIpAddress,
|
||||
SubnetID: &s.SubnetId,
|
||||
Groups: securityGroupIds,
|
||||
DeleteOnTermination: aws.Boolean(true),
|
||||
},
|
||||
},
|
||||
Placement: &ec2.SpotPlacement{
|
||||
AvailabilityZone: &availabilityZone,
|
||||
|
@ -223,36 +235,17 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi
|
|||
instanceId = *spotResp.SpotInstanceRequests[0].InstanceID
|
||||
}
|
||||
|
||||
instanceResp, err := ec2conn.DescribeInstances(&ec2.DescribeInstancesInput{
|
||||
InstanceIDs: []*string{&instanceId}})
|
||||
for i := 0; i < 10; i++ {
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(3 * time.Second)
|
||||
instanceResp, err = ec2conn.DescribeInstances(&ec2.DescribeInstancesInput{
|
||||
InstanceIDs: []*string{&instanceId}})
|
||||
}
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error finding source instance (%s): %s", instanceId, err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
s.instance = instanceResp.Reservations[0].Instances[0]
|
||||
ui.Message(fmt.Sprintf("Instance ID: %s", *s.instance.InstanceID))
|
||||
|
||||
ui.Say(fmt.Sprintf("Waiting for instance (%s) to become ready...", *s.instance.InstanceID))
|
||||
ui.Message(fmt.Sprintf("Instance ID: %s", instanceId))
|
||||
ui.Say(fmt.Sprintf("Waiting for instance (%v) to become ready...", instanceId))
|
||||
stateChange := StateChangeConf{
|
||||
Pending: []string{"pending"},
|
||||
Target: "running",
|
||||
Refresh: InstanceStateRefreshFunc(ec2conn, s.instance),
|
||||
Refresh: InstanceStateRefreshFunc(ec2conn, instanceId),
|
||||
StepState: state,
|
||||
}
|
||||
latestInstance, err := WaitForState(&stateChange)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for instance (%s) to become ready: %s", *s.instance.InstanceID, err)
|
||||
err := fmt.Errorf("Error waiting for instance (%s) to become ready: %s", instanceId, err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
|
@ -329,7 +322,7 @@ func (s *StepRunSourceInstance) Cleanup(state multistep.StateBag) {
|
|||
}
|
||||
stateChange := StateChangeConf{
|
||||
Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"},
|
||||
Refresh: InstanceStateRefreshFunc(ec2conn, s.instance),
|
||||
Refresh: InstanceStateRefreshFunc(ec2conn, *s.instance.InstanceID),
|
||||
Target: "terminated",
|
||||
}
|
||||
|
||||
|
|
|
@ -9,12 +9,13 @@ import (
|
|||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common/uuid"
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type StepSecurityGroup struct {
|
||||
CommConfig *communicator.Config
|
||||
SecurityGroupIds []string
|
||||
SSHPort int
|
||||
VpcId string
|
||||
|
||||
createdGroupId string
|
||||
|
@ -30,8 +31,9 @@ func (s *StepSecurityGroup) Run(state multistep.StateBag) multistep.StepAction {
|
|||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
if s.SSHPort == 0 {
|
||||
panic("SSHPort must be set to a non-zero value.")
|
||||
port := s.CommConfig.Port()
|
||||
if port == 0 {
|
||||
panic("port must be set to a non-zero value.")
|
||||
}
|
||||
|
||||
// Create the group
|
||||
|
@ -57,15 +59,17 @@ func (s *StepSecurityGroup) Run(state multistep.StateBag) multistep.StepAction {
|
|||
req := &ec2.AuthorizeSecurityGroupIngressInput{
|
||||
GroupID: groupResp.GroupID,
|
||||
IPProtocol: aws.String("tcp"),
|
||||
FromPort: aws.Long(int64(s.SSHPort)),
|
||||
ToPort: aws.Long(int64(s.SSHPort)),
|
||||
FromPort: aws.Long(int64(port)),
|
||||
ToPort: aws.Long(int64(port)),
|
||||
CIDRIP: aws.String("0.0.0.0/0"),
|
||||
}
|
||||
|
||||
// We loop and retry this a few times because sometimes the security
|
||||
// group isn't available immediately because AWS resources are eventaully
|
||||
// consistent.
|
||||
ui.Say("Authorizing SSH access on the temporary security group...")
|
||||
ui.Say(fmt.Sprintf(
|
||||
"Authorizing access to port %d the temporary security group...",
|
||||
port))
|
||||
for i := 0; i < 5; i++ {
|
||||
_, err = ec2conn.AuthorizeSecurityGroupIngress(req)
|
||||
if err == nil {
|
||||
|
|
|
@ -20,7 +20,7 @@ func isalphanumeric(b byte) bool {
|
|||
|
||||
// Clean up AMI name by replacing invalid characters with "-"
|
||||
func templateCleanAMIName(s string) string {
|
||||
allowed := []byte{'(', ')', ',', '/', '-', '_'}
|
||||
allowed := []byte{'(', ')', ',', '/', '-', '_', ' '}
|
||||
b := []byte(s)
|
||||
newb := make([]byte, len(b))
|
||||
for i, c := range b {
|
||||
|
|
|
@ -5,8 +5,8 @@ import (
|
|||
)
|
||||
|
||||
func TestAMITemplatePrepare_clean(t *testing.T) {
|
||||
origName := "AMZamz09(),/-_:&^$%"
|
||||
expected := "AMZamz09(),/-_-----"
|
||||
origName := "AMZamz09(),/-_:&^ $%"
|
||||
expected := "AMZamz09(),/-_--- --"
|
||||
|
||||
name := templateCleanAMIName(origName)
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/mitchellh/multistep"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
"github.com/mitchellh/packer/helper/config"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
|
@ -78,6 +79,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
|
||||
// Build the steps
|
||||
steps := []multistep.Step{
|
||||
&awscommon.StepPreValidate{
|
||||
DestAmiName: b.config.AMIName,
|
||||
ForceDeregister: b.config.AMIForceDeregister,
|
||||
},
|
||||
&awscommon.StepSourceAMIInfo{
|
||||
SourceAmi: b.config.SourceAmi,
|
||||
EnhancedNetworking: b.config.AMIEnhancedNetworking,
|
||||
|
@ -86,11 +91,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
Debug: b.config.PackerDebug,
|
||||
DebugKeyPath: fmt.Sprintf("ec2_%s.pem", b.config.PackerBuildName),
|
||||
KeyPairName: b.config.TemporaryKeyPairName,
|
||||
PrivateKeyFile: b.config.SSHPrivateKeyFile,
|
||||
PrivateKeyFile: b.config.RunConfig.Comm.SSHPrivateKey,
|
||||
},
|
||||
&awscommon.StepSecurityGroup{
|
||||
SecurityGroupIds: b.config.SecurityGroupIds,
|
||||
SSHPort: b.config.SSHPort,
|
||||
CommConfig: &b.config.RunConfig.Comm,
|
||||
VpcId: b.config.VpcId,
|
||||
},
|
||||
&awscommon.StepRunSourceInstance{
|
||||
|
@ -109,20 +114,31 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
BlockDevices: b.config.BlockDevices,
|
||||
Tags: b.config.RunTags,
|
||||
},
|
||||
&common.StepConnectSSH{
|
||||
SSHAddress: awscommon.SSHAddress(
|
||||
ec2conn, b.config.SSHPort, b.config.SSHPrivateIp),
|
||||
SSHConfig: awscommon.SSHConfig(b.config.SSHUsername),
|
||||
SSHWaitTimeout: b.config.SSHTimeout(),
|
||||
&awscommon.StepGetPassword{
|
||||
Comm: &b.config.RunConfig.Comm,
|
||||
Timeout: b.config.WindowsPasswordTimeout,
|
||||
},
|
||||
&communicator.StepConnect{
|
||||
Config: &b.config.RunConfig.Comm,
|
||||
Host: awscommon.SSHHost(
|
||||
ec2conn,
|
||||
b.config.SSHPrivateIp),
|
||||
SSHConfig: awscommon.SSHConfig(
|
||||
b.config.RunConfig.Comm.SSHUsername),
|
||||
},
|
||||
&common.StepProvision{},
|
||||
&stepStopInstance{SpotPrice: b.config.SpotPrice},
|
||||
// TODO(mitchellh): verify works with spots
|
||||
&stepModifyInstance{},
|
||||
&awscommon.StepDeregisterAMI{
|
||||
ForceDeregister: b.config.AMIForceDeregister,
|
||||
AMIName: b.config.AMIName,
|
||||
},
|
||||
&stepCreateAMI{},
|
||||
&awscommon.StepAMIRegionCopy{
|
||||
AccessConfig: &b.config.AccessConfig,
|
||||
Regions: b.config.AMIRegions,
|
||||
Name: b.config.AMIName,
|
||||
},
|
||||
&awscommon.StepModifyAMIAttributes{
|
||||
Description: b.config.AMIDescription,
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
package ebs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mitchellh/packer/builder/amazon/common"
|
||||
builderT "github.com/mitchellh/packer/helper/builder/testing"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
func TestBuilderAcc_basic(t *testing.T) {
|
||||
|
@ -15,6 +19,64 @@ func TestBuilderAcc_basic(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestBuilderAcc_regionCopy(t *testing.T) {
|
||||
builderT.Test(t, builderT.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Builder: &Builder{},
|
||||
Template: testBuilderAccRegionCopy,
|
||||
Check: checkRegionCopy([]string{"us-east-1", "us-west-2"}),
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuilderAcc_forceDeregister(t *testing.T) {
|
||||
// Build the same AMI name twice, with force_deregister on the second run
|
||||
builderT.Test(t, builderT.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Builder: &Builder{},
|
||||
Template: buildForceDeregisterConfig("false", "dereg"),
|
||||
SkipArtifactTeardown: true,
|
||||
})
|
||||
|
||||
builderT.Test(t, builderT.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Builder: &Builder{},
|
||||
Template: buildForceDeregisterConfig("true", "dereg"),
|
||||
})
|
||||
}
|
||||
|
||||
func checkRegionCopy(regions []string) builderT.TestCheckFunc {
|
||||
return func(artifacts []packer.Artifact) error {
|
||||
if len(artifacts) > 1 {
|
||||
return fmt.Errorf("more than 1 artifact")
|
||||
}
|
||||
|
||||
// Get the actual *Artifact pointer so we can access the AMIs directly
|
||||
artifactRaw := artifacts[0]
|
||||
artifact, ok := artifactRaw.(*common.Artifact)
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown artifact: %#v", artifactRaw)
|
||||
}
|
||||
|
||||
// Verify that we copied to only the regions given
|
||||
regionSet := make(map[string]struct{})
|
||||
for _, r := range regions {
|
||||
regionSet[r] = struct{}{}
|
||||
}
|
||||
for r, _ := range artifact.Amis {
|
||||
if _, ok := regionSet[r]; !ok {
|
||||
return fmt.Errorf("unknown region: %s", r)
|
||||
}
|
||||
|
||||
delete(regionSet, r)
|
||||
}
|
||||
if len(regionSet) > 0 {
|
||||
return fmt.Errorf("didn't copy to: %#v", regionSet)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func testAccPreCheck(t *testing.T) {
|
||||
if v := os.Getenv("AWS_ACCESS_KEY_ID"); v == "" {
|
||||
t.Fatal("AWS_ACCESS_KEY_ID must be set for acceptance tests")
|
||||
|
@ -25,6 +87,16 @@ func testAccPreCheck(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func testEC2Conn() (*ec2.EC2, error) {
|
||||
access := &common.AccessConfig{RawRegion: "us-east-1"}
|
||||
config, err := access.Config()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ec2.New(config), nil
|
||||
}
|
||||
|
||||
const testBuilderAccBasic = `
|
||||
{
|
||||
"builders": [{
|
||||
|
@ -37,3 +109,35 @@ const testBuilderAccBasic = `
|
|||
}]
|
||||
}
|
||||
`
|
||||
|
||||
const testBuilderAccRegionCopy = `
|
||||
{
|
||||
"builders": [{
|
||||
"type": "test",
|
||||
"region": "us-east-1",
|
||||
"instance_type": "m3.medium",
|
||||
"source_ami": "ami-76b2a71e",
|
||||
"ssh_username": "ubuntu",
|
||||
"ami_name": "packer-test {{timestamp}}",
|
||||
"ami_regions": ["us-east-1", "us-west-2"]
|
||||
}]
|
||||
}
|
||||
`
|
||||
|
||||
const testBuilderAccForceDeregister = `
|
||||
{
|
||||
"builders": [{
|
||||
"type": "test",
|
||||
"region": "us-east-1",
|
||||
"instance_type": "m3.medium",
|
||||
"source_ami": "ami-76b2a71e",
|
||||
"ssh_username": "ubuntu",
|
||||
"force_deregister": "%s",
|
||||
"ami_name": "packer-test-%s"
|
||||
}]
|
||||
}
|
||||
`
|
||||
|
||||
func buildForceDeregisterConfig(name, flag string) string {
|
||||
return fmt.Sprintf(testBuilderAccForceDeregister, name, flag)
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ func (s *stepStopInstance) Run(state multistep.StateBag) multistep.StepAction {
|
|||
stateChange := awscommon.StateChangeConf{
|
||||
Pending: []string{"running", "stopping"},
|
||||
Target: "stopped",
|
||||
Refresh: awscommon.InstanceStateRefreshFunc(ec2conn, instance),
|
||||
Refresh: awscommon.InstanceStateRefreshFunc(ec2conn, *instance.InstanceID),
|
||||
StepState: state,
|
||||
}
|
||||
_, err = awscommon.WaitForState(&stateChange)
|
||||
|
|
|
@ -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"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
"github.com/mitchellh/packer/helper/config"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
|
@ -73,15 +74,25 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
}
|
||||
|
||||
if b.config.BundleUploadCommand == "" {
|
||||
b.config.BundleUploadCommand = "sudo -i -n ec2-upload-bundle " +
|
||||
"-b {{.BucketName}} " +
|
||||
"-m {{.ManifestPath}} " +
|
||||
"-a {{.AccessKey}} " +
|
||||
"-s {{.SecretKey}} " +
|
||||
"-d {{.BundleDirectory}} " +
|
||||
"--batch " +
|
||||
"--location {{.Region}} " +
|
||||
"--retry"
|
||||
if b.config.IamInstanceProfile != "" {
|
||||
b.config.BundleUploadCommand = "sudo -i -n ec2-upload-bundle " +
|
||||
"-b {{.BucketName}} " +
|
||||
"-m {{.ManifestPath}} " +
|
||||
"-d {{.BundleDirectory}} " +
|
||||
"--batch " +
|
||||
"--region {{.Region}} " +
|
||||
"--retry"
|
||||
} else {
|
||||
b.config.BundleUploadCommand = "sudo -i -n ec2-upload-bundle " +
|
||||
"-b {{.BucketName}} " +
|
||||
"-m {{.ManifestPath}} " +
|
||||
"-a {{.AccessKey}} " +
|
||||
"-s {{.SecretKey}} " +
|
||||
"-d {{.BundleDirectory}} " +
|
||||
"--batch " +
|
||||
"--region {{.Region}} " +
|
||||
"--retry"
|
||||
}
|
||||
}
|
||||
|
||||
if b.config.BundleVolCommand == "" {
|
||||
|
@ -157,6 +168,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
|
||||
// Build the steps
|
||||
steps := []multistep.Step{
|
||||
&awscommon.StepPreValidate{
|
||||
DestAmiName: b.config.AMIName,
|
||||
ForceDeregister: b.config.AMIForceDeregister,
|
||||
},
|
||||
&awscommon.StepSourceAMIInfo{
|
||||
SourceAmi: b.config.SourceAmi,
|
||||
EnhancedNetworking: b.config.AMIEnhancedNetworking,
|
||||
|
@ -165,11 +180,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
Debug: b.config.PackerDebug,
|
||||
DebugKeyPath: fmt.Sprintf("ec2_%s.pem", b.config.PackerBuildName),
|
||||
KeyPairName: b.config.TemporaryKeyPairName,
|
||||
PrivateKeyFile: b.config.SSHPrivateKeyFile,
|
||||
PrivateKeyFile: b.config.RunConfig.Comm.SSHPrivateKey,
|
||||
},
|
||||
&awscommon.StepSecurityGroup{
|
||||
CommConfig: &b.config.RunConfig.Comm,
|
||||
SecurityGroupIds: b.config.SecurityGroupIds,
|
||||
SSHPort: b.config.SSHPort,
|
||||
VpcId: b.config.VpcId,
|
||||
},
|
||||
&awscommon.StepRunSourceInstance{
|
||||
|
@ -187,11 +202,17 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
BlockDevices: b.config.BlockDevices,
|
||||
Tags: b.config.RunTags,
|
||||
},
|
||||
&common.StepConnectSSH{
|
||||
SSHAddress: awscommon.SSHAddress(
|
||||
ec2conn, b.config.SSHPort, b.config.SSHPrivateIp),
|
||||
SSHConfig: awscommon.SSHConfig(b.config.SSHUsername),
|
||||
SSHWaitTimeout: b.config.SSHTimeout(),
|
||||
&awscommon.StepGetPassword{
|
||||
Comm: &b.config.RunConfig.Comm,
|
||||
Timeout: b.config.WindowsPasswordTimeout,
|
||||
},
|
||||
&communicator.StepConnect{
|
||||
Config: &b.config.RunConfig.Comm,
|
||||
Host: awscommon.SSHHost(
|
||||
ec2conn,
|
||||
b.config.SSHPrivateIp),
|
||||
SSHConfig: awscommon.SSHConfig(
|
||||
b.config.RunConfig.Comm.SSHUsername),
|
||||
},
|
||||
&common.StepProvision{},
|
||||
&StepUploadX509Cert{},
|
||||
|
@ -201,10 +222,15 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
&StepUploadBundle{
|
||||
Debug: b.config.PackerDebug,
|
||||
},
|
||||
&awscommon.StepDeregisterAMI{
|
||||
ForceDeregister: b.config.AMIForceDeregister,
|
||||
AMIName: b.config.AMIName,
|
||||
},
|
||||
&StepRegisterAMI{},
|
||||
&awscommon.StepAMIRegionCopy{
|
||||
AccessConfig: &b.config.AccessConfig,
|
||||
Regions: b.config.AMIRegions,
|
||||
Name: b.config.AMIName,
|
||||
},
|
||||
&awscommon.StepModifyAMIAttributes{
|
||||
Description: b.config.AMIDescription,
|
||||
|
|
|
@ -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"
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"github.com/digitalocean/godo"
|
||||
)
|
||||
|
||||
type Artifact struct {
|
||||
|
@ -11,13 +13,13 @@ type Artifact struct {
|
|||
snapshotName string
|
||||
|
||||
// The ID of the image
|
||||
snapshotId uint
|
||||
snapshotId int
|
||||
|
||||
// The name of the region
|
||||
regionName string
|
||||
|
||||
// The client for making API calls
|
||||
client DigitalOceanClient
|
||||
client *godo.Client
|
||||
}
|
||||
|
||||
func (*Artifact) BuilderId() string {
|
||||
|
@ -43,5 +45,6 @@ func (a *Artifact) State(name string) interface{} {
|
|||
|
||||
func (a *Artifact) Destroy() error {
|
||||
log.Printf("Destroying image: %d (%s)", a.snapshotId, a.snapshotName)
|
||||
return a.client.DestroyImage(a.snapshotId)
|
||||
_, err := a.client.Images.Delete(a.snapshotId)
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -4,208 +4,39 @@
|
|||
package digitalocean
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/digitalocean/godo"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/common/uuid"
|
||||
"github.com/mitchellh/packer/helper/config"
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// see https://api.digitalocean.com/images/?client_id=[client_id]&api_key=[api_key]
|
||||
// name="Ubuntu 12.04.4 x64", id=6374128,
|
||||
const DefaultImage = "ubuntu-12-04-x64"
|
||||
|
||||
// see https://api.digitalocean.com/regions/?client_id=[client_id]&api_key=[api_key]
|
||||
// name="New York 3", id=8
|
||||
const DefaultRegion = "nyc3"
|
||||
|
||||
// see https://api.digitalocean.com/sizes/?client_id=[client_id]&api_key=[api_key]
|
||||
// name="512MB", id=66 (the smallest droplet size)
|
||||
const DefaultSize = "512mb"
|
||||
|
||||
// The unique id for the builder
|
||||
const BuilderId = "pearkes.digitalocean"
|
||||
|
||||
// Configuration tells the builder the credentials
|
||||
// to use while communicating with DO and describes the image
|
||||
// you are creating
|
||||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
|
||||
ClientID string `mapstructure:"client_id"`
|
||||
APIKey string `mapstructure:"api_key"`
|
||||
APIURL string `mapstructure:"api_url"`
|
||||
APIToken string `mapstructure:"api_token"`
|
||||
RegionID uint `mapstructure:"region_id"`
|
||||
SizeID uint `mapstructure:"size_id"`
|
||||
ImageID uint `mapstructure:"image_id"`
|
||||
|
||||
Region string `mapstructure:"region"`
|
||||
Size string `mapstructure:"size"`
|
||||
Image string `mapstructure:"image"`
|
||||
|
||||
PrivateNetworking bool `mapstructure:"private_networking"`
|
||||
SnapshotName string `mapstructure:"snapshot_name"`
|
||||
DropletName string `mapstructure:"droplet_name"`
|
||||
SSHUsername string `mapstructure:"ssh_username"`
|
||||
SSHPort uint `mapstructure:"ssh_port"`
|
||||
|
||||
RawSSHTimeout string `mapstructure:"ssh_timeout"`
|
||||
RawStateTimeout string `mapstructure:"state_timeout"`
|
||||
|
||||
// These are unexported since they're set by other fields
|
||||
// being set.
|
||||
sshTimeout time.Duration
|
||||
stateTimeout time.Duration
|
||||
|
||||
ctx *interpolate.Context
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
config Config
|
||||
runner multistep.Runner
|
||||
}
|
||||
|
||||
func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||
err := config.Decode(&b.config, &config.DecodeOpts{
|
||||
Interpolate: true,
|
||||
}, raws...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
c, warnings, errs := NewConfig(raws...)
|
||||
if errs != nil {
|
||||
return warnings, errs
|
||||
}
|
||||
b.config = *c
|
||||
|
||||
// Optional configuration with defaults
|
||||
if b.config.APIKey == "" {
|
||||
// Default to environment variable for api_key, if it exists
|
||||
b.config.APIKey = os.Getenv("DIGITALOCEAN_API_KEY")
|
||||
}
|
||||
|
||||
if b.config.ClientID == "" {
|
||||
// Default to environment variable for client_id, if it exists
|
||||
b.config.ClientID = os.Getenv("DIGITALOCEAN_CLIENT_ID")
|
||||
}
|
||||
|
||||
if b.config.APIURL == "" {
|
||||
// Default to environment variable for api_url, if it exists
|
||||
b.config.APIURL = os.Getenv("DIGITALOCEAN_API_URL")
|
||||
}
|
||||
|
||||
if b.config.APIToken == "" {
|
||||
// Default to environment variable for api_token, if it exists
|
||||
b.config.APIToken = os.Getenv("DIGITALOCEAN_API_TOKEN")
|
||||
}
|
||||
|
||||
if b.config.Region == "" {
|
||||
if b.config.RegionID != 0 {
|
||||
b.config.Region = fmt.Sprintf("%v", b.config.RegionID)
|
||||
} else {
|
||||
b.config.Region = DefaultRegion
|
||||
}
|
||||
}
|
||||
|
||||
if b.config.Size == "" {
|
||||
if b.config.SizeID != 0 {
|
||||
b.config.Size = fmt.Sprintf("%v", b.config.SizeID)
|
||||
} else {
|
||||
b.config.Size = DefaultSize
|
||||
}
|
||||
}
|
||||
|
||||
if b.config.Image == "" {
|
||||
if b.config.ImageID != 0 {
|
||||
b.config.Image = fmt.Sprintf("%v", b.config.ImageID)
|
||||
} else {
|
||||
b.config.Image = DefaultImage
|
||||
}
|
||||
}
|
||||
|
||||
if b.config.SnapshotName == "" {
|
||||
// Default to packer-{{ unix timestamp (utc) }}
|
||||
b.config.SnapshotName = "packer-{{timestamp}}"
|
||||
}
|
||||
|
||||
if b.config.DropletName == "" {
|
||||
// Default to packer-[time-ordered-uuid]
|
||||
b.config.DropletName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
|
||||
}
|
||||
|
||||
if b.config.SSHUsername == "" {
|
||||
// Default to "root". You can override this if your
|
||||
// SourceImage has a different user account then the DO default
|
||||
b.config.SSHUsername = "root"
|
||||
}
|
||||
|
||||
if b.config.SSHPort == 0 {
|
||||
// Default to port 22 per DO default
|
||||
b.config.SSHPort = 22
|
||||
}
|
||||
|
||||
if b.config.RawSSHTimeout == "" {
|
||||
// Default to 1 minute timeouts
|
||||
b.config.RawSSHTimeout = "1m"
|
||||
}
|
||||
|
||||
if b.config.RawStateTimeout == "" {
|
||||
// Default to 6 minute timeouts waiting for
|
||||
// desired state. i.e waiting for droplet to become active
|
||||
b.config.RawStateTimeout = "6m"
|
||||
}
|
||||
|
||||
var errs *packer.MultiError
|
||||
if b.config.APIToken == "" {
|
||||
// Required configurations that will display errors if not set
|
||||
if b.config.ClientID == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("a client_id for v1 auth or api_token for v2 auth must be specified"))
|
||||
}
|
||||
|
||||
if b.config.APIKey == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("a api_key for v1 auth or api_token for v2 auth must be specified"))
|
||||
}
|
||||
}
|
||||
|
||||
if b.config.APIURL == "" {
|
||||
b.config.APIURL = "https://api.digitalocean.com"
|
||||
}
|
||||
|
||||
sshTimeout, err := time.ParseDuration(b.config.RawSSHTimeout)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
|
||||
}
|
||||
b.config.sshTimeout = sshTimeout
|
||||
|
||||
stateTimeout, err := time.ParseDuration(b.config.RawStateTimeout)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Failed parsing state_timeout: %s", err))
|
||||
}
|
||||
b.config.stateTimeout = stateTimeout
|
||||
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
common.ScrubConfig(b.config, b.config.ClientID, b.config.APIKey)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
||||
var client DigitalOceanClient
|
||||
// Initialize the DO API client
|
||||
if b.config.APIToken == "" {
|
||||
client = DigitalOceanClientNewV1(b.config.ClientID, b.config.APIKey, b.config.APIURL)
|
||||
} else {
|
||||
client = DigitalOceanClientNewV2(b.config.APIToken, b.config.APIURL)
|
||||
}
|
||||
client := godo.NewClient(oauth2.NewClient(oauth2.NoContext, &apiTokenSource{
|
||||
AccessToken: b.config.APIToken,
|
||||
}))
|
||||
|
||||
// Set up the state
|
||||
state := new(multistep.BasicStateBag)
|
||||
|
@ -216,13 +47,16 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
|
||||
// Build the steps
|
||||
steps := []multistep.Step{
|
||||
new(stepCreateSSHKey),
|
||||
&stepCreateSSHKey{
|
||||
Debug: b.config.PackerDebug,
|
||||
DebugKeyPath: fmt.Sprintf("do_%s.pem", b.config.PackerBuildName),
|
||||
},
|
||||
new(stepCreateDroplet),
|
||||
new(stepDropletInfo),
|
||||
&common.StepConnectSSH{
|
||||
SSHAddress: sshAddress,
|
||||
SSHConfig: sshConfig,
|
||||
SSHWaitTimeout: 5 * time.Minute,
|
||||
&communicator.StepConnect{
|
||||
Config: &b.config.Comm,
|
||||
Host: commHost,
|
||||
SSHConfig: sshConfig,
|
||||
},
|
||||
new(common.StepProvision),
|
||||
new(stepShutdown),
|
||||
|
@ -252,26 +86,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
sregion := state.Get("region")
|
||||
|
||||
var region string
|
||||
|
||||
if sregion != nil {
|
||||
region = sregion.(string)
|
||||
} else {
|
||||
region = fmt.Sprintf("%v", state.Get("region_id").(uint))
|
||||
}
|
||||
|
||||
found_region, err := client.Region(region)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
artifact := &Artifact{
|
||||
snapshotName: state.Get("snapshot_name").(string),
|
||||
snapshotId: state.Get("snapshot_image_id").(uint),
|
||||
regionName: found_region.Name,
|
||||
snapshotId: state.Get("snapshot_image_id").(int),
|
||||
regionName: state.Get("region").(string),
|
||||
client: client,
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
"time"
|
||||
|
||||
func init() {
|
||||
// Clear out the credential env vars
|
||||
os.Setenv("DIGITALOCEAN_API_KEY", "")
|
||||
os.Setenv("DIGITALOCEAN_CLIENT_ID", "")
|
||||
}
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
func testConfig() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"client_id": "foo",
|
||||
"api_key": "bar",
|
||||
"api_token": "bar",
|
||||
"region": "nyc2",
|
||||
"size": "512mb",
|
||||
"image": "foo",
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,90 +40,6 @@ func TestBuilder_Prepare_BadType(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_APIKey(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test good
|
||||
config["api_key"] = "foo"
|
||||
warnings, err := b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.APIKey != "foo" {
|
||||
t.Errorf("access key invalid: %s", b.config.APIKey)
|
||||
}
|
||||
|
||||
// Test bad
|
||||
delete(config, "api_key")
|
||||
b = Builder{}
|
||||
warnings, err = b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test env variable
|
||||
delete(config, "api_key")
|
||||
os.Setenv("DIGITALOCEAN_API_KEY", "foo")
|
||||
defer os.Setenv("DIGITALOCEAN_API_KEY", "")
|
||||
warnings, err = b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_ClientID(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test good
|
||||
config["client_id"] = "foo"
|
||||
warnings, err := b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.ClientID != "foo" {
|
||||
t.Errorf("invalid: %s", b.config.ClientID)
|
||||
}
|
||||
|
||||
// Test bad
|
||||
delete(config, "client_id")
|
||||
b = Builder{}
|
||||
warnings, err = b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test env variable
|
||||
delete(config, "client_id")
|
||||
os.Setenv("DIGITALOCEAN_CLIENT_ID", "foo")
|
||||
defer os.Setenv("DIGITALOCEAN_CLIENT_ID", "")
|
||||
warnings, err = b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_InvalidKey(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
@ -147,22 +60,18 @@ func TestBuilderPrepare_Region(t *testing.T) {
|
|||
config := testConfig()
|
||||
|
||||
// Test default
|
||||
delete(config, "region")
|
||||
warnings, err := b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.Region != DefaultRegion {
|
||||
t.Errorf("found %s, expected %s", b.config.Region, DefaultRegion)
|
||||
if err == nil {
|
||||
t.Fatalf("should error")
|
||||
}
|
||||
|
||||
expected := "sfo1"
|
||||
|
||||
// Test set
|
||||
config["region_id"] = 0
|
||||
config["region"] = expected
|
||||
b = Builder{}
|
||||
warnings, err = b.Prepare(config)
|
||||
|
@ -183,22 +92,18 @@ func TestBuilderPrepare_Size(t *testing.T) {
|
|||
config := testConfig()
|
||||
|
||||
// Test default
|
||||
delete(config, "size")
|
||||
warnings, err := b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.Size != DefaultSize {
|
||||
t.Errorf("found %s, expected %s", b.config.Size, DefaultSize)
|
||||
if err == nil {
|
||||
t.Fatalf("should error")
|
||||
}
|
||||
|
||||
expected := "1024mb"
|
||||
|
||||
// Test set
|
||||
config["size_id"] = 0
|
||||
config["size"] = expected
|
||||
b = Builder{}
|
||||
warnings, err = b.Prepare(config)
|
||||
|
@ -219,22 +124,18 @@ func TestBuilderPrepare_Image(t *testing.T) {
|
|||
config := testConfig()
|
||||
|
||||
// Test default
|
||||
delete(config, "image")
|
||||
warnings, err := b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.Image != DefaultImage {
|
||||
t.Errorf("found %s, expected %s", b.config.Image, DefaultImage)
|
||||
if err == nil {
|
||||
t.Fatal("should error")
|
||||
}
|
||||
|
||||
expected := "ubuntu-14-04-x64"
|
||||
|
||||
// Test set
|
||||
config["image_id"] = 0
|
||||
config["image"] = expected
|
||||
b = Builder{}
|
||||
warnings, err = b.Prepare(config)
|
||||
|
@ -263,8 +164,8 @@ func TestBuilderPrepare_SSHUsername(t *testing.T) {
|
|||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.SSHUsername != "root" {
|
||||
t.Errorf("invalid: %s", b.config.SSHUsername)
|
||||
if b.config.Comm.SSHUsername != "root" {
|
||||
t.Errorf("invalid: %s", b.config.Comm.SSHUsername)
|
||||
}
|
||||
|
||||
// Test set
|
||||
|
@ -278,52 +179,11 @@ func TestBuilderPrepare_SSHUsername(t *testing.T) {
|
|||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.SSHUsername != "foo" {
|
||||
t.Errorf("invalid: %s", b.config.SSHUsername)
|
||||
if b.config.Comm.SSHUsername != "foo" {
|
||||
t.Errorf("invalid: %s", b.config.Comm.SSHUsername)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_SSHTimeout(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test default
|
||||
warnings, err := b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.RawSSHTimeout != "1m" {
|
||||
t.Errorf("invalid: %s", b.config.RawSSHTimeout)
|
||||
}
|
||||
|
||||
// Test set
|
||||
config["ssh_timeout"] = "30s"
|
||||
b = Builder{}
|
||||
warnings, err = b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
// Test bad
|
||||
config["ssh_timeout"] = "tubes"
|
||||
b = Builder{}
|
||||
warnings, err = b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_StateTimeout(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
@ -337,8 +197,8 @@ func TestBuilderPrepare_StateTimeout(t *testing.T) {
|
|||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.RawStateTimeout != "6m" {
|
||||
t.Errorf("invalid: %s", b.config.RawStateTimeout)
|
||||
if b.config.StateTimeout != 6*time.Minute {
|
||||
t.Errorf("invalid: %s", b.config.StateTimeout)
|
||||
}
|
||||
|
||||
// Test set
|
||||
|
|
|
@ -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
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/ssh"
|
||||
"fmt"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
)
|
||||
|
||||
func sshAddress(state multistep.StateBag) (string, error) {
|
||||
config := state.Get("config").(Config)
|
||||
func commHost(state multistep.StateBag) (string, error) {
|
||||
ipAddress := state.Get("droplet_ip").(string)
|
||||
return fmt.Sprintf("%s:%d", ipAddress, config.SSHPort), nil
|
||||
return ipAddress, nil
|
||||
}
|
||||
|
||||
func sshConfig(state multistep.StateBag) (*ssh.ClientConfig, error) {
|
||||
|
@ -22,7 +22,7 @@ func sshConfig(state multistep.StateBag) (*ssh.ClientConfig, error) {
|
|||
}
|
||||
|
||||
return &ssh.ClientConfig{
|
||||
User: config.SSHUsername,
|
||||
User: config.Comm.SSHUsername,
|
||||
Auth: []ssh.AuthMethod{
|
||||
ssh.PublicKeys(signer),
|
||||
},
|
||||
|
|
|
@ -3,25 +3,36 @@ package digitalocean
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/digitalocean/godo"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type stepCreateDroplet struct {
|
||||
dropletId uint
|
||||
dropletId int
|
||||
}
|
||||
|
||||
func (s *stepCreateDroplet) Run(state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(DigitalOceanClient)
|
||||
client := state.Get("client").(*godo.Client)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
c := state.Get("config").(Config)
|
||||
sshKeyId := state.Get("ssh_key_id").(uint)
|
||||
|
||||
ui.Say("Creating droplet...")
|
||||
sshKeyId := state.Get("ssh_key_id").(int)
|
||||
|
||||
// Create the droplet based on configuration
|
||||
dropletId, err := client.CreateDroplet(c.DropletName, c.Size, c.Image, c.Region, sshKeyId, c.PrivateNetworking)
|
||||
|
||||
ui.Say("Creating droplet...")
|
||||
droplet, _, err := client.Droplets.Create(&godo.DropletCreateRequest{
|
||||
Name: c.DropletName,
|
||||
Region: c.Region,
|
||||
Size: c.Size,
|
||||
Image: godo.DropletCreateImage{
|
||||
Slug: c.Image,
|
||||
},
|
||||
SSHKeys: []godo.DropletCreateSSHKey{
|
||||
godo.DropletCreateSSHKey{ID: int(sshKeyId)},
|
||||
},
|
||||
PrivateNetworking: c.PrivateNetworking,
|
||||
UserData: c.UserData,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating droplet: %s", err)
|
||||
state.Put("error", err)
|
||||
|
@ -30,10 +41,10 @@ func (s *stepCreateDroplet) Run(state multistep.StateBag) multistep.StepAction {
|
|||
}
|
||||
|
||||
// We use this in cleanup
|
||||
s.dropletId = dropletId
|
||||
s.dropletId = droplet.ID
|
||||
|
||||
// Store the droplet id for later
|
||||
state.Put("droplet_id", dropletId)
|
||||
state.Put("droplet_id", droplet.ID)
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
@ -44,19 +55,14 @@ func (s *stepCreateDroplet) Cleanup(state multistep.StateBag) {
|
|||
return
|
||||
}
|
||||
|
||||
client := state.Get("client").(DigitalOceanClient)
|
||||
client := state.Get("client").(*godo.Client)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
c := state.Get("config").(Config)
|
||||
|
||||
// Destroy the droplet we just created
|
||||
ui.Say("Destroying droplet...")
|
||||
|
||||
err := client.DestroyDroplet(s.dropletId)
|
||||
_, err := client.Droplets.Delete(s.dropletId)
|
||||
if err != nil {
|
||||
curlstr := fmt.Sprintf("curl '%v/droplets/%v/destroy?client_id=%v&api_key=%v'",
|
||||
c.APIURL, s.dropletId, c.ClientID, c.APIKey)
|
||||
|
||||
ui.Error(fmt.Sprintf(
|
||||
"Error destroying droplet. Please destroy it manually: %v", curlstr))
|
||||
"Error destroying droplet. Please destroy it manually: %s", err))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,19 +7,25 @@ import (
|
|||
"encoding/pem"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"code.google.com/p/gosshold/ssh"
|
||||
"github.com/digitalocean/godo"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common/uuid"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type stepCreateSSHKey struct {
|
||||
keyId uint
|
||||
Debug bool
|
||||
DebugKeyPath string
|
||||
|
||||
keyId int
|
||||
}
|
||||
|
||||
func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(DigitalOceanClient)
|
||||
client := state.Get("client").(*godo.Client)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say("Creating temporary ssh key for droplet...")
|
||||
|
@ -46,7 +52,10 @@ func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction {
|
|||
name := fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
|
||||
|
||||
// Create the key!
|
||||
keyId, err := client.CreateKey(name, pub_sshformat)
|
||||
key, _, err := client.Keys.Create(&godo.KeyCreateRequest{
|
||||
Name: name,
|
||||
PublicKey: pub_sshformat,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating temporary SSH key: %s", err)
|
||||
state.Put("error", err)
|
||||
|
@ -55,12 +64,37 @@ func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction {
|
|||
}
|
||||
|
||||
// We use this to check cleanup
|
||||
s.keyId = keyId
|
||||
s.keyId = key.ID
|
||||
|
||||
log.Printf("temporary ssh key name: %s", name)
|
||||
|
||||
// Remember some state for the future
|
||||
state.Put("ssh_key_id", keyId)
|
||||
state.Put("ssh_key_id", key.ID)
|
||||
|
||||
// If we're in debug mode, output the private key to the working directory.
|
||||
if s.Debug {
|
||||
ui.Message(fmt.Sprintf("Saving key for debug purposes: %s", s.DebugKeyPath))
|
||||
f, err := os.Create(s.DebugKeyPath)
|
||||
if err != nil {
|
||||
state.Put("error", fmt.Errorf("Error saving debug key: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Write the key out
|
||||
if _, err := f.Write(pem.EncodeToMemory(&priv_blk)); err != nil {
|
||||
state.Put("error", fmt.Errorf("Error saving debug key: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Chmod it so that it is SSH ready
|
||||
if runtime.GOOS != "windows" {
|
||||
if err := f.Chmod(0600); err != nil {
|
||||
state.Put("error", fmt.Errorf("Error setting permissions of debug key: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
@ -71,18 +105,14 @@ func (s *stepCreateSSHKey) Cleanup(state multistep.StateBag) {
|
|||
return
|
||||
}
|
||||
|
||||
client := state.Get("client").(DigitalOceanClient)
|
||||
client := state.Get("client").(*godo.Client)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
c := state.Get("config").(Config)
|
||||
|
||||
ui.Say("Deleting temporary ssh key...")
|
||||
err := client.DestroyKey(s.keyId)
|
||||
|
||||
curlstr := fmt.Sprintf("curl -H 'Authorization: Bearer #TOKEN#' -X DELETE '%v/v2/account/keys/%v'", c.APIURL, s.keyId)
|
||||
|
||||
_, err := client.Keys.DeleteByID(s.keyId)
|
||||
if err != nil {
|
||||
log.Printf("Error cleaning up ssh key: %v", err.Error())
|
||||
log.Printf("Error cleaning up ssh key: %s", err)
|
||||
ui.Error(fmt.Sprintf(
|
||||
"Error cleaning up ssh key. Please delete the key manually: %v", curlstr))
|
||||
"Error cleaning up ssh key. Please delete the key manually: %s", err))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package digitalocean
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/digitalocean/godo"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
@ -10,14 +11,14 @@ import (
|
|||
type stepDropletInfo struct{}
|
||||
|
||||
func (s *stepDropletInfo) Run(state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(DigitalOceanClient)
|
||||
client := state.Get("client").(*godo.Client)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
c := state.Get("config").(Config)
|
||||
dropletId := state.Get("droplet_id").(uint)
|
||||
dropletId := state.Get("droplet_id").(int)
|
||||
|
||||
ui.Say("Waiting for droplet to become active...")
|
||||
|
||||
err := waitForDropletState("active", dropletId, client, c.stateTimeout)
|
||||
err := waitForDropletState("active", dropletId, client, c.StateTimeout)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for droplet to become active: %s", err)
|
||||
state.Put("error", err)
|
||||
|
@ -26,16 +27,25 @@ func (s *stepDropletInfo) Run(state multistep.StateBag) multistep.StepAction {
|
|||
}
|
||||
|
||||
// Set the IP on the state for later
|
||||
ip, _, err := client.DropletStatus(dropletId)
|
||||
droplet, _, err := client.Droplets.Get(dropletId)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error retrieving droplet ID: %s", err)
|
||||
err := fmt.Errorf("Error retrieving droplet: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state.Put("droplet_ip", ip)
|
||||
// Verify we have an IPv4 address
|
||||
invalid := droplet.Networks == nil ||
|
||||
len(droplet.Networks.V4) == 0
|
||||
if invalid {
|
||||
err := fmt.Errorf("IPv4 address not found for droplet!")
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state.Put("droplet_ip", droplet.Networks.V4[0].IPAddress)
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,9 @@ package digitalocean
|
|||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/digitalocean/godo"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
@ -11,12 +13,12 @@ import (
|
|||
type stepPowerOff struct{}
|
||||
|
||||
func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(DigitalOceanClient)
|
||||
client := state.Get("client").(*godo.Client)
|
||||
c := state.Get("config").(Config)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
dropletId := state.Get("droplet_id").(uint)
|
||||
dropletId := state.Get("droplet_id").(int)
|
||||
|
||||
_, status, err := client.DropletStatus(dropletId)
|
||||
droplet, _, err := client.Droplets.Get(dropletId)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error checking droplet state: %s", err)
|
||||
state.Put("error", err)
|
||||
|
@ -24,14 +26,14 @@ func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
|
|||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if status == "off" {
|
||||
if droplet.Status == "off" {
|
||||
// Droplet is already off, don't do anything
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
// Pull the plug on the Droplet
|
||||
ui.Say("Forcefully shutting down Droplet...")
|
||||
err = client.PowerOffDroplet(dropletId)
|
||||
_, _, err = client.DropletActions.PowerOff(dropletId)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error powering off droplet: %s", err)
|
||||
state.Put("error", err)
|
||||
|
@ -40,13 +42,22 @@ func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
|
|||
}
|
||||
|
||||
log.Println("Waiting for poweroff event to complete...")
|
||||
err = waitForDropletState("off", dropletId, client, c.stateTimeout)
|
||||
err = waitForDropletState("off", dropletId, client, c.StateTimeout)
|
||||
if err != nil {
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Wait for the droplet to become unlocked for future steps
|
||||
if err := waitForDropletUnlocked(client, dropletId, 2*time.Minute); err != nil {
|
||||
// If we get an error the first time, actually report it
|
||||
err := fmt.Errorf("Error powering off droplet: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/digitalocean/godo"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
@ -12,16 +13,16 @@ import (
|
|||
type stepShutdown struct{}
|
||||
|
||||
func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(DigitalOceanClient)
|
||||
client := state.Get("client").(*godo.Client)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
dropletId := state.Get("droplet_id").(uint)
|
||||
dropletId := state.Get("droplet_id").(int)
|
||||
|
||||
// Gracefully power off the droplet. We have to retry this a number
|
||||
// of times because sometimes it says it completed when it actually
|
||||
// did absolutely nothing (*ALAKAZAM!* magic!). We give up after
|
||||
// a pretty arbitrary amount of time.
|
||||
ui.Say("Gracefully shutting down droplet...")
|
||||
err := client.ShutdownDroplet(dropletId)
|
||||
_, _, err := client.DropletActions.Shutdown(dropletId)
|
||||
if err != nil {
|
||||
// If we get an error the first time, actually report it
|
||||
err := fmt.Errorf("Error shutting down droplet: %s", err)
|
||||
|
@ -48,7 +49,7 @@ func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
|
|||
|
||||
for attempts := 2; attempts > 0; attempts++ {
|
||||
log.Printf("ShutdownDroplet attempt #%d...", attempts)
|
||||
err := client.ShutdownDroplet(dropletId)
|
||||
_, _, err := client.DropletActions.Shutdown(dropletId)
|
||||
if err != nil {
|
||||
log.Printf("Shutdown retry error: %s", err)
|
||||
}
|
||||
|
@ -64,7 +65,19 @@ func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
|
|||
|
||||
err = waitForDropletState("off", dropletId, client, 2*time.Minute)
|
||||
if err != nil {
|
||||
log.Printf("Error waiting for graceful off: %s", err)
|
||||
// If we get an error the first time, actually report it
|
||||
err := fmt.Errorf("Error shutting down droplet: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if err := waitForDropletUnlocked(client, dropletId, 2*time.Minute); err != nil {
|
||||
// If we get an error the first time, actually report it
|
||||
err := fmt.Errorf("Error shutting down droplet: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
|
|
|
@ -4,7 +4,9 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/digitalocean/godo"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
@ -12,13 +14,13 @@ import (
|
|||
type stepSnapshot struct{}
|
||||
|
||||
func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(DigitalOceanClient)
|
||||
client := state.Get("client").(*godo.Client)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
c := state.Get("config").(Config)
|
||||
dropletId := state.Get("droplet_id").(uint)
|
||||
dropletId := state.Get("droplet_id").(int)
|
||||
|
||||
ui.Say(fmt.Sprintf("Creating snapshot: %v", c.SnapshotName))
|
||||
err := client.CreateSnapshot(dropletId, c.SnapshotName)
|
||||
_, _, err := client.DropletActions.Snapshot(dropletId, c.SnapshotName)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating snapshot: %s", err)
|
||||
state.Put("error", err)
|
||||
|
@ -26,8 +28,20 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
|
|||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Wait for the droplet to become unlocked first. For snapshots
|
||||
// this can end up taking quite a long time, so we hardcode this to
|
||||
// 10 minutes.
|
||||
if err := waitForDropletUnlocked(client, dropletId, 10*time.Minute); err != nil {
|
||||
// If we get an error the first time, actually report it
|
||||
err := fmt.Errorf("Error shutting down droplet: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// With the pending state over, verify that we're in the active state
|
||||
ui.Say("Waiting for snapshot to complete...")
|
||||
err = waitForDropletState("active", dropletId, client, c.stateTimeout)
|
||||
err = waitForDropletState("active", dropletId, client, c.StateTimeout)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for snapshot to complete: %s", err)
|
||||
state.Put("error", err)
|
||||
|
@ -36,7 +50,7 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
|
|||
}
|
||||
|
||||
log.Printf("Looking up snapshot ID for snapshot: %s", c.SnapshotName)
|
||||
images, err := client.Images()
|
||||
images, _, err := client.Images.ListUser(&godo.ListOptions{PerPage: 200})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error looking up snapshot ID: %s", err)
|
||||
state.Put("error", err)
|
||||
|
@ -44,10 +58,10 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
|
|||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
var imageId uint
|
||||
var imageId int
|
||||
for _, image := range images {
|
||||
if image.Name == c.SnapshotName {
|
||||
imageId = image.Id
|
||||
imageId = image.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +74,6 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
|
|||
}
|
||||
|
||||
log.Printf("Snapshot image ID: %d", imageId)
|
||||
|
||||
state.Put("snapshot_image_id", imageId)
|
||||
state.Put("snapshot_name", c.SnapshotName)
|
||||
state.Put("region", c.Region)
|
||||
|
|
|
@ -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"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/digitalocean/godo"
|
||||
)
|
||||
|
||||
// waitForDropletUnlocked waits for the Droplet to be unlocked to
|
||||
// avoid "pending" errors when making state changes.
|
||||
func waitForDropletUnlocked(
|
||||
client *godo.Client, dropletId int, timeout time.Duration) error {
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
|
||||
result := make(chan error, 1)
|
||||
go func() {
|
||||
attempts := 0
|
||||
for {
|
||||
attempts += 1
|
||||
|
||||
log.Printf("[DEBUG] Checking droplet lock state... (attempt: %d)", attempts)
|
||||
droplet, _, err := client.Droplets.Get(dropletId)
|
||||
if err != nil {
|
||||
result <- err
|
||||
return
|
||||
}
|
||||
|
||||
if !droplet.Locked {
|
||||
result <- nil
|
||||
return
|
||||
}
|
||||
|
||||
// Wait 3 seconds in between
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
// Verify we shouldn't exit
|
||||
select {
|
||||
case <-done:
|
||||
// We finished, so just exit the goroutine
|
||||
return
|
||||
default:
|
||||
// Keep going
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
log.Printf("[DEBUG] Waiting for up to %d seconds for droplet to unlock", timeout/time.Second)
|
||||
select {
|
||||
case err := <-result:
|
||||
return err
|
||||
case <-time.After(timeout):
|
||||
return fmt.Errorf(
|
||||
"Timeout while waiting to for droplet to unlock")
|
||||
}
|
||||
}
|
||||
|
||||
// waitForState simply blocks until the droplet is in
|
||||
// a state we expect, while eventually timing out.
|
||||
func waitForDropletState(desiredState string, dropletId uint, client DigitalOceanClient, timeout time.Duration) error {
|
||||
func waitForDropletState(
|
||||
desiredState string, dropletId int,
|
||||
client *godo.Client, timeout time.Duration) error {
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
|
||||
|
@ -19,13 +72,13 @@ func waitForDropletState(desiredState string, dropletId uint, client DigitalOcea
|
|||
attempts += 1
|
||||
|
||||
log.Printf("Checking droplet status... (attempt: %d)", attempts)
|
||||
_, status, err := client.DropletStatus(dropletId)
|
||||
droplet, _, err := client.Droplets.Get(dropletId)
|
||||
if err != nil {
|
||||
result <- err
|
||||
return
|
||||
}
|
||||
|
||||
if status == desiredState {
|
||||
if droplet.Status == desiredState {
|
||||
result <- nil
|
||||
return
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
|
@ -42,7 +43,15 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
&StepTempDir{},
|
||||
&StepPull{},
|
||||
&StepRun{},
|
||||
&StepProvision{},
|
||||
&communicator.StepConnect{
|
||||
Config: &b.config.Comm,
|
||||
Host: commHost,
|
||||
SSHConfig: sshConfig(&b.config.Comm),
|
||||
CustomConnect: map[string]multistep.Step{
|
||||
"docker": &StepConnectDocker{},
|
||||
},
|
||||
},
|
||||
&common.StepProvision{},
|
||||
}
|
||||
|
||||
if b.config.Commit {
|
||||
|
|
|
@ -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/packer/common"
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
"github.com/mitchellh/packer/helper/config"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
|
@ -13,6 +14,7 @@ import (
|
|||
|
||||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
Comm communicator.Config `mapstructure:",squash"`
|
||||
|
||||
Commit bool
|
||||
ExportPath string `mapstructure:"export_path"`
|
||||
|
@ -31,10 +33,10 @@ type Config struct {
|
|||
}
|
||||
|
||||
func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
||||
var c Config
|
||||
c := new(Config)
|
||||
|
||||
var md mapstructure.Metadata
|
||||
err := config.Decode(&c, &config.DecodeOpts{
|
||||
err := config.Decode(c, &config.DecodeOpts{
|
||||
Metadata: &md,
|
||||
Interpolate: true,
|
||||
InterpolateFilter: &interpolate.RenderFilter{
|
||||
|
@ -69,7 +71,15 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
|||
c.Pull = true
|
||||
}
|
||||
|
||||
// Default to the normal Docker type
|
||||
if c.Comm.Type == "" {
|
||||
c.Comm.Type = "docker"
|
||||
}
|
||||
|
||||
var errs *packer.MultiError
|
||||
if es := c.Comm.Prepare(&c.ctx); len(es) > 0 {
|
||||
errs = packer.MultiErrorAppend(errs, es...)
|
||||
}
|
||||
if c.Image == "" {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("image must be specified"))
|
||||
|
@ -91,5 +101,5 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
|||
return nil, nil, errs
|
||||
}
|
||||
|
||||
return &c, nil, nil
|
||||
return c, nil, nil
|
||||
}
|
||||
|
|
|
@ -22,6 +22,10 @@ type Driver interface {
|
|||
// Import imports a container from a tar file
|
||||
Import(path, repo string) (string, error)
|
||||
|
||||
// IPAddress returns the address of the container that can be used
|
||||
// for external access.
|
||||
IPAddress(id string) (string, error)
|
||||
|
||||
// Login. This will lock the driver from performing another Login
|
||||
// until Logout is called. Therefore, any users MUST call Logout.
|
||||
Login(repo, email, username, password string) error
|
||||
|
|
|
@ -116,6 +116,23 @@ func (d *DockerDriver) Import(path string, repo string) (string, error) {
|
|||
return strings.TrimSpace(stdout.String()), nil
|
||||
}
|
||||
|
||||
func (d *DockerDriver) IPAddress(id string) (string, error) {
|
||||
var stderr, stdout bytes.Buffer
|
||||
cmd := exec.Command(
|
||||
"docker",
|
||||
"inspect",
|
||||
"--format",
|
||||
"{{ .NetworkSettings.IPAddress }}",
|
||||
id)
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "", fmt.Errorf("Error: %s\n\nStderr: %s", err, stderr.String())
|
||||
}
|
||||
|
||||
return strings.TrimSpace(stdout.String()), nil
|
||||
}
|
||||
|
||||
func (d *DockerDriver) Login(repo, email, user, pass string) error {
|
||||
d.l.Lock()
|
||||
|
||||
|
|
|
@ -23,6 +23,11 @@ type MockDriver struct {
|
|||
ImportId string
|
||||
ImportErr error
|
||||
|
||||
IPAddressCalled bool
|
||||
IPAddressID string
|
||||
IPAddressResult string
|
||||
IPAddressErr error
|
||||
|
||||
LoginCalled bool
|
||||
LoginEmail string
|
||||
LoginUsername string
|
||||
|
@ -104,6 +109,12 @@ func (d *MockDriver) Import(path, repo string) (string, error) {
|
|||
return d.ImportId, d.ImportErr
|
||||
}
|
||||
|
||||
func (d *MockDriver) IPAddress(id string) (string, error) {
|
||||
d.IPAddressCalled = true
|
||||
d.IPAddressID = id
|
||||
return d.IPAddressResult, d.IPAddressErr
|
||||
}
|
||||
|
||||
func (d *MockDriver) Login(r, e, u, p string) error {
|
||||
d.LoginCalled = true
|
||||
d.LoginRepo = r
|
||||
|
|
|
@ -2,12 +2,11 @@ package docker
|
|||
|
||||
import (
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common"
|
||||
)
|
||||
|
||||
type StepProvision struct{}
|
||||
type StepConnectDocker struct{}
|
||||
|
||||
func (s *StepProvision) Run(state multistep.StateBag) multistep.StepAction {
|
||||
func (s *StepConnectDocker) Run(state multistep.StateBag) multistep.StepAction {
|
||||
containerId := state.Get("container_id").(string)
|
||||
driver := state.Get("driver").(Driver)
|
||||
tempDir := state.Get("temp_dir").(string)
|
||||
|
@ -28,8 +27,8 @@ func (s *StepProvision) Run(state multistep.StateBag) multistep.StepAction {
|
|||
Version: version,
|
||||
}
|
||||
|
||||
prov := common.StepProvision{Comm: comm}
|
||||
return prov.Run(state)
|
||||
state.Put("communicator", comm)
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepProvision) Cleanup(state multistep.StateBag) {}
|
||||
func (s *StepConnectDocker) Cleanup(state multistep.StateBag) {}
|
|
@ -4,11 +4,12 @@ package googlecompute
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The unique ID for this builder.
|
||||
|
@ -60,10 +61,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
&StepInstanceInfo{
|
||||
Debug: b.config.PackerDebug,
|
||||
},
|
||||
&common.StepConnectSSH{
|
||||
SSHAddress: sshAddress,
|
||||
SSHConfig: sshConfig,
|
||||
SSHWaitTimeout: 5 * time.Minute,
|
||||
&communicator.StepConnect{
|
||||
Config: &b.config.Comm,
|
||||
Host: commHost,
|
||||
SSHConfig: sshConfig,
|
||||
},
|
||||
new(common.StepProvision),
|
||||
new(StepTeardownInstance),
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/common/uuid"
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
"github.com/mitchellh/packer/helper/config"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
|
@ -17,6 +18,7 @@ import (
|
|||
// state of the config object.
|
||||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
Comm communicator.Config `mapstructure:",squash"`
|
||||
|
||||
AccountFile string `mapstructure:"account_file"`
|
||||
ProjectId string `mapstructure:"project_id"`
|
||||
|
@ -31,23 +33,19 @@ type Config struct {
|
|||
Network string `mapstructure:"network"`
|
||||
SourceImage string `mapstructure:"source_image"`
|
||||
SourceImageProjectId string `mapstructure:"source_image_project_id"`
|
||||
SSHUsername string `mapstructure:"ssh_username"`
|
||||
SSHPort uint `mapstructure:"ssh_port"`
|
||||
RawSSHTimeout string `mapstructure:"ssh_timeout"`
|
||||
RawStateTimeout string `mapstructure:"state_timeout"`
|
||||
Tags []string `mapstructure:"tags"`
|
||||
Zone string `mapstructure:"zone"`
|
||||
|
||||
account accountFile
|
||||
privateKeyBytes []byte
|
||||
sshTimeout time.Duration
|
||||
stateTimeout time.Duration
|
||||
ctx *interpolate.Context
|
||||
}
|
||||
|
||||
func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
||||
c := new(Config)
|
||||
err := config.Decode(&c, &config.DecodeOpts{
|
||||
err := config.Decode(c, &config.DecodeOpts{
|
||||
Interpolate: true,
|
||||
InterpolateFilter: &interpolate.RenderFilter{
|
||||
Exclude: []string{
|
||||
|
@ -88,20 +86,12 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
|||
c.MachineType = "n1-standard-1"
|
||||
}
|
||||
|
||||
if c.RawSSHTimeout == "" {
|
||||
c.RawSSHTimeout = "5m"
|
||||
}
|
||||
|
||||
if c.RawStateTimeout == "" {
|
||||
c.RawStateTimeout = "5m"
|
||||
}
|
||||
|
||||
if c.SSHUsername == "" {
|
||||
c.SSHUsername = "root"
|
||||
}
|
||||
|
||||
if c.SSHPort == 0 {
|
||||
c.SSHPort = 22
|
||||
if c.Comm.SSHUsername == "" {
|
||||
c.Comm.SSHUsername = "root"
|
||||
}
|
||||
|
||||
var errs *packer.MultiError
|
||||
|
@ -122,14 +112,6 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
|||
errs, errors.New("a zone must be specified"))
|
||||
}
|
||||
|
||||
// Process timeout settings.
|
||||
sshTimeout, err := time.ParseDuration(c.RawSSHTimeout)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
|
||||
}
|
||||
c.sshTimeout = sshTimeout
|
||||
|
||||
stateTimeout, err := time.ParseDuration(c.RawStateTimeout)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
package googlecompute
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/ssh"
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// sshAddress returns the ssh address.
|
||||
func sshAddress(state multistep.StateBag) (string, error) {
|
||||
config := state.Get("config").(*Config)
|
||||
func commHost(state multistep.StateBag) (string, error) {
|
||||
ipAddress := state.Get("instance_ip").(string)
|
||||
return fmt.Sprintf("%s:%d", ipAddress, config.SSHPort), nil
|
||||
return ipAddress, nil
|
||||
}
|
||||
|
||||
// sshConfig returns the ssh configuration.
|
||||
|
@ -24,7 +23,7 @@ func sshConfig(state multistep.StateBag) (*ssh.ClientConfig, error) {
|
|||
}
|
||||
|
||||
return &ssh.ClientConfig{
|
||||
User: config.SSHUsername,
|
||||
User: config.Comm.SSHUsername,
|
||||
Auth: []ssh.AuthMethod{
|
||||
ssh.PublicKeys(signer),
|
||||
},
|
||||
|
|
|
@ -32,7 +32,7 @@ func (config *Config) getInstanceMetadata(sshPublicKey string) map[string]string
|
|||
|
||||
// Merge any existing ssh keys with our public key
|
||||
sshMetaKey := "sshKeys"
|
||||
sshKeys := fmt.Sprintf("%s:%s", config.SSHUsername, sshPublicKey)
|
||||
sshKeys := fmt.Sprintf("%s:%s", config.Comm.SSHUsername, sshPublicKey)
|
||||
if confSshKeys, exists := instanceMetadata[sshMetaKey]; exists {
|
||||
sshKeys = fmt.Sprintf("%s\n%s", sshKeys, confSshKeys)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
package null
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
const BuilderId = "fnoeding.null"
|
||||
|
@ -27,10 +28,13 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
|
||||
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
||||
steps := []multistep.Step{
|
||||
&common.StepConnectSSH{
|
||||
SSHAddress: SSHAddress(b.config.Host, b.config.Port),
|
||||
SSHConfig: SSHConfig(b.config.SSHUsername, b.config.SSHPassword, b.config.SSHPrivateKeyFile),
|
||||
SSHWaitTimeout: 1 * time.Minute,
|
||||
&communicator.StepConnect{
|
||||
Config: &b.config.CommConfig,
|
||||
Host: CommHost(b.config.CommConfig.SSHHost),
|
||||
SSHConfig: SSHConfig(
|
||||
b.config.CommConfig.SSHUsername,
|
||||
b.config.CommConfig.SSHPassword,
|
||||
b.config.CommConfig.SSHPrivateKey),
|
||||
},
|
||||
&common.StepProvision{},
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@ package null
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
"github.com/mitchellh/packer/helper/config"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
|
@ -11,49 +13,40 @@ import (
|
|||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
SSHUsername string `mapstructure:"ssh_username"`
|
||||
SSHPassword string `mapstructure:"ssh_password"`
|
||||
SSHPrivateKeyFile string `mapstructure:"ssh_private_key_file"`
|
||||
CommConfig communicator.Config `mapstructure:",squash"`
|
||||
}
|
||||
|
||||
func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
||||
c := new(Config)
|
||||
var c Config
|
||||
|
||||
err := config.Decode(&c, &config.DecodeOpts{
|
||||
Interpolate: true,
|
||||
InterpolateFilter: &interpolate.RenderFilter{
|
||||
Exclude: []string{
|
||||
"run_command",
|
||||
},
|
||||
},
|
||||
Interpolate: true,
|
||||
InterpolateFilter: &interpolate.RenderFilter{},
|
||||
}, raws...)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if c.Port == 0 {
|
||||
c.Port = 22
|
||||
}
|
||||
|
||||
var errs *packer.MultiError
|
||||
if c.Host == "" {
|
||||
if es := c.CommConfig.Prepare(nil); len(es) > 0 {
|
||||
errs = packer.MultiErrorAppend(errs, es...)
|
||||
}
|
||||
if c.CommConfig.SSHHost == "" {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("host must be specified"))
|
||||
}
|
||||
|
||||
if c.SSHUsername == "" {
|
||||
if c.CommConfig.SSHUsername == "" {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("ssh_username must be specified"))
|
||||
}
|
||||
|
||||
if c.SSHPassword == "" && c.SSHPrivateKeyFile == "" {
|
||||
if c.CommConfig.SSHPassword == "" && c.CommConfig.SSHPrivateKey == "" {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("one of ssh_password and ssh_private_key_file must be specified"))
|
||||
}
|
||||
|
||||
if c.SSHPassword != "" && c.SSHPrivateKeyFile != "" {
|
||||
if c.CommConfig.SSHPassword != "" && c.CommConfig.SSHPrivateKey != "" {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("only one of ssh_password and ssh_private_key_file must be specified"))
|
||||
}
|
||||
|
@ -62,5 +55,5 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
|||
return nil, nil, errs
|
||||
}
|
||||
|
||||
return c, nil, nil
|
||||
return &c, nil, nil
|
||||
}
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
package null
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
)
|
||||
|
||||
func testConfig() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"host": "foo",
|
||||
"ssh_host": "foo",
|
||||
"ssh_username": "bar",
|
||||
"ssh_password": "baz",
|
||||
}
|
||||
|
@ -48,8 +51,8 @@ func TestConfigPrepare_port(t *testing.T) {
|
|||
// default port should be 22
|
||||
delete(raw, "port")
|
||||
c, warns, errs := NewConfig(raw)
|
||||
if c.Port != 22 {
|
||||
t.Fatalf("bad: port should default to 22, not %d", c.Port)
|
||||
if c.CommConfig.SSHPort != 22 {
|
||||
t.Fatalf("bad: port should default to 22, not %d", c.CommConfig.SSHPort)
|
||||
}
|
||||
testConfigOk(t, warns, errs)
|
||||
}
|
||||
|
@ -58,12 +61,12 @@ func TestConfigPrepare_host(t *testing.T) {
|
|||
raw := testConfig()
|
||||
|
||||
// No host
|
||||
delete(raw, "host")
|
||||
delete(raw, "ssh_host")
|
||||
_, warns, errs := NewConfig(raw)
|
||||
testConfigErr(t, warns, errs)
|
||||
|
||||
// Good host
|
||||
raw["host"] = "good"
|
||||
raw["ssh_host"] = "good"
|
||||
_, warns, errs = NewConfig(raw)
|
||||
testConfigOk(t, warns, errs)
|
||||
}
|
||||
|
@ -97,7 +100,9 @@ func TestConfigPrepare_sshCredential(t *testing.T) {
|
|||
testConfigOk(t, warns, errs)
|
||||
|
||||
// only ssh_private_key_file
|
||||
raw["ssh_private_key_file"] = "good"
|
||||
testFile := communicator.TestPEM(t)
|
||||
defer os.Remove(testFile)
|
||||
raw["ssh_private_key_file"] = testFile
|
||||
delete(raw, "ssh_password")
|
||||
_, warns, errs = NewConfig(raw)
|
||||
testConfigOk(t, warns, errs)
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
package null
|
||||
|
||||
import (
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/communicator/ssh"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
// SSHAddress returns a function that can be given to the SSH communicator
|
||||
// for determining the SSH address
|
||||
func SSHAddress(host string, port int) func(multistep.StateBag) (string, error) {
|
||||
func CommHost(host string) func(multistep.StateBag) (string, error) {
|
||||
return func(state multistep.StateBag) (string, error) {
|
||||
return fmt.Sprintf("%s:%d", host, port), nil
|
||||
return host, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,99 +4,120 @@ import (
|
|||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"github.com/rackspace/gophercloud/openstack"
|
||||
)
|
||||
|
||||
// AccessConfig is for common configuration related to openstack access
|
||||
type AccessConfig struct {
|
||||
Username string `mapstructure:"username"`
|
||||
Password string `mapstructure:"password"`
|
||||
ApiKey string `mapstructure:"api_key"`
|
||||
Project string `mapstructure:"project"`
|
||||
Provider string `mapstructure:"provider"`
|
||||
RawRegion string `mapstructure:"region"`
|
||||
ProxyUrl string `mapstructure:"proxy_url"`
|
||||
TenantId string `mapstructure:"tenant_id"`
|
||||
Insecure bool `mapstructure:"insecure"`
|
||||
}
|
||||
Username string `mapstructure:"username"`
|
||||
UserID string `mapstructure:"user_id"`
|
||||
Password string `mapstructure:"password"`
|
||||
APIKey string `mapstructure:"api_key"`
|
||||
IdentityEndpoint string `mapstructure:"identity_endpoint"`
|
||||
TenantID string `mapstructure:"tenant_id"`
|
||||
TenantName string `mapstructure:"tenant_name"`
|
||||
DomainID string `mapstructure:"domain_id"`
|
||||
DomainName string `mapstructure:"domain_name"`
|
||||
Insecure bool `mapstructure:"insecure"`
|
||||
Region string `mapstructure:"region"`
|
||||
EndpointType string `mapstructure:"endpoint_type"`
|
||||
|
||||
// Auth returns a valid Auth object for access to openstack services, or
|
||||
// an error if the authentication couldn't be resolved.
|
||||
func (c *AccessConfig) Auth() (gophercloud.AccessProvider, error) {
|
||||
c.Username = common.ChooseString(c.Username, os.Getenv("SDK_USERNAME"), os.Getenv("OS_USERNAME"))
|
||||
c.Password = common.ChooseString(c.Password, os.Getenv("SDK_PASSWORD"), os.Getenv("OS_PASSWORD"))
|
||||
c.ApiKey = common.ChooseString(c.ApiKey, os.Getenv("SDK_API_KEY"))
|
||||
c.Project = common.ChooseString(c.Project, os.Getenv("SDK_PROJECT"), os.Getenv("OS_TENANT_NAME"))
|
||||
c.Provider = common.ChooseString(c.Provider, os.Getenv("SDK_PROVIDER"), os.Getenv("OS_AUTH_URL"))
|
||||
c.RawRegion = common.ChooseString(c.RawRegion, os.Getenv("SDK_REGION"), os.Getenv("OS_REGION_NAME"))
|
||||
c.TenantId = common.ChooseString(c.TenantId, os.Getenv("OS_TENANT_ID"))
|
||||
|
||||
// OpenStack's auto-generated openrc.sh files do not append the suffix
|
||||
// /tokens to the authentication URL. This ensures it is present when
|
||||
// specifying the URL.
|
||||
if strings.Contains(c.Provider, "://") && !strings.HasSuffix(c.Provider, "/tokens") {
|
||||
c.Provider += "/tokens"
|
||||
}
|
||||
|
||||
authoptions := gophercloud.AuthOptions{
|
||||
AllowReauth: true,
|
||||
|
||||
ApiKey: c.ApiKey,
|
||||
TenantId: c.TenantId,
|
||||
TenantName: c.Project,
|
||||
Username: c.Username,
|
||||
Password: c.Password,
|
||||
}
|
||||
|
||||
default_transport := &http.Transport{}
|
||||
|
||||
if c.Insecure {
|
||||
cfg := new(tls.Config)
|
||||
cfg.InsecureSkipVerify = true
|
||||
default_transport.TLSClientConfig = cfg
|
||||
}
|
||||
|
||||
// For corporate networks it may be the case where we want our API calls
|
||||
// to be sent through a separate HTTP proxy than external traffic.
|
||||
if c.ProxyUrl != "" {
|
||||
url, err := url.Parse(c.ProxyUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The gophercloud.Context has a UseCustomClient method which
|
||||
// would allow us to override with a new instance of http.Client.
|
||||
default_transport.Proxy = http.ProxyURL(url)
|
||||
}
|
||||
|
||||
if c.Insecure || c.ProxyUrl != "" {
|
||||
http.DefaultTransport = default_transport
|
||||
}
|
||||
|
||||
return gophercloud.Authenticate(c.Provider, authoptions)
|
||||
}
|
||||
|
||||
func (c *AccessConfig) Region() string {
|
||||
return common.ChooseString(c.RawRegion, os.Getenv("SDK_REGION"), os.Getenv("OS_REGION_NAME"))
|
||||
osClient *gophercloud.ProviderClient
|
||||
}
|
||||
|
||||
func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
|
||||
errs := make([]error, 0)
|
||||
if strings.HasPrefix(c.Provider, "rackspace") {
|
||||
if c.Region() == "" {
|
||||
errs = append(errs, fmt.Errorf("region must be specified when using rackspace"))
|
||||
if c.EndpointType != "internal" && c.EndpointType != "internalURL" &&
|
||||
c.EndpointType != "admin" && c.EndpointType != "adminURL" &&
|
||||
c.EndpointType != "public" && c.EndpointType != "publicURL" &&
|
||||
c.EndpointType != "" {
|
||||
return []error{fmt.Errorf("Invalid endpoint type provided")}
|
||||
}
|
||||
|
||||
if c.Region == "" {
|
||||
c.Region = os.Getenv("OS_REGION_NAME")
|
||||
}
|
||||
|
||||
// Legacy RackSpace stuff. We're keeping this around to keep things BC.
|
||||
if c.APIKey == "" {
|
||||
c.APIKey = os.Getenv("SDK_API_KEY")
|
||||
}
|
||||
if c.Password == "" {
|
||||
c.Password = os.Getenv("SDK_PASSWORD")
|
||||
}
|
||||
if c.Region == "" {
|
||||
c.Region = os.Getenv("SDK_REGION")
|
||||
}
|
||||
if c.TenantName == "" {
|
||||
c.TenantName = os.Getenv("SDK_PROJECT")
|
||||
}
|
||||
if c.Username == "" {
|
||||
c.Username = os.Getenv("SDK_USERNAME")
|
||||
}
|
||||
|
||||
// Get as much as possible from the end
|
||||
ao, _ := openstack.AuthOptionsFromEnv()
|
||||
|
||||
// Override values if we have them in our config
|
||||
overrides := []struct {
|
||||
From, To *string
|
||||
}{
|
||||
{&c.Username, &ao.Username},
|
||||
{&c.UserID, &ao.UserID},
|
||||
{&c.Password, &ao.Password},
|
||||
{&c.APIKey, &ao.APIKey},
|
||||
{&c.IdentityEndpoint, &ao.IdentityEndpoint},
|
||||
{&c.TenantID, &ao.TenantID},
|
||||
{&c.TenantName, &ao.TenantName},
|
||||
{&c.DomainID, &ao.DomainID},
|
||||
{&c.DomainName, &ao.DomainName},
|
||||
}
|
||||
for _, s := range overrides {
|
||||
if *s.From != "" {
|
||||
*s.To = *s.From
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errs
|
||||
// Build the client itself
|
||||
client, err := openstack.NewClient(ao.IdentityEndpoint)
|
||||
if err != nil {
|
||||
return []error{err}
|
||||
}
|
||||
|
||||
// If we have insecure set, then create a custom HTTP client that
|
||||
// ignores SSL errors.
|
||||
if c.Insecure {
|
||||
config := &tls.Config{InsecureSkipVerify: true}
|
||||
transport := &http.Transport{TLSClientConfig: config}
|
||||
client.HTTPClient.Transport = transport
|
||||
}
|
||||
|
||||
// Auth
|
||||
err = openstack.Authenticate(client, ao)
|
||||
if err != nil {
|
||||
return []error{err}
|
||||
}
|
||||
|
||||
c.osClient = client
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *AccessConfig) computeV2Client() (*gophercloud.ServiceClient, error) {
|
||||
return openstack.NewComputeV2(c.osClient, gophercloud.EndpointOpts{
|
||||
Region: c.Region,
|
||||
Availability: c.getEndpointType(),
|
||||
})
|
||||
}
|
||||
|
||||
func (c *AccessConfig) getEndpointType() gophercloud.Availability {
|
||||
if c.EndpointType == "internal" || c.EndpointType == "internalURL" {
|
||||
return gophercloud.AvailabilityInternal
|
||||
}
|
||||
if c.EndpointType == "admin" || c.EndpointType == "adminURL" {
|
||||
return gophercloud.AvailabilityAdmin
|
||||
}
|
||||
return gophercloud.AvailabilityPublic
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
"log"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"github.com/rackspace/gophercloud/openstack/compute/v2/images"
|
||||
)
|
||||
|
||||
// Artifact is an artifact implementation that contains built images.
|
||||
|
@ -16,7 +17,7 @@ type Artifact struct {
|
|||
BuilderIdValue string
|
||||
|
||||
// OpenStack connection for performing API stuff.
|
||||
Conn gophercloud.CloudServersProvider
|
||||
Client *gophercloud.ServiceClient
|
||||
}
|
||||
|
||||
func (a *Artifact) BuilderId() string {
|
||||
|
@ -42,5 +43,5 @@ func (a *Artifact) State(name string) interface{} {
|
|||
|
||||
func (a *Artifact) Destroy() error {
|
||||
log.Printf("Destroying image: %s", a.ImageId)
|
||||
return a.Conn.DeleteImageById(a.ImageId)
|
||||
return images.Delete(a.Client, a.ImageId).ExtractErr()
|
||||
}
|
||||
|
|
|
@ -5,11 +5,11 @@ package openstack
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"log"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
"github.com/mitchellh/packer/helper/config"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
|
@ -20,9 +20,10 @@ const BuilderId = "mitchellh.openstack"
|
|||
|
||||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
AccessConfig `mapstructure:",squash"`
|
||||
ImageConfig `mapstructure:",squash"`
|
||||
RunConfig `mapstructure:",squash"`
|
||||
|
||||
AccessConfig `mapstructure:",squash"`
|
||||
ImageConfig `mapstructure:",squash"`
|
||||
RunConfig `mapstructure:",squash"`
|
||||
|
||||
ctx interpolate.Context
|
||||
}
|
||||
|
@ -55,43 +56,35 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
}
|
||||
|
||||
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
||||
auth, err := b.config.AccessConfig.Auth()
|
||||
computeClient, err := b.config.computeV2Client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//fetches the api requisites from gophercloud for the appropriate
|
||||
//openstack variant
|
||||
api, err := gophercloud.PopulateApi(b.config.RunConfig.OpenstackProvider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
api.Region = b.config.AccessConfig.Region()
|
||||
|
||||
csp, err := gophercloud.ServersApi(auth, api)
|
||||
if err != nil {
|
||||
log.Printf("Region: %s", b.config.AccessConfig.Region())
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("Error initializing compute client: %s", err)
|
||||
}
|
||||
|
||||
// Setup the state bag and initial state for the steps
|
||||
state := new(multistep.BasicStateBag)
|
||||
state.Put("config", b.config)
|
||||
state.Put("csp", csp)
|
||||
state.Put("hook", hook)
|
||||
state.Put("ui", ui)
|
||||
|
||||
// Build the steps
|
||||
steps := []multistep.Step{
|
||||
&StepLoadExtensions{},
|
||||
&StepLoadFlavor{
|
||||
Flavor: b.config.Flavor,
|
||||
},
|
||||
&StepKeyPair{
|
||||
Debug: b.config.PackerDebug,
|
||||
DebugKeyPath: fmt.Sprintf("os_%s.pem", b.config.PackerBuildName),
|
||||
},
|
||||
&StepRunSourceServer{
|
||||
Name: b.config.ImageName,
|
||||
Flavor: b.config.Flavor,
|
||||
SourceImage: b.config.SourceImage,
|
||||
SecurityGroups: b.config.SecurityGroups,
|
||||
Networks: b.config.Networks,
|
||||
Name: b.config.ImageName,
|
||||
SourceImage: b.config.SourceImage,
|
||||
SecurityGroups: b.config.SecurityGroups,
|
||||
Networks: b.config.Networks,
|
||||
AvailabilityZone: b.config.AvailabilityZone,
|
||||
UserData: b.config.UserData,
|
||||
UserDataFile: b.config.UserDataFile,
|
||||
},
|
||||
&StepWaitForRackConnect{
|
||||
Wait: b.config.RackconnectWait,
|
||||
|
@ -100,12 +93,15 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
FloatingIpPool: b.config.FloatingIpPool,
|
||||
FloatingIp: b.config.FloatingIp,
|
||||
},
|
||||
&common.StepConnectSSH{
|
||||
SSHAddress: SSHAddress(csp, b.config.SSHInterface, b.config.SSHPort),
|
||||
SSHConfig: SSHConfig(b.config.SSHUsername),
|
||||
SSHWaitTimeout: b.config.SSHTimeout(),
|
||||
&communicator.StepConnect{
|
||||
Config: &b.config.RunConfig.Comm,
|
||||
Host: CommHost(
|
||||
computeClient,
|
||||
b.config.SSHInterface),
|
||||
SSHConfig: SSHConfig(b.config.RunConfig.Comm.SSHUsername),
|
||||
},
|
||||
&common.StepProvision{},
|
||||
&StepStopServer{},
|
||||
&stepCreateImage{},
|
||||
}
|
||||
|
||||
|
@ -135,7 +131,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
artifact := &Artifact{
|
||||
ImageId: state.Get("image").(string),
|
||||
BuilderIdValue: BuilderId,
|
||||
Conn: csp,
|
||||
Client: computeClient,
|
||||
}
|
||||
|
||||
return artifact, nil
|
||||
|
|
|
@ -9,7 +9,6 @@ func testConfig() map[string]interface{} {
|
|||
return map[string]interface{}{
|
||||
"username": "foo",
|
||||
"password": "bar",
|
||||
"provider": "foo",
|
||||
"region": "DFW",
|
||||
"image_name": "foo",
|
||||
"source_image": "foo",
|
||||
|
@ -40,55 +39,3 @@ func TestBuilder_Prepare_BadType(t *testing.T) {
|
|||
t.Fatalf("prepare should fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_ImageName(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test good
|
||||
config["image_name"] = "foo"
|
||||
warns, err := b.Prepare(config)
|
||||
if len(warns) > 0 {
|
||||
t.Fatalf("bad: %#v", warns)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
// Test bad
|
||||
config["image_name"] = "foo {{"
|
||||
b = Builder{}
|
||||
warns, err = b.Prepare(config)
|
||||
if len(warns) > 0 {
|
||||
t.Fatalf("bad: %#v", warns)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test bad
|
||||
delete(config, "image_name")
|
||||
b = Builder{}
|
||||
warns, err = b.Prepare(config)
|
||||
if len(warns) > 0 {
|
||||
t.Fatalf("bad: %#v", warns)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_InvalidKey(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Add a random key
|
||||
config["i_should_not_be_valid"] = true
|
||||
warns, err := b.Prepare(config)
|
||||
if len(warns) > 0 {
|
||||
t.Fatalf("bad: %#v", warns)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,45 +2,37 @@ package openstack
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
)
|
||||
|
||||
// RunConfig contains configuration for running an instance from a source
|
||||
// image and details on how to access that launched image.
|
||||
type RunConfig struct {
|
||||
SourceImage string `mapstructure:"source_image"`
|
||||
Flavor string `mapstructure:"flavor"`
|
||||
RawSSHTimeout string `mapstructure:"ssh_timeout"`
|
||||
SSHUsername string `mapstructure:"ssh_username"`
|
||||
SSHPort int `mapstructure:"ssh_port"`
|
||||
SSHInterface string `mapstructure:"ssh_interface"`
|
||||
OpenstackProvider string `mapstructure:"openstack_provider"`
|
||||
UseFloatingIp bool `mapstructure:"use_floating_ip"`
|
||||
RackconnectWait bool `mapstructure:"rackconnect_wait"`
|
||||
FloatingIpPool string `mapstructure:"floating_ip_pool"`
|
||||
FloatingIp string `mapstructure:"floating_ip"`
|
||||
SecurityGroups []string `mapstructure:"security_groups"`
|
||||
Networks []string `mapstructure:"networks"`
|
||||
Comm communicator.Config `mapstructure:",squash"`
|
||||
SSHInterface string `mapstructure:"ssh_interface"`
|
||||
|
||||
// Unexported fields that are calculated from others
|
||||
sshTimeout time.Duration
|
||||
SourceImage string `mapstructure:"source_image"`
|
||||
Flavor string `mapstructure:"flavor"`
|
||||
AvailabilityZone string `mapstructure:"availability_zone"`
|
||||
RackconnectWait bool `mapstructure:"rackconnect_wait"`
|
||||
FloatingIpPool string `mapstructure:"floating_ip_pool"`
|
||||
FloatingIp string `mapstructure:"floating_ip"`
|
||||
SecurityGroups []string `mapstructure:"security_groups"`
|
||||
Networks []string `mapstructure:"networks"`
|
||||
UserData string `mapstructure:"user_data"`
|
||||
UserDataFile string `mapstructure:"user_data_file"`
|
||||
|
||||
// Not really used, but here for BC
|
||||
OpenstackProvider string `mapstructure:"openstack_provider"`
|
||||
UseFloatingIp bool `mapstructure:"use_floating_ip"`
|
||||
}
|
||||
|
||||
func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
|
||||
// Defaults
|
||||
if c.SSHUsername == "" {
|
||||
c.SSHUsername = "root"
|
||||
}
|
||||
|
||||
if c.SSHPort == 0 {
|
||||
c.SSHPort = 22
|
||||
}
|
||||
|
||||
if c.RawSSHTimeout == "" {
|
||||
c.RawSSHTimeout = "5m"
|
||||
if c.Comm.SSHUsername == "" {
|
||||
c.Comm.SSHUsername = "root"
|
||||
}
|
||||
|
||||
if c.UseFloatingIp && c.FloatingIpPool == "" {
|
||||
|
@ -48,8 +40,7 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
|
|||
}
|
||||
|
||||
// Validation
|
||||
var err error
|
||||
errs := make([]error, 0)
|
||||
errs := c.Comm.Prepare(ctx)
|
||||
if c.SourceImage == "" {
|
||||
errs = append(errs, errors.New("A source_image must be specified"))
|
||||
}
|
||||
|
@ -58,18 +49,5 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
|
|||
errs = append(errs, errors.New("A flavor must be specified"))
|
||||
}
|
||||
|
||||
if c.SSHUsername == "" {
|
||||
errs = append(errs, errors.New("An ssh_username must be specified"))
|
||||
}
|
||||
|
||||
c.sshTimeout, err = time.ParseDuration(c.RawSSHTimeout)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (c *RunConfig) SSHTimeout() time.Duration {
|
||||
return c.sshTimeout
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ package openstack
|
|||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -17,7 +19,10 @@ func testRunConfig() *RunConfig {
|
|||
return &RunConfig{
|
||||
SourceImage: "abcd",
|
||||
Flavor: "m1.small",
|
||||
SSHUsername: "root",
|
||||
|
||||
Comm: communicator.Config{
|
||||
SSHUsername: "foo",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,41 +52,28 @@ func TestRunConfigPrepare_SourceImage(t *testing.T) {
|
|||
|
||||
func TestRunConfigPrepare_SSHPort(t *testing.T) {
|
||||
c := testRunConfig()
|
||||
c.SSHPort = 0
|
||||
c.Comm.SSHPort = 0
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if c.SSHPort != 22 {
|
||||
t.Fatalf("invalid value: %d", c.SSHPort)
|
||||
if c.Comm.SSHPort != 22 {
|
||||
t.Fatalf("invalid value: %d", c.Comm.SSHPort)
|
||||
}
|
||||
|
||||
c.SSHPort = 44
|
||||
c.Comm.SSHPort = 44
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if c.SSHPort != 44 {
|
||||
t.Fatalf("invalid value: %d", c.SSHPort)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigPrepare_SSHTimeout(t *testing.T) {
|
||||
c := testRunConfig()
|
||||
c.RawSSHTimeout = ""
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
c.RawSSHTimeout = "bad"
|
||||
if err := c.Prepare(nil); len(err) != 1 {
|
||||
t.Fatalf("err: %s", err)
|
||||
if c.Comm.SSHPort != 44 {
|
||||
t.Fatalf("invalid value: %d", c.Comm.SSHPort)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigPrepare_SSHUsername(t *testing.T) {
|
||||
c := testRunConfig()
|
||||
c.SSHUsername = ""
|
||||
c.Comm.SSHUsername = ""
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
|
|
@ -3,12 +3,12 @@ package openstack
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/racker/perigee"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
|
||||
)
|
||||
|
||||
// StateRefreshFunc is a function type used for StateChangeConf that is
|
||||
|
@ -28,26 +28,27 @@ type StateChangeConf struct {
|
|||
Pending []string
|
||||
Refresh StateRefreshFunc
|
||||
StepState multistep.StateBag
|
||||
Target string
|
||||
Target []string
|
||||
}
|
||||
|
||||
// ServerStateRefreshFunc returns a StateRefreshFunc that is used to watch
|
||||
// an openstack server.
|
||||
func ServerStateRefreshFunc(csp gophercloud.CloudServersProvider, s *gophercloud.Server) StateRefreshFunc {
|
||||
func ServerStateRefreshFunc(
|
||||
client *gophercloud.ServiceClient, s *servers.Server) StateRefreshFunc {
|
||||
return func() (interface{}, string, int, error) {
|
||||
resp, err := csp.ServerById(s.Id)
|
||||
serverNew, err := servers.Get(client, s.ID).Extract()
|
||||
if err != nil {
|
||||
urce, ok := err.(*perigee.UnexpectedResponseCodeError)
|
||||
if ok && (urce.Actual == 404) {
|
||||
log.Printf("404 on ServerStateRefresh, returning DELETED")
|
||||
|
||||
errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError)
|
||||
if ok && errCode.Actual == 404 {
|
||||
log.Printf("[INFO] 404 on ServerStateRefresh, returning DELETED")
|
||||
return nil, "DELETED", 0, nil
|
||||
} else {
|
||||
log.Printf("Error on ServerStateRefresh: %s", err)
|
||||
log.Printf("[ERROR] Error on ServerStateRefresh: %s", err)
|
||||
return nil, "", 0, err
|
||||
}
|
||||
}
|
||||
return resp, resp.Status, resp.Progress, nil
|
||||
|
||||
return serverNew, serverNew.Status, serverNew.Progress, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,8 +65,10 @@ func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
if currentState == conf.Target {
|
||||
return
|
||||
for _, t := range conf.Target {
|
||||
if currentState == t {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if conf.StepState != nil {
|
||||
|
|
|
@ -1,51 +1,53 @@
|
|||
package openstack
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/ssh"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip"
|
||||
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// SSHAddress returns a function that can be given to the SSH communicator
|
||||
// for determining the SSH address based on the server AccessIPv4 setting..
|
||||
func SSHAddress(csp gophercloud.CloudServersProvider, sshinterface string, port int) func(multistep.StateBag) (string, error) {
|
||||
// CommHost looks up the host for the communicator.
|
||||
func CommHost(
|
||||
client *gophercloud.ServiceClient,
|
||||
sshinterface string) func(multistep.StateBag) (string, error) {
|
||||
return func(state multistep.StateBag) (string, error) {
|
||||
s := state.Get("server").(*gophercloud.Server)
|
||||
s := state.Get("server").(*servers.Server)
|
||||
|
||||
if ip := state.Get("access_ip").(gophercloud.FloatingIp); ip.Ip != "" {
|
||||
return fmt.Sprintf("%s:%d", ip.Ip, port), nil
|
||||
}
|
||||
|
||||
ip_pools, err := s.AllAddressPools()
|
||||
if err != nil {
|
||||
return "", errors.New("Error parsing SSH addresses")
|
||||
}
|
||||
for pool, addresses := range ip_pools {
|
||||
if sshinterface != "" {
|
||||
if pool != sshinterface {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if pool != "" {
|
||||
for _, address := range addresses {
|
||||
if address.Addr != "" && address.Version == 4 {
|
||||
return fmt.Sprintf("%s:%d", address.Addr, port), nil
|
||||
}
|
||||
}
|
||||
// If we have a specific interface, try that
|
||||
if sshinterface != "" {
|
||||
if addr := sshAddrFromPool(s, sshinterface); addr != "" {
|
||||
return addr, nil
|
||||
}
|
||||
}
|
||||
|
||||
serverState, err := csp.ServerById(s.Id)
|
||||
// If we have a floating IP, use that
|
||||
ip := state.Get("access_ip").(*floatingip.FloatingIP)
|
||||
if ip != nil && ip.IP != "" {
|
||||
return ip.IP, nil
|
||||
}
|
||||
|
||||
if s.AccessIPv4 != "" {
|
||||
return s.AccessIPv4, nil
|
||||
}
|
||||
|
||||
// Try to get it from the requested interface
|
||||
if addr := sshAddrFromPool(s, sshinterface); addr != "" {
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
s, err := servers.Get(client, s.ID).Extract()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
state.Put("server", serverState)
|
||||
state.Put("server", s)
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
return "", errors.New("couldn't determine IP address for server")
|
||||
|
@ -72,3 +74,42 @@ func SSHConfig(username string) func(multistep.StateBag) (*ssh.ClientConfig, err
|
|||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func sshAddrFromPool(s *servers.Server, desired string) string {
|
||||
// Get all the addresses associated with this server. This
|
||||
// was taken directly from Terraform.
|
||||
for pool, networkAddresses := range s.Addresses {
|
||||
// If we have an SSH interface specified, skip it if no match
|
||||
if desired != "" && pool != desired {
|
||||
log.Printf(
|
||||
"[INFO] Skipping pool %s, doesn't match requested %s",
|
||||
pool, desired)
|
||||
continue
|
||||
}
|
||||
|
||||
elements, ok := networkAddresses.([]interface{})
|
||||
if !ok {
|
||||
log.Printf(
|
||||
"[ERROR] Unknown return type for address field: %#v",
|
||||
networkAddresses)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, element := range elements {
|
||||
var addr string
|
||||
address := element.(map[string]interface{})
|
||||
if address["OS-EXT-IPS:type"] == "floating" {
|
||||
addr = address["addr"].(string)
|
||||
} else {
|
||||
if address["version"].(float64) == 4 {
|
||||
addr = address["addr"].(string)
|
||||
}
|
||||
}
|
||||
if addr != "" {
|
||||
return addr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
|
|
@ -2,10 +2,11 @@ package openstack
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip"
|
||||
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
|
||||
)
|
||||
|
||||
type StepAllocateIp struct {
|
||||
|
@ -15,53 +16,83 @@ type StepAllocateIp struct {
|
|||
|
||||
func (s *StepAllocateIp) Run(state multistep.StateBag) multistep.StepAction {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
csp := state.Get("csp").(gophercloud.CloudServersProvider)
|
||||
server := state.Get("server").(*gophercloud.Server)
|
||||
config := state.Get("config").(Config)
|
||||
server := state.Get("server").(*servers.Server)
|
||||
|
||||
// We need the v2 compute client
|
||||
client, err := config.computeV2Client()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error initializing compute client: %s", err)
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
var instanceIp floatingip.FloatingIP
|
||||
|
||||
var instanceIp gophercloud.FloatingIp
|
||||
// This is here in case we error out before putting instanceIp into the
|
||||
// statebag below, because it is requested by Cleanup()
|
||||
state.Put("access_ip", instanceIp)
|
||||
state.Put("access_ip", &instanceIp)
|
||||
|
||||
if s.FloatingIp != "" {
|
||||
instanceIp.Ip = s.FloatingIp
|
||||
instanceIp.IP = s.FloatingIp
|
||||
} else if s.FloatingIpPool != "" {
|
||||
newIp, err := csp.CreateFloatingIp(s.FloatingIpPool)
|
||||
ui.Say(fmt.Sprintf("Creating floating IP..."))
|
||||
ui.Message(fmt.Sprintf("Pool: %s", s.FloatingIpPool))
|
||||
newIp, err := floatingip.Create(client, floatingip.CreateOpts{
|
||||
Pool: s.FloatingIpPool,
|
||||
}).Extract()
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating floating ip from pool '%s'", s.FloatingIpPool)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
instanceIp = newIp
|
||||
ui.Say(fmt.Sprintf("Created temporary floating IP %s...", instanceIp.Ip))
|
||||
|
||||
instanceIp = *newIp
|
||||
ui.Message(fmt.Sprintf("Created floating IP: %s", instanceIp.IP))
|
||||
}
|
||||
|
||||
if instanceIp.Ip != "" {
|
||||
if err := csp.AssociateFloatingIp(server.Id, instanceIp); err != nil {
|
||||
err := fmt.Errorf("Error associating floating IP %s with instance.", instanceIp.Ip)
|
||||
if instanceIp.IP != "" {
|
||||
ui.Say(fmt.Sprintf("Associating floating IP with server..."))
|
||||
ui.Message(fmt.Sprintf("IP: %s", instanceIp.IP))
|
||||
err := floatingip.Associate(client, server.ID, instanceIp.IP).ExtractErr()
|
||||
if err != nil {
|
||||
err := fmt.Errorf(
|
||||
"Error associating floating IP %s with instance: %s",
|
||||
instanceIp.IP, err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
} else {
|
||||
ui.Say(fmt.Sprintf("Added floating IP %s to instance...", instanceIp.Ip))
|
||||
}
|
||||
|
||||
ui.Message(fmt.Sprintf(
|
||||
"Added floating IP %s to instance!", instanceIp.IP))
|
||||
}
|
||||
|
||||
state.Put("access_ip", instanceIp)
|
||||
|
||||
state.Put("access_ip", &instanceIp)
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepAllocateIp) Cleanup(state multistep.StateBag) {
|
||||
config := state.Get("config").(Config)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
csp := state.Get("csp").(gophercloud.CloudServersProvider)
|
||||
instanceIp := state.Get("access_ip").(gophercloud.FloatingIp)
|
||||
if s.FloatingIpPool != "" && instanceIp.Id != 0 {
|
||||
if err := csp.DeleteFloatingIp(instanceIp); err != nil {
|
||||
ui.Error(fmt.Sprintf("Error deleting temporary floating IP %s", instanceIp.Ip))
|
||||
instanceIp := state.Get("access_ip").(*floatingip.FloatingIP)
|
||||
|
||||
// We need the v2 compute client
|
||||
client, err := config.computeV2Client()
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf(
|
||||
"Error deleting temporary floating IP %s", instanceIp.IP))
|
||||
return
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
ui.Say(fmt.Sprintf("Deleted temporary floating IP %s", instanceIp.IP))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,28 +2,36 @@ package openstack
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"github.com/rackspace/gophercloud/openstack/compute/v2/images"
|
||||
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
|
||||
)
|
||||
|
||||
type stepCreateImage struct{}
|
||||
|
||||
func (s *stepCreateImage) Run(state multistep.StateBag) multistep.StepAction {
|
||||
csp := state.Get("csp").(gophercloud.CloudServersProvider)
|
||||
config := state.Get("config").(Config)
|
||||
server := state.Get("server").(*gophercloud.Server)
|
||||
server := state.Get("server").(*servers.Server)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
// We need the v2 compute client
|
||||
client, err := config.computeV2Client()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error initializing compute client: %s", err)
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Create the image
|
||||
ui.Say(fmt.Sprintf("Creating the image: %s", config.ImageName))
|
||||
createOpts := gophercloud.CreateImage{
|
||||
imageId, err := servers.CreateImage(client, server.ID, servers.CreateImageOpts{
|
||||
Name: config.ImageName,
|
||||
}
|
||||
imageId, err := csp.CreateImage(server.Id, createOpts)
|
||||
}).ExtractImageID()
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating image: %s", err)
|
||||
state.Put("error", err)
|
||||
|
@ -32,12 +40,12 @@ func (s *stepCreateImage) Run(state multistep.StateBag) multistep.StepAction {
|
|||
}
|
||||
|
||||
// Set the Image ID in the state
|
||||
ui.Say(fmt.Sprintf("Image: %s", imageId))
|
||||
ui.Message(fmt.Sprintf("Image: %s", imageId))
|
||||
state.Put("image", imageId)
|
||||
|
||||
// Wait for the image to become ready
|
||||
ui.Say("Waiting for image to become ready...")
|
||||
if err := WaitForImage(csp, imageId); err != nil {
|
||||
if err := WaitForImage(client, imageId); err != nil {
|
||||
err := fmt.Errorf("Error waiting for image: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
|
@ -52,10 +60,17 @@ func (s *stepCreateImage) Cleanup(multistep.StateBag) {
|
|||
}
|
||||
|
||||
// WaitForImage waits for the given Image ID to become ready.
|
||||
func WaitForImage(csp gophercloud.CloudServersProvider, imageId string) error {
|
||||
func WaitForImage(client *gophercloud.ServiceClient, imageId string) error {
|
||||
for {
|
||||
image, err := csp.ImageById(imageId)
|
||||
image, err := images.Get(client, imageId).Extract()
|
||||
if err != nil {
|
||||
errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError)
|
||||
if ok && errCode.Actual == 500 {
|
||||
log.Printf("[ERROR] 500 error received, will ignore and retry: %s", err)
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -2,14 +2,13 @@ package openstack
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common/uuid"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common/uuid"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
|
||||
)
|
||||
|
||||
type StepKeyPair struct {
|
||||
|
@ -19,18 +18,28 @@ type StepKeyPair struct {
|
|||
}
|
||||
|
||||
func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction {
|
||||
csp := state.Get("csp").(gophercloud.CloudServersProvider)
|
||||
config := state.Get("config").(Config)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
// We need the v2 compute client
|
||||
computeClient, err := config.computeV2Client()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error initializing compute client: %s", err)
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
ui.Say("Creating temporary keypair for this instance...")
|
||||
keyName := fmt.Sprintf("packer %s", uuid.TimeOrderedUUID())
|
||||
log.Printf("temporary keypair name: %s", keyName)
|
||||
keyResp, err := csp.CreateKeyPair(gophercloud.NewKeyPair{Name: keyName})
|
||||
keypair, err := keypairs.Create(computeClient, keypairs.CreateOpts{
|
||||
Name: keyName,
|
||||
}).Extract()
|
||||
if err != nil {
|
||||
state.Put("error", fmt.Errorf("Error creating temporary keypair: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
if keyResp.PrivateKey == "" {
|
||||
|
||||
if keypair.PrivateKey == "" {
|
||||
state.Put("error", fmt.Errorf("The temporary keypair returned was blank"))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
@ -47,7 +56,7 @@ func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction {
|
|||
defer f.Close()
|
||||
|
||||
// Write the key out
|
||||
if _, err := f.Write([]byte(keyResp.PrivateKey)); err != nil {
|
||||
if _, err := f.Write([]byte(keypair.PrivateKey)); err != nil {
|
||||
state.Put("error", fmt.Errorf("Error saving debug key: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
@ -66,7 +75,7 @@ func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction {
|
|||
|
||||
// Set some state data for use in future steps
|
||||
state.Put("keyPair", keyName)
|
||||
state.Put("privateKey", keyResp.PrivateKey)
|
||||
state.Put("privateKey", keypair.PrivateKey)
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
@ -77,11 +86,19 @@ func (s *StepKeyPair) Cleanup(state multistep.StateBag) {
|
|||
return
|
||||
}
|
||||
|
||||
csp := state.Get("csp").(gophercloud.CloudServersProvider)
|
||||
config := state.Get("config").(Config)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
// We need the v2 compute client
|
||||
computeClient, err := config.computeV2Client()
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf(
|
||||
"Error cleaning up keypair. Please delete the key manually: %s", s.keyName))
|
||||
return
|
||||
}
|
||||
|
||||
ui.Say("Deleting temporary keypair...")
|
||||
err := csp.DeleteKeyPair(s.keyName)
|
||||
err = keypairs.Delete(computeClient, s.keyName).ExtractErr()
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf(
|
||||
"Error cleaning up keypair. Please delete the key manually: %s", s.keyName))
|
||||
|
|
|
@ -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 (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
|
||||
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
|
||||
)
|
||||
|
||||
type StepRunSourceServer struct {
|
||||
Flavor string
|
||||
Name string
|
||||
SourceImage string
|
||||
SecurityGroups []string
|
||||
Networks []string
|
||||
Name string
|
||||
SourceImage string
|
||||
SecurityGroups []string
|
||||
Networks []string
|
||||
AvailabilityZone string
|
||||
UserData string
|
||||
UserDataFile string
|
||||
|
||||
server *gophercloud.Server
|
||||
server *servers.Server
|
||||
}
|
||||
|
||||
func (s *StepRunSourceServer) Run(state multistep.StateBag) multistep.StepAction {
|
||||
csp := state.Get("csp").(gophercloud.CloudServersProvider)
|
||||
config := state.Get("config").(Config)
|
||||
flavor := state.Get("flavor_id").(string)
|
||||
keyName := state.Get("keyPair").(string)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
// XXX - validate image and flavor is available
|
||||
|
||||
securityGroups := make([]map[string]interface{}, len(s.SecurityGroups))
|
||||
for i, groupName := range s.SecurityGroups {
|
||||
securityGroups[i] = make(map[string]interface{})
|
||||
securityGroups[i]["name"] = groupName
|
||||
// We need the v2 compute client
|
||||
computeClient, err := config.computeV2Client()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error initializing compute client: %s", err)
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
networks := make([]gophercloud.NetworkConfig, len(s.Networks))
|
||||
networks := make([]servers.Network, len(s.Networks))
|
||||
for i, networkUuid := range s.Networks {
|
||||
networks[i].Uuid = networkUuid
|
||||
networks[i].UUID = networkUuid
|
||||
}
|
||||
|
||||
server := gophercloud.NewServer{
|
||||
Name: s.Name,
|
||||
ImageRef: s.SourceImage,
|
||||
FlavorRef: s.Flavor,
|
||||
KeyPairName: keyName,
|
||||
SecurityGroup: securityGroups,
|
||||
Networks: networks,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
serverResp, err := csp.CreateServer(server)
|
||||
ui.Say("Launching server...")
|
||||
s.server, err = servers.Create(computeClient, keypairs.CreateOptsExt{
|
||||
CreateOptsBuilder: servers.CreateOpts{
|
||||
Name: s.Name,
|
||||
ImageRef: s.SourceImage,
|
||||
FlavorRef: flavor,
|
||||
SecurityGroups: s.SecurityGroups,
|
||||
Networks: networks,
|
||||
AvailabilityZone: s.AvailabilityZone,
|
||||
UserData: userData,
|
||||
},
|
||||
|
||||
KeyName: keyName,
|
||||
}).Extract()
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error launching source server: %s", err)
|
||||
state.Put("error", err)
|
||||
|
@ -54,25 +73,25 @@ func (s *StepRunSourceServer) Run(state multistep.StateBag) multistep.StepAction
|
|||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
s.server, err = csp.ServerById(serverResp.Id)
|
||||
log.Printf("server id: %s", s.server.Id)
|
||||
ui.Message(fmt.Sprintf("Server ID: %s", s.server.ID))
|
||||
log.Printf("server id: %s", s.server.ID)
|
||||
|
||||
ui.Say(fmt.Sprintf("Waiting for server (%s) to become ready...", s.server.Id))
|
||||
ui.Say("Waiting for server to become ready...")
|
||||
stateChange := StateChangeConf{
|
||||
Pending: []string{"BUILD"},
|
||||
Target: "ACTIVE",
|
||||
Refresh: ServerStateRefreshFunc(csp, s.server),
|
||||
Target: []string{"ACTIVE"},
|
||||
Refresh: ServerStateRefreshFunc(computeClient, s.server),
|
||||
StepState: state,
|
||||
}
|
||||
latestServer, err := WaitForState(&stateChange)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for server (%s) to become ready: %s", s.server.Id, err)
|
||||
err := fmt.Errorf("Error waiting for server (%s) to become ready: %s", s.server.ID, err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
s.server = latestServer.(*gophercloud.Server)
|
||||
s.server = latestServer.(*servers.Server)
|
||||
state.Put("server", s.server)
|
||||
|
||||
return multistep.ActionContinue
|
||||
|
@ -83,19 +102,26 @@ func (s *StepRunSourceServer) Cleanup(state multistep.StateBag) {
|
|||
return
|
||||
}
|
||||
|
||||
csp := state.Get("csp").(gophercloud.CloudServersProvider)
|
||||
config := state.Get("config").(Config)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
// We need the v2 compute client
|
||||
computeClient, err := config.computeV2Client()
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf("Error terminating server, may still be around: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
ui.Say("Terminating the source server...")
|
||||
if err := csp.DeleteServerById(s.server.Id); err != nil {
|
||||
if err := servers.Delete(computeClient, s.server.ID).ExtractErr(); err != nil {
|
||||
ui.Error(fmt.Sprintf("Error terminating server, may still be around: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
stateChange := StateChangeConf{
|
||||
Pending: []string{"ACTIVE", "BUILD", "REBUILD", "SUSPENDED"},
|
||||
Refresh: ServerStateRefreshFunc(csp, s.server),
|
||||
Target: "DELETED",
|
||||
Pending: []string{"ACTIVE", "BUILD", "REBUILD", "SUSPENDED", "SHUTOFF", "STOPPED"},
|
||||
Refresh: ServerStateRefreshFunc(computeClient, s.server),
|
||||
Target: []string{"DELETED"},
|
||||
}
|
||||
|
||||
WaitForState(&stateChange)
|
||||
|
|
|
@ -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 (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
|
||||
)
|
||||
|
||||
type StepWaitForRackConnect struct {
|
||||
|
@ -18,14 +18,22 @@ func (s *StepWaitForRackConnect) Run(state multistep.StateBag) multistep.StepAct
|
|||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
csp := state.Get("csp").(gophercloud.CloudServersProvider)
|
||||
server := state.Get("server").(*gophercloud.Server)
|
||||
config := state.Get("config").(Config)
|
||||
server := state.Get("server").(*servers.Server)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say(fmt.Sprintf("Waiting for server (%s) to become RackConnect ready...", server.Id))
|
||||
// We need the v2 compute client
|
||||
computeClient, err := config.computeV2Client()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error initializing compute client: %s", err)
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
ui.Say(fmt.Sprintf(
|
||||
"Waiting for server (%s) to become RackConnect ready...", server.ID))
|
||||
for {
|
||||
server, err := csp.ServerById(server.Id)
|
||||
server, err = servers.Get(computeClient, server.ID).Extract()
|
||||
if err != nil {
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
|
|
@ -73,6 +73,12 @@ func NewDriver() (Driver, error) {
|
|||
log.Printf("prlctl path: %s", prlctlPath)
|
||||
|
||||
drivers = map[string]Driver{
|
||||
"11": &Parallels10Driver{
|
||||
Parallels9Driver: Parallels9Driver{
|
||||
PrlctlPath: prlctlPath,
|
||||
dhcp_lease_file: dhcp_lease_file,
|
||||
},
|
||||
},
|
||||
"10": &Parallels10Driver{
|
||||
Parallels9Driver: Parallels9Driver{
|
||||
PrlctlPath: prlctlPath,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package common
|
||||
|
||||
// Parallels10Driver are inherited from Parallels9Driver.
|
||||
// Used for Parallels v 10 & 11
|
||||
type Parallels10Driver struct {
|
||||
Parallels9Driver
|
||||
}
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
"github.com/mitchellh/multistep"
|
||||
commonssh "github.com/mitchellh/packer/common/ssh"
|
||||
packerssh "github.com/mitchellh/packer/communicator/ssh"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func SSHAddress(state multistep.StateBag) (string, error) {
|
||||
func CommHost(state multistep.StateBag) (string, error) {
|
||||
vmName := state.Get("vmName").(string)
|
||||
driver := state.Get("driver").(Driver)
|
||||
|
||||
|
@ -23,19 +21,19 @@ func SSHAddress(state multistep.StateBag) (string, error) {
|
|||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s:22", ip), nil
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*ssh.ClientConfig, error) {
|
||||
return func(state multistep.StateBag) (*ssh.ClientConfig, error) {
|
||||
auth := []ssh.AuthMethod{
|
||||
ssh.Password(config.SSHPassword),
|
||||
ssh.Password(config.Comm.SSHPassword),
|
||||
ssh.KeyboardInteractive(
|
||||
packerssh.PasswordKeyboardInteractive(config.SSHPassword)),
|
||||
packerssh.PasswordKeyboardInteractive(config.Comm.SSHPassword)),
|
||||
}
|
||||
|
||||
if config.SSHKeyPath != "" {
|
||||
signer, err := commonssh.FileSigner(config.SSHKeyPath)
|
||||
signer, err := commonssh.FileSigner(config.Comm.SSHPrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -44,7 +42,7 @@ func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*ssh.ClientConfig
|
|||
}
|
||||
|
||||
return &ssh.ClientConfig{
|
||||
User: config.SSHUser,
|
||||
User: config.Comm.SSHUsername,
|
||||
Auth: auth,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -1,52 +1,29 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
commonssh "github.com/mitchellh/packer/common/ssh"
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
)
|
||||
|
||||
type SSHConfig struct {
|
||||
SSHKeyPath string `mapstructure:"ssh_key_path"`
|
||||
SSHPassword string `mapstructure:"ssh_password"`
|
||||
SSHPort uint `mapstructure:"ssh_port"`
|
||||
SSHUser string `mapstructure:"ssh_username"`
|
||||
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
|
||||
Comm communicator.Config `mapstructure:",squash"`
|
||||
|
||||
SSHWaitTimeout time.Duration
|
||||
// These are deprecated, but we keep them around for BC
|
||||
// TODO(@mitchellh): remove
|
||||
SSHKeyPath string `mapstructure:"ssh_key_path"`
|
||||
SSHWaitTimeout time.Duration `mapstructure:"ssh_wait_timeout"`
|
||||
}
|
||||
|
||||
func (c *SSHConfig) Prepare(ctx *interpolate.Context) []error {
|
||||
if c.SSHPort == 0 {
|
||||
c.SSHPort = 22
|
||||
}
|
||||
|
||||
if c.RawSSHWaitTimeout == "" {
|
||||
c.RawSSHWaitTimeout = "20m"
|
||||
}
|
||||
|
||||
var errs []error
|
||||
// TODO: backwards compatibility, write fixer instead
|
||||
if c.SSHKeyPath != "" {
|
||||
if _, err := os.Stat(c.SSHKeyPath); err != nil {
|
||||
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
|
||||
} else if _, err := commonssh.FileSigner(c.SSHKeyPath); err != nil {
|
||||
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
|
||||
}
|
||||
c.Comm.SSHPrivateKey = c.SSHKeyPath
|
||||
}
|
||||
if c.SSHWaitTimeout != 0 {
|
||||
c.Comm.SSHTimeout = c.SSHWaitTimeout
|
||||
}
|
||||
|
||||
if c.SSHUser == "" {
|
||||
errs = append(errs, errors.New("An ssh_username must be specified."))
|
||||
}
|
||||
|
||||
var err error
|
||||
c.SSHWaitTimeout, err = time.ParseDuration(c.RawSSHWaitTimeout)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
|
||||
}
|
||||
|
||||
return errs
|
||||
return c.Comm.Prepare(ctx)
|
||||
}
|
||||
|
|
|
@ -4,11 +4,15 @@ import (
|
|||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
)
|
||||
|
||||
func testSSHConfig() *SSHConfig {
|
||||
return &SSHConfig{
|
||||
SSHUser: "foo",
|
||||
Comm: communicator.Config{
|
||||
SSHUsername: "foo",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,8 +23,8 @@ func TestSSHConfigPrepare(t *testing.T) {
|
|||
t.Fatalf("err: %#v", errs)
|
||||
}
|
||||
|
||||
if c.SSHPort != 22 {
|
||||
t.Errorf("bad ssh port: %d", c.SSHPort)
|
||||
if c.Comm.SSHPort != 22 {
|
||||
t.Errorf("bad ssh port: %d", c.Comm.SSHPort)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -78,46 +82,14 @@ func TestSSHConfigPrepare_SSHUser(t *testing.T) {
|
|||
var errs []error
|
||||
|
||||
c = testSSHConfig()
|
||||
c.SSHUser = ""
|
||||
c.Comm.SSHUsername = ""
|
||||
errs = c.Prepare(testConfigTemplate(t))
|
||||
if len(errs) == 0 {
|
||||
t.Fatalf("should have error")
|
||||
}
|
||||
|
||||
c = testSSHConfig()
|
||||
c.SSHUser = "exists"
|
||||
errs = c.Prepare(testConfigTemplate(t))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("should not have error: %#v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSHConfigPrepare_SSHWaitTimeout(t *testing.T) {
|
||||
var c *SSHConfig
|
||||
var errs []error
|
||||
|
||||
// Defaults
|
||||
c = testSSHConfig()
|
||||
c.RawSSHWaitTimeout = ""
|
||||
errs = c.Prepare(testConfigTemplate(t))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("should not have error: %#v", errs)
|
||||
}
|
||||
if c.RawSSHWaitTimeout != "20m" {
|
||||
t.Fatalf("bad value: %s", c.RawSSHWaitTimeout)
|
||||
}
|
||||
|
||||
// Test with a bad value
|
||||
c = testSSHConfig()
|
||||
c.RawSSHWaitTimeout = "this is not good"
|
||||
errs = c.Prepare(testConfigTemplate(t))
|
||||
if len(errs) == 0 {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test with a good one
|
||||
c = testSSHConfig()
|
||||
c.RawSSHWaitTimeout = "5s"
|
||||
c.Comm.SSHUsername = "exists"
|
||||
errs = c.Prepare(testConfigTemplate(t))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("should not have error: %#v", errs)
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/mitchellh/multistep"
|
||||
parallelscommon "github.com/mitchellh/packer/builder/parallels/common"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
"github.com/mitchellh/packer/helper/config"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
|
@ -245,10 +246,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
VMName: b.config.VMName,
|
||||
Ctx: b.config.ctx,
|
||||
},
|
||||
&common.StepConnectSSH{
|
||||
SSHAddress: parallelscommon.SSHAddress,
|
||||
SSHConfig: parallelscommon.SSHConfigFunc(b.config.SSHConfig),
|
||||
SSHWaitTimeout: b.config.SSHWaitTimeout,
|
||||
&communicator.StepConnect{
|
||||
Config: &b.config.SSHConfig.Comm,
|
||||
Host: parallelscommon.CommHost,
|
||||
SSHConfig: parallelscommon.SSHConfigFunc(b.config.SSHConfig),
|
||||
},
|
||||
¶llelscommon.StepUploadVersion{
|
||||
Path: b.config.PrlctlVersionFile,
|
||||
|
|
|
@ -3,11 +3,13 @@ package pvm
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
parallelscommon "github.com/mitchellh/packer/builder/parallels/common"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
)
|
||||
|
||||
// Builder implements packer.Builder and builds the actual Parallels
|
||||
|
@ -80,10 +82,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
VMName: b.config.VMName,
|
||||
Ctx: b.config.ctx,
|
||||
},
|
||||
&common.StepConnectSSH{
|
||||
SSHAddress: parallelscommon.SSHAddress,
|
||||
SSHConfig: parallelscommon.SSHConfigFunc(b.config.SSHConfig),
|
||||
SSHWaitTimeout: b.config.SSHWaitTimeout,
|
||||
&communicator.StepConnect{
|
||||
Config: &b.config.SSHConfig.Comm,
|
||||
Host: parallelscommon.CommHost,
|
||||
SSHConfig: parallelscommon.SSHConfigFunc(b.config.SSHConfig),
|
||||
},
|
||||
¶llelscommon.StepUploadVersion{
|
||||
Path: b.config.PrlctlVersionFile,
|
||||
|
|
|
@ -33,7 +33,7 @@ type Config struct {
|
|||
|
||||
func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
||||
c := new(Config)
|
||||
err := config.Decode(&c, &config.DecodeOpts{
|
||||
err := config.Decode(c, &config.DecodeOpts{
|
||||
Interpolate: true,
|
||||
InterpolateFilter: &interpolate.RenderFilter{
|
||||
Exclude: []string{
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common"
|
||||
commonssh "github.com/mitchellh/packer/common/ssh"
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
"github.com/mitchellh/packer/helper/config"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
|
@ -78,6 +78,7 @@ type Builder struct {
|
|||
|
||||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
Comm communicator.Config `mapstructure:",squash"`
|
||||
|
||||
Accelerator string `mapstructure:"accelerator"`
|
||||
BootCommand []string `mapstructure:"boot_command"`
|
||||
|
@ -103,25 +104,24 @@ type Config struct {
|
|||
ShutdownCommand string `mapstructure:"shutdown_command"`
|
||||
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
|
||||
SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
|
||||
SSHPassword string `mapstructure:"ssh_password"`
|
||||
SSHPort uint `mapstructure:"ssh_port"`
|
||||
SSHUser string `mapstructure:"ssh_username"`
|
||||
SSHKeyPath string `mapstructure:"ssh_key_path"`
|
||||
VNCPortMin uint `mapstructure:"vnc_port_min"`
|
||||
VNCPortMax uint `mapstructure:"vnc_port_max"`
|
||||
VMName string `mapstructure:"vm_name"`
|
||||
|
||||
// These are deprecated, but we keep them around for BC
|
||||
// TODO(@mitchellh): remove
|
||||
SSHKeyPath string `mapstructure:"ssh_key_path"`
|
||||
SSHWaitTimeout time.Duration `mapstructure:"ssh_wait_timeout"`
|
||||
|
||||
// TODO(mitchellh): deprecate
|
||||
RunOnce bool `mapstructure:"run_once"`
|
||||
|
||||
RawBootWait string `mapstructure:"boot_wait"`
|
||||
RawSingleISOUrl string `mapstructure:"iso_url"`
|
||||
RawShutdownTimeout string `mapstructure:"shutdown_timeout"`
|
||||
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
|
||||
|
||||
bootWait time.Duration ``
|
||||
shutdownTimeout time.Duration ``
|
||||
sshWaitTimeout time.Duration ``
|
||||
ctx interpolate.Context
|
||||
}
|
||||
|
||||
|
@ -139,9 +139,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
var errs *packer.MultiError
|
||||
warnings := make([]string, 0)
|
||||
|
||||
if b.config.DiskSize == 0 {
|
||||
b.config.DiskSize = 40000
|
||||
}
|
||||
|
@ -190,10 +187,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
b.config.SSHHostPortMax = 4444
|
||||
}
|
||||
|
||||
if b.config.SSHPort == 0 {
|
||||
b.config.SSHPort = 22
|
||||
}
|
||||
|
||||
if b.config.VNCPortMin == 0 {
|
||||
b.config.VNCPortMin = 5900
|
||||
}
|
||||
|
@ -222,6 +215,21 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
b.config.DiskInterface = "virtio"
|
||||
}
|
||||
|
||||
// TODO: backwards compatibility, write fixer instead
|
||||
if b.config.SSHKeyPath != "" {
|
||||
b.config.Comm.SSHPrivateKey = b.config.SSHKeyPath
|
||||
}
|
||||
if b.config.SSHWaitTimeout != 0 {
|
||||
b.config.Comm.SSHTimeout = b.config.SSHWaitTimeout
|
||||
}
|
||||
|
||||
var errs *packer.MultiError
|
||||
warnings := make([]string, 0)
|
||||
|
||||
if es := b.config.Comm.Prepare(&b.config.ctx); len(es) > 0 {
|
||||
errs = packer.MultiErrorAppend(errs, es...)
|
||||
}
|
||||
|
||||
if !(b.config.Format == "qcow2" || b.config.Format == "raw") {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("invalid format, only 'qcow2' or 'raw' are allowed"))
|
||||
|
@ -314,42 +322,17 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
b.config.RawShutdownTimeout = "5m"
|
||||
}
|
||||
|
||||
if b.config.RawSSHWaitTimeout == "" {
|
||||
b.config.RawSSHWaitTimeout = "20m"
|
||||
}
|
||||
|
||||
b.config.shutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
|
||||
}
|
||||
|
||||
if b.config.SSHKeyPath != "" {
|
||||
if _, err := os.Stat(b.config.SSHKeyPath); err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
|
||||
} else if _, err := commonssh.FileSigner(b.config.SSHKeyPath); err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
if b.config.SSHHostPortMin > b.config.SSHHostPortMax {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
|
||||
}
|
||||
|
||||
if b.config.SSHUser == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("An ssh_username must be specified."))
|
||||
}
|
||||
|
||||
b.config.sshWaitTimeout, err = time.ParseDuration(b.config.RawSSHWaitTimeout)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
|
||||
}
|
||||
|
||||
if b.config.VNCPortMin > b.config.VNCPortMax {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
|
||||
|
@ -409,10 +392,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
steprun,
|
||||
&stepBootWait{},
|
||||
&stepTypeBootCommand{},
|
||||
&common.StepConnectSSH{
|
||||
SSHAddress: sshAddress,
|
||||
SSHConfig: sshConfig,
|
||||
SSHWaitTimeout: b.config.sshWaitTimeout,
|
||||
&communicator.StepConnect{
|
||||
Config: &b.config.Comm,
|
||||
Host: commHost,
|
||||
SSHConfig: sshConfig,
|
||||
SSHPort: commPort,
|
||||
},
|
||||
new(common.StepProvision),
|
||||
new(stepShutdown),
|
||||
|
|
|
@ -79,8 +79,8 @@ func TestBuilderPrepare_Defaults(t *testing.T) {
|
|||
t.Errorf("bad max ssh host port: %d", b.config.SSHHostPortMax)
|
||||
}
|
||||
|
||||
if b.config.SSHPort != 22 {
|
||||
t.Errorf("bad ssh port: %d", b.config.SSHPort)
|
||||
if b.config.Comm.SSHPort != 22 {
|
||||
t.Errorf("bad ssh port: %d", b.config.Comm.SSHPort)
|
||||
}
|
||||
|
||||
if b.config.VMName != "packer-foo" {
|
||||
|
@ -595,10 +595,6 @@ func TestBuilderPrepare_SSHWaitTimeout(t *testing.T) {
|
|||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if b.config.RawSSHWaitTimeout != "20m" {
|
||||
t.Fatalf("bad value: %s", b.config.RawSSHWaitTimeout)
|
||||
}
|
||||
|
||||
// Test with a bad value
|
||||
config["ssh_wait_timeout"] = "this is not good"
|
||||
b = Builder{}
|
||||
|
|
|
@ -1,30 +1,32 @@
|
|||
package qemu
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"github.com/mitchellh/multistep"
|
||||
commonssh "github.com/mitchellh/packer/common/ssh"
|
||||
"github.com/mitchellh/packer/communicator/ssh"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func sshAddress(state multistep.StateBag) (string, error) {
|
||||
func commHost(state multistep.StateBag) (string, error) {
|
||||
return "127.0.0.1", nil
|
||||
}
|
||||
|
||||
func commPort(state multistep.StateBag) (int, error) {
|
||||
sshHostPort := state.Get("sshHostPort").(uint)
|
||||
return fmt.Sprintf("127.0.0.1:%d", sshHostPort), nil
|
||||
return int(sshHostPort), nil
|
||||
}
|
||||
|
||||
func sshConfig(state multistep.StateBag) (*gossh.ClientConfig, error) {
|
||||
config := state.Get("config").(*Config)
|
||||
|
||||
auth := []gossh.AuthMethod{
|
||||
gossh.Password(config.SSHPassword),
|
||||
gossh.Password(config.Comm.SSHPassword),
|
||||
gossh.KeyboardInteractive(
|
||||
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
|
||||
ssh.PasswordKeyboardInteractive(config.Comm.SSHPassword)),
|
||||
}
|
||||
|
||||
if config.SSHKeyPath != "" {
|
||||
signer, err := commonssh.FileSigner(config.SSHKeyPath)
|
||||
if config.Comm.SSHPrivateKey != "" {
|
||||
signer, err := commonssh.FileSigner(config.Comm.SSHPrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -33,7 +35,7 @@ func sshConfig(state multistep.StateBag) (*gossh.ClientConfig, error) {
|
|||
}
|
||||
|
||||
return &gossh.ClientConfig{
|
||||
User: config.SSHUser,
|
||||
User: config.Comm.SSHUsername,
|
||||
Auth: auth,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -80,7 +80,8 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error
|
|||
|
||||
defaultArgs["-name"] = vmName
|
||||
defaultArgs["-machine"] = fmt.Sprintf("type=%s", config.MachineType)
|
||||
defaultArgs["-netdev"] = fmt.Sprintf("user,id=user.0,hostfwd=tcp::%v-:22", sshHostPort)
|
||||
defaultArgs["-netdev"] = fmt.Sprintf(
|
||||
"user,id=user.0,hostfwd=tcp::%v-:%d", sshHostPort, config.Comm.Port())
|
||||
defaultArgs["-device"] = fmt.Sprintf("%s,netdev=user.0", config.NetDevice)
|
||||
defaultArgs["-drive"] = fmt.Sprintf("file=%s,if=%s,cache=%s,discard=%s", imgPath, config.DiskInterface, config.DiskCache, config.DiskDiscard)
|
||||
if !config.DiskImage {
|
||||
|
|
|
@ -2,7 +2,6 @@ package common
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
|
@ -17,13 +16,5 @@ func (c *OutputConfig) Prepare(ctx *interpolate.Context, pc *common.PackerConfig
|
|||
c.OutputDir = fmt.Sprintf("output-%s", pc.PackerBuildName)
|
||||
}
|
||||
|
||||
var errs []error
|
||||
if !pc.PackerForce {
|
||||
if _, err := os.Stat(c.OutputDir); err == nil {
|
||||
errs = append(errs, fmt.Errorf(
|
||||
"Output directory '%s' already exists. It must not exist.", c.OutputDir))
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -39,27 +39,7 @@ func TestOutputConfigPrepare_exists(t *testing.T) {
|
|||
PackerForce: false,
|
||||
}
|
||||
errs := c.Prepare(testConfigTemplate(t), pc)
|
||||
if len(errs) == 0 {
|
||||
t.Fatal("should have errors")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutputConfigPrepare_forceExists(t *testing.T) {
|
||||
td, err := ioutil.TempDir("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
defer os.RemoveAll(td)
|
||||
|
||||
c := new(OutputConfig)
|
||||
c.OutputDir = td
|
||||
|
||||
pc := &common.PackerConfig{
|
||||
PackerBuildName: "foo",
|
||||
PackerForce: true,
|
||||
}
|
||||
errs := c.Prepare(testConfigTemplate(t), pc)
|
||||
if len(errs) > 0 {
|
||||
if len(errs) != 0 {
|
||||
t.Fatal("should not have errors")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +1,31 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"github.com/mitchellh/multistep"
|
||||
commonssh "github.com/mitchellh/packer/common/ssh"
|
||||
"github.com/mitchellh/packer/communicator/ssh"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func SSHAddress(state multistep.StateBag) (string, error) {
|
||||
func CommHost(state multistep.StateBag) (string, error) {
|
||||
return "127.0.0.1", nil
|
||||
}
|
||||
|
||||
func SSHPort(state multistep.StateBag) (int, error) {
|
||||
sshHostPort := state.Get("sshHostPort").(uint)
|
||||
return fmt.Sprintf("127.0.0.1:%d", sshHostPort), nil
|
||||
return int(sshHostPort), nil
|
||||
}
|
||||
|
||||
func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*gossh.ClientConfig, error) {
|
||||
return func(state multistep.StateBag) (*gossh.ClientConfig, error) {
|
||||
auth := []gossh.AuthMethod{
|
||||
gossh.Password(config.SSHPassword),
|
||||
gossh.Password(config.Comm.SSHPassword),
|
||||
gossh.KeyboardInteractive(
|
||||
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
|
||||
ssh.PasswordKeyboardInteractive(config.Comm.SSHPassword)),
|
||||
}
|
||||
|
||||
if config.SSHKeyPath != "" {
|
||||
signer, err := commonssh.FileSigner(config.SSHKeyPath)
|
||||
signer, err := commonssh.FileSigner(config.Comm.SSHPrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -32,7 +34,7 @@ func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*gossh.ClientConf
|
|||
}
|
||||
|
||||
return &gossh.ClientConfig{
|
||||
User: config.SSHUser,
|
||||
User: config.Comm.SSHUsername,
|
||||
Auth: auth,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -2,24 +2,23 @@ package common
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
commonssh "github.com/mitchellh/packer/common/ssh"
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
)
|
||||
|
||||
type SSHConfig struct {
|
||||
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
|
||||
SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
|
||||
SSHKeyPath string `mapstructure:"ssh_key_path"`
|
||||
SSHPassword string `mapstructure:"ssh_password"`
|
||||
SSHPort uint `mapstructure:"ssh_port"`
|
||||
SSHUser string `mapstructure:"ssh_username"`
|
||||
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
|
||||
Comm communicator.Config `mapstructure:",squash"`
|
||||
|
||||
SSHWaitTimeout time.Duration
|
||||
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
|
||||
SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
|
||||
SSHSkipNatMapping bool `mapstructure:"ssh_skip_nat_mapping"`
|
||||
|
||||
// These are deprecated, but we keep them around for BC
|
||||
// TODO(@mitchellh): remove
|
||||
SSHKeyPath string `mapstructure:"ssh_key_path"`
|
||||
SSHWaitTimeout time.Duration `mapstructure:"ssh_wait_timeout"`
|
||||
}
|
||||
|
||||
func (c *SSHConfig) Prepare(ctx *interpolate.Context) []error {
|
||||
|
@ -31,37 +30,19 @@ func (c *SSHConfig) Prepare(ctx *interpolate.Context) []error {
|
|||
c.SSHHostPortMax = 4444
|
||||
}
|
||||
|
||||
if c.SSHPort == 0 {
|
||||
c.SSHPort = 22
|
||||
}
|
||||
|
||||
if c.RawSSHWaitTimeout == "" {
|
||||
c.RawSSHWaitTimeout = "20m"
|
||||
}
|
||||
|
||||
var errs []error
|
||||
// TODO: backwards compatibility, write fixer instead
|
||||
if c.SSHKeyPath != "" {
|
||||
if _, err := os.Stat(c.SSHKeyPath); err != nil {
|
||||
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
|
||||
} else if _, err := commonssh.FileSigner(c.SSHKeyPath); err != nil {
|
||||
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
|
||||
}
|
||||
c.Comm.SSHPrivateKey = c.SSHKeyPath
|
||||
}
|
||||
if c.SSHWaitTimeout != 0 {
|
||||
c.Comm.SSHTimeout = c.SSHWaitTimeout
|
||||
}
|
||||
|
||||
errs := c.Comm.Prepare(ctx)
|
||||
if c.SSHHostPortMin > c.SSHHostPortMax {
|
||||
errs = append(errs,
|
||||
errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
|
||||
}
|
||||
|
||||
if c.SSHUser == "" {
|
||||
errs = append(errs, errors.New("An ssh_username must be specified."))
|
||||
}
|
||||
|
||||
var err error
|
||||
c.SSHWaitTimeout, err = time.ParseDuration(c.RawSSHWaitTimeout)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
|
|
@ -4,11 +4,15 @@ import (
|
|||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
)
|
||||
|
||||
func testSSHConfig() *SSHConfig {
|
||||
return &SSHConfig{
|
||||
SSHUser: "foo",
|
||||
Comm: communicator.Config{
|
||||
SSHUsername: "foo",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -27,8 +31,8 @@ func TestSSHConfigPrepare(t *testing.T) {
|
|||
t.Errorf("bad max ssh host port: %d", c.SSHHostPortMax)
|
||||
}
|
||||
|
||||
if c.SSHPort != 22 {
|
||||
t.Errorf("bad ssh port: %d", c.SSHPort)
|
||||
if c.Comm.SSHPort != 22 {
|
||||
t.Errorf("bad ssh port: %d", c.Comm.SSHPort)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,46 +113,14 @@ func TestSSHConfigPrepare_SSHUser(t *testing.T) {
|
|||
var errs []error
|
||||
|
||||
c = testSSHConfig()
|
||||
c.SSHUser = ""
|
||||
c.Comm.SSHUsername = ""
|
||||
errs = c.Prepare(testConfigTemplate(t))
|
||||
if len(errs) == 0 {
|
||||
t.Fatalf("should have error")
|
||||
}
|
||||
|
||||
c = testSSHConfig()
|
||||
c.SSHUser = "exists"
|
||||
errs = c.Prepare(testConfigTemplate(t))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("should not have error: %#v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSHConfigPrepare_SSHWaitTimeout(t *testing.T) {
|
||||
var c *SSHConfig
|
||||
var errs []error
|
||||
|
||||
// Defaults
|
||||
c = testSSHConfig()
|
||||
c.RawSSHWaitTimeout = ""
|
||||
errs = c.Prepare(testConfigTemplate(t))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("should not have error: %#v", errs)
|
||||
}
|
||||
if c.RawSSHWaitTimeout != "20m" {
|
||||
t.Fatalf("bad value: %s", c.RawSSHWaitTimeout)
|
||||
}
|
||||
|
||||
// Test with a bad value
|
||||
c = testSSHConfig()
|
||||
c.RawSSHWaitTimeout = "this is not good"
|
||||
errs = c.Prepare(testConfigTemplate(t))
|
||||
if len(errs) == 0 {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test with a good one
|
||||
c = testSSHConfig()
|
||||
c.RawSSHWaitTimeout = "5s"
|
||||
c.Comm.SSHUsername = "exists"
|
||||
errs = c.Prepare(testConfigTemplate(t))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("should not have error: %#v", errs)
|
||||
|
|
|
@ -17,9 +17,10 @@ import (
|
|||
// Produces:
|
||||
// exportPath string - The path to the resulting export.
|
||||
type StepExport struct {
|
||||
Format string
|
||||
OutputDir string
|
||||
ExportOpts []string
|
||||
Format string
|
||||
OutputDir string
|
||||
ExportOpts []string
|
||||
SkipNatMapping bool
|
||||
}
|
||||
|
||||
func (s *StepExport) Run(state multistep.StateBag) multistep.StepAction {
|
||||
|
@ -30,30 +31,31 @@ func (s *StepExport) Run(state multistep.StateBag) multistep.StepAction {
|
|||
// Wait a second to ensure VM is really shutdown.
|
||||
log.Println("1 second timeout to ensure VM is really shutdown")
|
||||
time.Sleep(1 * time.Second)
|
||||
ui.Say("Preparing to export machine...")
|
||||
|
||||
// Clear out the Packer-created forwarding rule
|
||||
ui.Say("Preparing to export machine...")
|
||||
ui.Message(fmt.Sprintf(
|
||||
"Deleting forwarded port mapping for SSH (host port %d)",
|
||||
state.Get("sshHostPort")))
|
||||
command := []string{"modifyvm", vmName, "--natpf1", "delete", "packerssh"}
|
||||
if err := driver.VBoxManage(command...); err != nil {
|
||||
err := fmt.Errorf("Error deleting port forwarding rule: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
if !s.SkipNatMapping {
|
||||
ui.Message(fmt.Sprintf(
|
||||
"Deleting forwarded port mapping for SSH (host port %d)",
|
||||
state.Get("sshHostPort")))
|
||||
command := []string{"modifyvm", vmName, "--natpf1", "delete", "packerssh"}
|
||||
if err := driver.VBoxManage(command...); err != nil {
|
||||
err := fmt.Errorf("Error deleting port forwarding rule: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
// Export the VM to an OVF
|
||||
outputPath := filepath.Join(s.OutputDir, vmName+"."+s.Format)
|
||||
|
||||
command = []string{
|
||||
command := []string{
|
||||
"export",
|
||||
vmName,
|
||||
"--output",
|
||||
outputPath,
|
||||
}
|
||||
|
||||
command = append(command, s.ExportOpts...)
|
||||
|
||||
ui.Say("Exporting virtual machine...")
|
||||
|
|
|
@ -2,11 +2,13 @@ package common
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// This step adds a NAT port forwarding definition so that SSH is available
|
||||
|
@ -19,9 +21,10 @@ import (
|
|||
//
|
||||
// Produces:
|
||||
type StepForwardSSH struct {
|
||||
GuestPort uint
|
||||
HostPortMin uint
|
||||
HostPortMax uint
|
||||
CommConfig *communicator.Config
|
||||
HostPortMin uint
|
||||
HostPortMax uint
|
||||
SkipNatMapping bool
|
||||
}
|
||||
|
||||
func (s *StepForwardSSH) Run(state multistep.StateBag) multistep.StepAction {
|
||||
|
@ -29,39 +32,42 @@ func (s *StepForwardSSH) Run(state multistep.StateBag) multistep.StepAction {
|
|||
ui := state.Get("ui").(packer.Ui)
|
||||
vmName := state.Get("vmName").(string)
|
||||
|
||||
log.Printf("Looking for available SSH port between %d and %d",
|
||||
s.HostPortMin, s.HostPortMax)
|
||||
var sshHostPort uint
|
||||
var offset uint = 0
|
||||
guestPort := s.CommConfig.Port()
|
||||
sshHostPort := guestPort
|
||||
if !s.SkipNatMapping {
|
||||
log.Printf("Looking for available SSH port between %d and %d",
|
||||
s.HostPortMin, s.HostPortMax)
|
||||
offset := 0
|
||||
|
||||
portRange := int(s.HostPortMax - s.HostPortMin)
|
||||
if portRange > 0 {
|
||||
// Have to check if > 0 to avoid a panic
|
||||
offset = uint(rand.Intn(portRange))
|
||||
}
|
||||
|
||||
for {
|
||||
sshHostPort = offset + s.HostPortMin
|
||||
log.Printf("Trying port: %d", sshHostPort)
|
||||
l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", sshHostPort))
|
||||
if err == nil {
|
||||
defer l.Close()
|
||||
break
|
||||
portRange := int(s.HostPortMax - s.HostPortMin)
|
||||
if portRange > 0 {
|
||||
// Have to check if > 0 to avoid a panic
|
||||
offset = rand.Intn(portRange)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a forwarded port mapping to the VM
|
||||
ui.Say(fmt.Sprintf("Creating forwarded port mapping for SSH (host port %d)", sshHostPort))
|
||||
command := []string{
|
||||
"modifyvm", vmName,
|
||||
"--natpf1",
|
||||
fmt.Sprintf("packerssh,tcp,127.0.0.1,%d,,%d", sshHostPort, s.GuestPort),
|
||||
}
|
||||
if err := driver.VBoxManage(command...); err != nil {
|
||||
err := fmt.Errorf("Error creating port forwarding rule: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
for {
|
||||
sshHostPort = offset + int(s.HostPortMin)
|
||||
log.Printf("Trying port: %d", sshHostPort)
|
||||
l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", sshHostPort))
|
||||
if err == nil {
|
||||
defer l.Close()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Create a forwarded port mapping to the VM
|
||||
ui.Say(fmt.Sprintf("Creating forwarded port mapping for SSH (host port %d)", sshHostPort))
|
||||
command := []string{
|
||||
"modifyvm", vmName,
|
||||
"--natpf1",
|
||||
fmt.Sprintf("packerssh,tcp,127.0.0.1,%d,,%d", sshHostPort, guestPort),
|
||||
}
|
||||
if err := driver.VBoxManage(command...); err != nil {
|
||||
err := fmt.Errorf("Error creating port forwarding rule: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
// Save the port we're using so that future steps can use it
|
||||
|
|
|
@ -49,7 +49,7 @@ func (s *StepHTTPServer) Run(state multistep.StateBag) multistep.StepAction {
|
|||
}
|
||||
|
||||
httpPort = offset + s.HTTPPortMin
|
||||
httpAddr = fmt.Sprintf(":%d", httpPort)
|
||||
httpAddr = fmt.Sprintf("0.0.0.0:%d", httpPort)
|
||||
log.Printf("Trying port: %d", httpPort)
|
||||
s.l, err = net.Listen("tcp", httpAddr)
|
||||
if err == nil {
|
||||
|
|
|
@ -22,7 +22,16 @@ type StepOutputDir struct {
|
|||
func (s *StepOutputDir) Run(state multistep.StateBag) multistep.StepAction {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
if _, err := os.Stat(s.Path); err == nil && s.Force {
|
||||
if _, err := os.Stat(s.Path); err == nil {
|
||||
if !s.Force {
|
||||
err := fmt.Errorf(
|
||||
"Output directory exists: %s\n\n"+
|
||||
"Use the force flag to delete it prior to building.",
|
||||
s.Path)
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
ui.Say("Deleting previous output directory...")
|
||||
os.RemoveAll(s.Path)
|
||||
}
|
||||
|
|
|
@ -45,6 +45,30 @@ func TestStepOutputDir(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestStepOutputDir_exists(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := testStepOutputDir(t)
|
||||
|
||||
// Make the dir
|
||||
if err := os.MkdirAll(step.Path, 0755); err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
||||
// Test the run
|
||||
if action := step.Run(state); action != multistep.ActionHalt {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
if _, ok := state.GetOk("error"); !ok {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test the cleanup
|
||||
step.Cleanup(state)
|
||||
if _, err := os.Stat(step.Path); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepOutputDir_cancelled(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := testStepOutputDir(t)
|
||||
|
|
|
@ -38,6 +38,19 @@ func (s *StepRemoveDevices) Run(state multistep.StateBag) multistep.StepAction {
|
|||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Don't forget to remove the floppy controller as well
|
||||
command = []string{
|
||||
"storagectl", vmName,
|
||||
"--name", "Floppy Controller",
|
||||
"--remove",
|
||||
}
|
||||
if err := driver.VBoxManage(command...); err != nil {
|
||||
err := fmt.Errorf("Error removing floppy controller: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := state.GetOk("attachedIso"); ok {
|
||||
|
|
|
@ -102,10 +102,13 @@ func TestStepRemoveDevices_floppyPath(t *testing.T) {
|
|||
}
|
||||
|
||||
// Test that both were removed
|
||||
if len(driver.VBoxManageCalls) != 1 {
|
||||
if len(driver.VBoxManageCalls) != 2 {
|
||||
t.Fatalf("bad: %#v", driver.VBoxManageCalls)
|
||||
}
|
||||
if driver.VBoxManageCalls[0][3] != "Floppy Controller" {
|
||||
t.Fatalf("bad: %#v", driver.VBoxManageCalls)
|
||||
}
|
||||
if driver.VBoxManageCalls[1][3] != "Floppy Controller" {
|
||||
t.Fatalf("bad: %#v", driver.VBoxManageCalls)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/mitchellh/multistep"
|
||||
vboxcommon "github.com/mitchellh/packer/builder/virtualbox/common"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
"github.com/mitchellh/packer/helper/config"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/mitchellh/packer/template/interpolate"
|
||||
|
@ -230,6 +231,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
Description: "ISO",
|
||||
ResultKey: "iso_path",
|
||||
Url: b.config.ISOUrls,
|
||||
Extension: "iso",
|
||||
},
|
||||
&vboxcommon.StepOutputDir{
|
||||
Force: b.config.PackerForce,
|
||||
|
@ -252,9 +254,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
},
|
||||
new(vboxcommon.StepAttachFloppy),
|
||||
&vboxcommon.StepForwardSSH{
|
||||
GuestPort: b.config.SSHPort,
|
||||
HostPortMin: b.config.SSHHostPortMin,
|
||||
HostPortMax: b.config.SSHHostPortMax,
|
||||
CommConfig: &b.config.SSHConfig.Comm,
|
||||
HostPortMin: b.config.SSHHostPortMin,
|
||||
HostPortMax: b.config.SSHHostPortMax,
|
||||
SkipNatMapping: b.config.SSHSkipNatMapping,
|
||||
},
|
||||
&vboxcommon.StepVBoxManage{
|
||||
Commands: b.config.VBoxManage,
|
||||
|
@ -269,10 +272,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
VMName: b.config.VMName,
|
||||
Ctx: b.config.ctx,
|
||||
},
|
||||
&common.StepConnectSSH{
|
||||
SSHAddress: vboxcommon.SSHAddress,
|
||||
SSHConfig: vboxcommon.SSHConfigFunc(b.config.SSHConfig),
|
||||
SSHWaitTimeout: b.config.SSHWaitTimeout,
|
||||
&communicator.StepConnect{
|
||||
Config: &b.config.SSHConfig.Comm,
|
||||
Host: vboxcommon.CommHost,
|
||||
SSHConfig: vboxcommon.SSHConfigFunc(b.config.SSHConfig),
|
||||
SSHPort: vboxcommon.SSHPort,
|
||||
},
|
||||
&vboxcommon.StepUploadVersion{
|
||||
Path: b.config.VBoxVersionFile,
|
||||
|
@ -293,9 +297,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
Ctx: b.config.ctx,
|
||||
},
|
||||
&vboxcommon.StepExport{
|
||||
Format: b.config.Format,
|
||||
OutputDir: b.config.OutputDir,
|
||||
ExportOpts: b.config.ExportOpts.ExportOpts,
|
||||
Format: b.config.Format,
|
||||
OutputDir: b.config.OutputDir,
|
||||
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"
|
||||
vboxcommon "github.com/mitchellh/packer/builder/virtualbox/common"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/helper/communicator"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
|
@ -82,9 +83,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
},
|
||||
new(vboxcommon.StepAttachFloppy),
|
||||
&vboxcommon.StepForwardSSH{
|
||||
GuestPort: b.config.SSHPort,
|
||||
HostPortMin: b.config.SSHHostPortMin,
|
||||
HostPortMax: b.config.SSHHostPortMax,
|
||||
CommConfig: &b.config.SSHConfig.Comm,
|
||||
HostPortMin: b.config.SSHHostPortMin,
|
||||
HostPortMax: b.config.SSHHostPortMax,
|
||||
SkipNatMapping: b.config.SSHSkipNatMapping,
|
||||
},
|
||||
&vboxcommon.StepVBoxManage{
|
||||
Commands: b.config.VBoxManage,
|
||||
|
@ -99,10 +101,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
VMName: b.config.VMName,
|
||||
Ctx: b.config.ctx,
|
||||
},
|
||||
&common.StepConnectSSH{
|
||||
SSHAddress: vboxcommon.SSHAddress,
|
||||
SSHConfig: vboxcommon.SSHConfigFunc(b.config.SSHConfig),
|
||||
SSHWaitTimeout: b.config.SSHWaitTimeout,
|
||||
&communicator.StepConnect{
|
||||
Config: &b.config.SSHConfig.Comm,
|
||||
Host: vboxcommon.CommHost,
|
||||
SSHConfig: vboxcommon.SSHConfigFunc(b.config.SSHConfig),
|
||||
SSHPort: vboxcommon.SSHPort,
|
||||
},
|
||||
&vboxcommon.StepUploadVersion{
|
||||
Path: b.config.VBoxVersionFile,
|
||||
|
@ -123,9 +126,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
Ctx: b.config.ctx,
|
||||
},
|
||||
&vboxcommon.StepExport{
|
||||
Format: b.config.Format,
|
||||
OutputDir: b.config.OutputDir,
|
||||
ExportOpts: b.config.ExportOpts.ExportOpts,
|
||||
Format: b.config.Format,
|
||||
OutputDir: b.config.OutputDir,
|
||||
ExportOpts: b.config.ExportOpts.ExportOpts,
|
||||
SkipNatMapping: b.config.SSHSkipNatMapping,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue