Commit 8f9e9abc authored by Vasiliy Tolstov's avatar Vasiliy Tolstov

Merge branch 'master' into digitalocean

parents e614a8c3 4406c20a
...@@ -6,5 +6,4 @@ ...@@ -6,5 +6,4 @@
/website/build /website/build
.DS_Store .DS_Store
.vagrant .vagrant
Vagrantfile
test/.env test/.env
...@@ -7,16 +7,9 @@ go: ...@@ -7,16 +7,9 @@ go:
install: make updatedeps install: make updatedeps
script: script:
- make test - GOMAXPROCS=2 make test
#- go test -race ./... #- go test -race ./...
notifications:
irc:
channels:
- "chat.freenode.net#packer-tool"
on_success: change
on_failure: always
matrix: matrix:
allow_failures: allow_failures:
- go: tip - go: tip
## 0.7.0 (unreleased) ## 0.7.2 (unreleased)
FEATURES: FEATURES:
* builder/parallels: Don't depend on _prl-utils_ [GH-1499]
IMPROVEMENTS:
* builder/amazon/all: Support new AWS Frankfurt region.
* builder/docker: Allow remote `DOCKER_HOST`, which works as long as
volumes work. [GH-1594]
* builder/qemu: Can set cache mode for main disk. [GH-1558]
* builder/vmware/vmx: Source VMX can have a disk connected via SATA. [GH-1604]
* post-processors/vagrantcloud: Support self-hosted box URLs.
BUG FIXES:
* core: Fix loading plugins from pwd. [GH-1521]
* builder/amazon: Prefer token in config if given. [GH-1544]
* builder/virtualbox: Can read VirtualBox version on FreeBSD. [GH-1570]
* builder/virtualbox: More robust reading of guest additions URL. [GH-1509]
* builder/vmware: Always remove floppies/drives. [GH-1504]
* builder/vmware: Wait some time so that post-VMX update aren't
overwritten. [GH-1504]
* builder/vmware-vmx: Fix issue with order of boot command support [GH-1492]
* builder/parallels: Ignore 'The fdd0 device does not exist' [GH-1501]
* builder/parallels: Rely on Cleanup functions to detach devices [GH-1502]
* builder/parallels: Create VM without hdd and then add it later [GH-1548]
* builder/parallels: Disconnect cdrom0 [GH-1605]
* builder/qemu: Don't use `-redir` flag anymore, replace with
`hostfwd` options. [GH-1561]
* builder/qmeu: Use `pc` as default machine type instead of `pc-1.0`.
* providers/aws: Ignore transient network errors. [GH-1579]
* provisioner/ansible: Don't buffer output so output streams in. [GH-1585]
* provisioner/ansible: Use inventory file always to avoid potentially
deprecated feature. [GH-1562]
* provisioner/shell: Quote environmental variables. [GH-1568]
* provisioner/salt: Bootstrap over SSL. [GH-1608]
* post-processors/docker-push: Work with docker-tag artifacts. [GH-1526]
* post-processors/vsphere: Append "/" to object address. [GH-1615]
## 0.7.1 (September 10, 2014)
FEATURES:
* builder/vmware: VMware Fusion Pro 7 is now supported. [GH-1478]
BUG FIXES:
* core: SSH will connect slightly faster if it is ready immediately.
* provisioner/file: directory uploads no longer hang. [GH-1484]
* provisioner/file: fixed crash on large files. [GH-1473]
* scripts: Windows executable renamed to packer.exe. [GH-1483]
## 0.7.0 (September 8, 2014)
BACKWARDS INCOMPATIBILITIES:
* The authentication configuration for Google Compute Engine has changed.
The new method is much simpler, but is not backwards compatible.
`packer fix` will _not_ fix this. Please read the updated GCE docs.
FEATURES:
* **New Post-Processor: `compress`** - Gzip compresses artifacts with files.
* **New Post-Processor: `docker-save`** - Save an image. This is similar to
export, but preserves the image hierarchy.
* **New Post-Processor: `docker-tag`** - Tag a created image.
* **New Template Functions: `upper`, `lower`** - See documentation for
more details.
* core: Plugins are automatically discovered if they're named properly. * core: Plugins are automatically discovered if they're named properly.
Packer will look in the PWD and the directory with `packer` for Packer will look in the PWD and the directory with `packer` for
binaries named `packer-TYPE-NAME`. binaries named `packer-TYPE-NAME`.
* core: Plugins placed in `~/.packer.d/plugins` are now automatically
discovered.
* builder/amazon: Spot instances can now be used to build EBS backed and
instance store images. [GH-1139]
* builder/docker: Images can now be committed instead of exported. [GH-1198]
* builder/virtualbox-ovf: New `import_flags` setting can be used to add
new command line flags to `VBoxManage import` to allow things such
as EULAs to be accepted. [GH-1383]
* builder/virtualbox-ovf: Boot commands and the HTTP server are supported.
[GH-1169]
* builder/vmware: VMware Player 6 is now supported. [GH-1168] * builder/vmware: VMware Player 6 is now supported. [GH-1168]
* builder/vmware-vmx: Boot commands and the HTTP server are supported.
[GH-1169]
IMPROVEMENTS: IMPROVEMENTS:
* core: `isotime` function can take a format. [GH-1126]
* builder/amazon/all: `AWS_SECURITY_TOKEN` is read and can also be
set with the `token` configuration. [GH-1236]
* builder/amazon/all: Can force SSH on the private IP address with
`ssh_private_ip`. [GH-1229]
* builder/amazon/all: String fields in device mappings can use variables. [GH-1090]
* builder/amazon-instance: EBS AMIs can be used as a source. [GH-1453] * builder/amazon-instance: EBS AMIs can be used as a source. [GH-1453]
* builder/digitalocean: Can set API URL endpoint. [GH-1448] * builder/digitalocean: Can set API URL endpoint. [GH-1448]
* builder/digitalocean: Region supports variables. [GH-1452] * builder/digitalocean: Region supports variables. [GH-1452]
* builder/parallels/all Path to tools ISO is calculated automatically. [GH-1455] * builder/docker: Can now specify login credentials to pull images.
* builder/docker: Support mounting additional volumes. [GH-1430]
* builder/parallels/all: Path to tools ISO is calculated automatically. [GH-1455]
* builder/parallels-pvm: `reassign_mac` option to choose wehther or not
to generate a new MAC address. [GH-1461]
* builder/qemu: Can specify "none" acceleration type. [GH-1395] * builder/qemu: Can specify "none" acceleration type. [GH-1395]
* builder/qemu: Can specify "tcg" acceleration type. [GH-1395] * builder/qemu: Can specify "tcg" acceleration type. [GH-1395]
* builder/virtualbox/all: `iso_interface` option to mount ISO with SATA. [GH-1200] * builder/virtualbox/all: `iso_interface` option to mount ISO with SATA. [GH-1200]
* builder/vmware-vmx: Proper `floppy_files` support. [GH-1057] * builder/vmware-vmx: Proper `floppy_files` support. [GH-1057]
* command/build: Add `-color=false` flag to disable color. [GH-1433]
* post-processor/docker-push: Can now specify login credentials. [GH-1243]
* provisioner/chef-client: Support `chef_environment`. [GH-1190]
BUG FIXES: BUG FIXES:
* core: nicer error message if an encrypted private key is used for
SSH. [GH-1445]
* core: Fix crash that could happen with a well timed double Ctrl-C.
[GH-1328] [GH-1314]
* core: SSH TCP keepalive period is now 5 seconds (shorter). [GH-1232]
* builder/amazon-chroot: Can properly build HVM images now. [GH-1360] * builder/amazon-chroot: Can properly build HVM images now. [GH-1360]
* builder/amazon-chroot: Fix crash in root device check. [GH-1360] * builder/amazon-chroot: Fix crash in root device check. [GH-1360]
* builder/amazon-chroot: Add description that Packer made the snapshot
with a time. [GH-1388]
* builder/amazon-ebs: AMI is deregistered if an error. [GH-1186]
* builder/amazon-instance: Fix deprecation warning for `ec2-bundle-vol` * builder/amazon-instance: Fix deprecation warning for `ec2-bundle-vol`
[GH-1424] [GH-1424]
* builder/amazon-instance: Add `--no-filter` to the `ec2-bundle-vol`
command by default to avoid corrupting data by removing package
manager certs. [GH-1137]
* builder/amazon/all: `delete_on_termination` set to false will work.
* builder/amazon/all: Fix race condition on setting tags. [GH-1367]
* builder/amazon/all: More desctriptive error messages if Amazon only
sends an error code. [GH-1189]
* builder/docker: Error if `DOCKER_HOST` is set.
* builder/docker: Remove the container during cleanup. [GH-1206]
* builder/docker: Fix case where not all output would show up from
provisioners.
* builder/googlecompute: add `disk_size` option. [GH-1397] * builder/googlecompute: add `disk_size` option. [GH-1397]
* builder/googlecompute: Auth works with latest formats on Google Cloud
Console. [GH-1344]
* builder/openstack: Region is not required. [GH-1418] * builder/openstack: Region is not required. [GH-1418]
* builder/parallels-iso: ISO not removed from VM after install [GH-1338] * builder/parallels-iso: ISO not removed from VM after install [GH-1338]
* builder/parallels/all: Add support for Parallels Desktop 10 [GH-1438] * builder/parallels/all: Add support for Parallels Desktop 10 [GH-1438]
* builder/parallels/all: Added some navigation keys [GH-1442] * builder/parallels/all: Added some navigation keys [GH-1442]
* builder/qemu: If headless, sdl display won't be used. [GH-1395] * builder/qemu: If headless, sdl display won't be used. [GH-1395]
* builder/qemu: Use `512M` as `-m` default. [GH-1444] * builder/qemu: Use `512M` as `-m` default. [GH-1444]
* builder/virtualbox/all: Search `VBOX_MSI_INSTALL_PATH` for path to
`VBoxManage` on Windows. [GH-1337]
* builder/virtualbox/all: Seed RNG to avoid same ports. [GH-1386] * builder/virtualbox/all: Seed RNG to avoid same ports. [GH-1386]
* builder/virtualbox/all: Better error if guest additions URL couldn't be
detected. [GH-1439]
* builder/virtualbox/all: Detect errors even when `VBoxManage` exits
with a zero exit code. [GH-1119]
* builder/virtualbox/iso: Append timestamp to default name for parallel
builds. [GH-1365]
* builder/vmware/all: No more error when Packer stops an already-stopped
VM. [GH-1300]
* builder/vmware/all: `ssh_host` accepts templates. [GH-1396] * builder/vmware/all: `ssh_host` accepts templates. [GH-1396]
* builder/vmware/all: Don't remount floppy in VMX post step. [GH-1239] * builder/vmware/all: Don't remount floppy in VMX post step. [GH-1239]
* builder/vmware/vmx: Do not re-add floppy disk files to VMX [GH-1361] * builder/vmware/vmx: Do not re-add floppy disk files to VMX [GH-1361]
* builder/vmware-iso: Fix crash when `vnc_port_min` and max were the
same value. [GH-1288]
* builder/vmware-iso: Finding an available VNC port on Windows works. [GH-1372]
* builder/vmware-vmx: Nice error if Clone is not supported (not VMware
Fusion Pro). [GH-787]
* post-processor/vagrant: Can supply your own metadata.json. [GH-1143] * post-processor/vagrant: Can supply your own metadata.json. [GH-1143]
* provisioner/ansible-local: Use proper path on Windows. [GH-1375] * provisioner/ansible-local: Use proper path on Windows. [GH-1375]
* provisioner/file: Mode will now be preserved. [GH-1064]
## 0.6.1 (July 20, 2014) ## 0.6.1 (July 20, 2014)
......
...@@ -15,6 +15,6 @@ testrace: ...@@ -15,6 +15,6 @@ testrace:
go test -race $(TEST) $(TESTARGS) go test -race $(TEST) $(TESTARGS)
updatedeps: updatedeps:
go get -u -v ./... go get -u -v -p 2 ./...
.PHONY: bin default test updatedeps .PHONY: bin default test updatedeps
# -*- mode: ruby -*-
# vi: set ft=ruby :
$script = <<SCRIPT
SRCROOT="/opt/go"
# Install Go
sudo apt-get update
sudo apt-get install -y build-essential mercurial
sudo hg clone -u release https://code.google.com/p/go ${SRCROOT}
cd ${SRCROOT}/src
sudo ./all.bash
# Setup the GOPATH
sudo mkdir -p /opt/gopath
cat <<EOF >/tmp/gopath.sh
export GOPATH="/opt/gopath"
export PATH="/opt/go/bin:\$GOPATH/bin:\$PATH"
EOF
sudo mv /tmp/gopath.sh /etc/profile.d/gopath.sh
sudo chmod 0755 /etc/profile.d/gopath.sh
# Make sure the gopath is usable by vagrant
sudo chown -R vagrant:vagrant $SRCROOT
sudo chown -R vagrant:vagrant /opt/gopath
# Install some other stuff we need
sudo apt-get install -y curl git-core zip
SCRIPT
Vagrant.configure(2) do |config|
config.vm.box = "chef/ubuntu-12.04"
config.vm.provision "shell", inline: $script
config.vm.synced_folder ".", "/vagrant", disabled: true
["vmware_fusion", "vmware_workstation"].each do |p|
config.vm.provider "p" do |v|
v.vmx["memsize"] = "2048"
v.vmx["numvcpus"] = "2"
v.vmx["cpuid.coresPerSocket"] = "1"
end
end
end
...@@ -60,7 +60,7 @@ func (c *Communicator) Start(cmd *packer.RemoteCmd) error { ...@@ -60,7 +60,7 @@ func (c *Communicator) Start(cmd *packer.RemoteCmd) error {
return nil return nil
} }
func (c *Communicator) Upload(dst string, r io.Reader) error { func (c *Communicator) Upload(dst string, r io.Reader, fi *os.FileInfo) error {
dst = filepath.Join(c.Chroot, dst) dst = filepath.Join(c.Chroot, dst)
log.Printf("Uploading to chroot dir: %s", dst) log.Printf("Uploading to chroot dir: %s", dst)
tf, err := ioutil.TempFile("", "packer-amazon-chroot") tf, err := ioutil.TempFile("", "packer-amazon-chroot")
......
...@@ -3,6 +3,8 @@ package chroot ...@@ -3,6 +3,8 @@ package chroot
import ( import (
"errors" "errors"
"fmt" "fmt"
"time"
"github.com/mitchellh/goamz/ec2" "github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common" awscommon "github.com/mitchellh/packer/builder/amazon/common"
...@@ -23,7 +25,9 @@ func (s *StepSnapshot) Run(state multistep.StateBag) multistep.StepAction { ...@@ -23,7 +25,9 @@ func (s *StepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
volumeId := state.Get("volume_id").(string) volumeId := state.Get("volume_id").(string)
ui.Say("Creating snapshot...") ui.Say("Creating snapshot...")
createSnapResp, err := ec2conn.CreateSnapshot(volumeId, "") createSnapResp, err := ec2conn.CreateSnapshot(
volumeId,
fmt.Sprintf("Packer: %s", time.Now().String()))
if err != nil { if err != nil {
err := fmt.Errorf("Error creating snapshot: %s", err) err := fmt.Errorf("Error creating snapshot: %s", err)
state.Put("error", err) state.Put("error", err)
......
...@@ -13,6 +13,7 @@ type AccessConfig struct { ...@@ -13,6 +13,7 @@ type AccessConfig struct {
AccessKey string `mapstructure:"access_key"` AccessKey string `mapstructure:"access_key"`
SecretKey string `mapstructure:"secret_key"` SecretKey string `mapstructure:"secret_key"`
RawRegion string `mapstructure:"region"` RawRegion string `mapstructure:"region"`
Token string `mapstructure:"token"`
} }
// Auth returns a valid aws.Auth object for access to AWS services, or // Auth returns a valid aws.Auth object for access to AWS services, or
...@@ -23,6 +24,10 @@ func (c *AccessConfig) Auth() (aws.Auth, error) { ...@@ -23,6 +24,10 @@ func (c *AccessConfig) Auth() (aws.Auth, error) {
// Store the accesskey and secret that we got... // Store the accesskey and secret that we got...
c.AccessKey = auth.AccessKey c.AccessKey = auth.AccessKey
c.SecretKey = auth.SecretKey c.SecretKey = auth.SecretKey
c.Token = auth.Token
}
if c.Token != "" {
auth.Token = c.Token
} }
return auth, err return auth, err
......
...@@ -37,6 +37,7 @@ func (a *Artifact) Id() string { ...@@ -37,6 +37,7 @@ func (a *Artifact) Id() string {
parts = append(parts, fmt.Sprintf("%s:%s", region, amiId)) parts = append(parts, fmt.Sprintf("%s:%s", region, amiId))
} }
sort.Strings(parts)
return strings.Join(parts, ",") return strings.Join(parts, ",")
} }
...@@ -47,7 +48,7 @@ func (a *Artifact) String() string { ...@@ -47,7 +48,7 @@ func (a *Artifact) String() string {
amiStrings = append(amiStrings, single) amiStrings = append(amiStrings, single)
} }
sort.Sort(sort.StringSlice(amiStrings)) sort.Strings(amiStrings)
return fmt.Sprintf("AMIs were created:\n\n%s", strings.Join(amiStrings, "\n")) return fmt.Sprintf("AMIs were created:\n\n%s", strings.Join(amiStrings, "\n"))
} }
......
package common package common
import ( import (
"fmt"
"github.com/mitchellh/goamz/ec2" "github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/packer/packer"
) )
// BlockDevice // BlockDevice
...@@ -41,6 +44,51 @@ func buildBlockDevices(b []BlockDevice) []ec2.BlockDeviceMapping { ...@@ -41,6 +44,51 @@ func buildBlockDevices(b []BlockDevice) []ec2.BlockDeviceMapping {
return blockDevices return blockDevices
} }
func (b *BlockDevices) Prepare(t *packer.ConfigTemplate) []error {
if t == nil {
var err error
t, err = packer.NewConfigTemplate()
if err != nil {
return []error{err}
}
}
lists := map[string][]BlockDevice{
"ami_block_device_mappings": b.AMIMappings,
"launch_block_device_mappings": b.LaunchMappings,
}
var errs []error
for outer, bds := range lists {
for i, bd := range bds {
templates := map[string]*string{
"device_name": &bd.DeviceName,
"snapshot_id": &bd.SnapshotId,
"virtual_name": &bd.VirtualName,
"volume_type": &bd.VolumeType,
}
errs := make([]error, 0)
for n, ptr := range templates {
var err error
*ptr, err = t.Process(*ptr, nil)
if err != nil {
errs = append(
errs, fmt.Errorf(
"Error processing %s[%d].%s: %s",
outer, i, n, err))
}
}
}
}
if len(errs) > 0 {
return errs
}
return nil
}
func (b *BlockDevices) BuildAMIDevices() []ec2.BlockDeviceMapping { func (b *BlockDevices) BuildAMIDevices() []ec2.BlockDeviceMapping {
return buildBlockDevices(b.AMIMappings) return buildBlockDevices(b.AMIMappings)
} }
......
...@@ -7,38 +7,47 @@ import ( ...@@ -7,38 +7,47 @@ import (
) )
func TestBlockDevice(t *testing.T) { func TestBlockDevice(t *testing.T) {
ec2Mapping := []ec2.BlockDeviceMapping{ cases := []struct {
ec2.BlockDeviceMapping{ Config *BlockDevice
DeviceName: "/dev/sdb", Result *ec2.BlockDeviceMapping
VirtualName: "ephemeral0", }{
SnapshotId: "snap-1234", {
VolumeType: "standard", Config: &BlockDevice{
VolumeSize: 8, DeviceName: "/dev/sdb",
DeleteOnTermination: true, VirtualName: "ephemeral0",
IOPS: 1000, SnapshotId: "snap-1234",
VolumeType: "standard",
VolumeSize: 8,
DeleteOnTermination: true,
IOPS: 1000,
},
Result: &ec2.BlockDeviceMapping{
DeviceName: "/dev/sdb",
VirtualName: "ephemeral0",
SnapshotId: "snap-1234",
VolumeType: "standard",
VolumeSize: 8,
DeleteOnTermination: true,
IOPS: 1000,
},
}, },
} }
blockDevice := BlockDevice{ for _, tc := range cases {
DeviceName: "/dev/sdb", blockDevices := BlockDevices{
VirtualName: "ephemeral0", AMIMappings: []BlockDevice{*tc.Config},
SnapshotId: "snap-1234", LaunchMappings: []BlockDevice{*tc.Config},
VolumeType: "standard", }
VolumeSize: 8,
DeleteOnTermination: true,
IOPS: 1000,
}
blockDevices := BlockDevices{ expected := []ec2.BlockDeviceMapping{*tc.Result}
AMIMappings: []BlockDevice{blockDevice},
LaunchMappings: []BlockDevice{blockDevice},
}
if !reflect.DeepEqual(ec2Mapping, blockDevices.BuildAMIDevices()) { if !reflect.DeepEqual(expected, blockDevices.BuildAMIDevices()) {
t.Fatalf("bad: %#v", ec2Mapping) t.Fatalf("bad: %#v", expected)
} }
if !reflect.DeepEqual(ec2Mapping, blockDevices.BuildLaunchDevices()) { if !reflect.DeepEqual(expected, blockDevices.BuildLaunchDevices()) {
t.Fatalf("bad: %#v", ec2Mapping) t.Fatalf("bad: %#v", expected)
}
} }
} }
...@@ -3,9 +3,11 @@ package common ...@@ -3,9 +3,11 @@ package common
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/mitchellh/packer/packer"
"os" "os"
"time" "time"
"github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/packer"
) )
// RunConfig contains configuration for running an instance from a source // RunConfig contains configuration for running an instance from a source
...@@ -17,9 +19,12 @@ type RunConfig struct { ...@@ -17,9 +19,12 @@ type RunConfig struct {
InstanceType string `mapstructure:"instance_type"` InstanceType string `mapstructure:"instance_type"`
RunTags map[string]string `mapstructure:"run_tags"` RunTags map[string]string `mapstructure:"run_tags"`
SourceAmi string `mapstructure:"source_ami"` SourceAmi string `mapstructure:"source_ami"`
SpotPrice string `mapstructure:"spot_price"`
SpotPriceAutoProduct string `mapstructure:"spot_price_auto_product"`
RawSSHTimeout string `mapstructure:"ssh_timeout"` RawSSHTimeout string `mapstructure:"ssh_timeout"`
SSHUsername string `mapstructure:"ssh_username"` SSHUsername string `mapstructure:"ssh_username"`
SSHPrivateKeyFile string `mapstructure:"ssh_private_key_file"` SSHPrivateKeyFile string `mapstructure:"ssh_private_key_file"`
SSHPrivateIp bool `mapstructure:"ssh_private_ip"`
SSHPort int `mapstructure:"ssh_port"` SSHPort int `mapstructure:"ssh_port"`
SecurityGroupId string `mapstructure:"security_group_id"` SecurityGroupId string `mapstructure:"security_group_id"`
SecurityGroupIds []string `mapstructure:"security_group_ids"` SecurityGroupIds []string `mapstructure:"security_group_ids"`
...@@ -45,6 +50,8 @@ func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error { ...@@ -45,6 +50,8 @@ func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error {
templates := map[string]*string{ templates := map[string]*string{
"iam_instance_profile": &c.IamInstanceProfile, "iam_instance_profile": &c.IamInstanceProfile,
"instance_type": &c.InstanceType, "instance_type": &c.InstanceType,
"spot_price": &c.SpotPrice,
"spot_price_auto_product": &c.SpotPriceAutoProduct,
"ssh_timeout": &c.RawSSHTimeout, "ssh_timeout": &c.RawSSHTimeout,
"ssh_username": &c.SSHUsername, "ssh_username": &c.SSHUsername,
"ssh_private_key_file": &c.SSHPrivateKeyFile, "ssh_private_key_file": &c.SSHPrivateKeyFile,
...@@ -78,7 +85,8 @@ func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error { ...@@ -78,7 +85,8 @@ func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error {
} }
if c.TemporaryKeyPairName == "" { if c.TemporaryKeyPairName == "" {
c.TemporaryKeyPairName = "packer {{uuid}}" c.TemporaryKeyPairName = fmt.Sprintf(
"packer %s", uuid.TimeOrderedUUID())
} }
// Validation // Validation
...@@ -91,6 +99,13 @@ func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error { ...@@ -91,6 +99,13 @@ func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error {
errs = append(errs, errors.New("An instance_type must be specified")) errs = append(errs, errors.New("An instance_type must be specified"))
} }
if c.SpotPrice == "auto" {
if c.SpotPriceAutoProduct == "" {
errs = append(errs, errors.New(
"spot_price_auto_product must be specified when spot_price is auto"))
}
}
if c.SSHUsername == "" { if c.SSHUsername == "" {
errs = append(errs, errors.New("An ssh_username must be specified")) errs = append(errs, errors.New("An ssh_username must be specified"))
} }
......
...@@ -47,6 +47,19 @@ func TestRunConfigPrepare_SourceAmi(t *testing.T) { ...@@ -47,6 +47,19 @@ func TestRunConfigPrepare_SourceAmi(t *testing.T) {
} }
} }
func TestRunConfigPrepare_SpotAuto(t *testing.T) {
c := testConfig()
c.SpotPrice = "auto"
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
}
c.SpotPriceAutoProduct = "foo"
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_SSHPort(t *testing.T) { func TestRunConfigPrepare_SSHPort(t *testing.T) {
c := testConfig() c := testConfig()
c.SSHPort = 0 c.SSHPort = 0
......
...@@ -11,7 +11,7 @@ import ( ...@@ -11,7 +11,7 @@ import (
// SSHAddress returns a function that can be given to the SSH communicator // SSHAddress returns a function that can be given to the SSH communicator
// for determining the SSH address based on the instance DNS name. // for determining the SSH address based on the instance DNS name.
func SSHAddress(e *ec2.EC2, port int) func(multistep.StateBag) (string, error) { func SSHAddress(e *ec2.EC2, port int, private bool) func(multistep.StateBag) (string, error) {
return func(state multistep.StateBag) (string, error) { return func(state multistep.StateBag) (string, error) {
for j := 0; j < 2; j++ { for j := 0; j < 2; j++ {
var host string var host string
...@@ -19,7 +19,7 @@ func SSHAddress(e *ec2.EC2, port int) func(multistep.StateBag) (string, error) { ...@@ -19,7 +19,7 @@ func SSHAddress(e *ec2.EC2, port int) func(multistep.StateBag) (string, error) {
if i.DNSName != "" { if i.DNSName != "" {
host = i.DNSName host = i.DNSName
} else if i.VpcId != "" { } else if i.VpcId != "" {
if i.PublicIpAddress != "" { if i.PublicIpAddress != "" && !private {
host = i.PublicIpAddress host = i.PublicIpAddress
} else { } else {
host = i.PrivateIpAddress host = i.PrivateIpAddress
......
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"github.com/mitchellh/goamz/ec2" "github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"log" "log"
"net"
"time" "time"
) )
...@@ -38,6 +39,9 @@ func AMIStateRefreshFunc(conn *ec2.EC2, imageId string) StateRefreshFunc { ...@@ -38,6 +39,9 @@ func AMIStateRefreshFunc(conn *ec2.EC2, imageId string) StateRefreshFunc {
if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidAMIID.NotFound" { if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidAMIID.NotFound" {
// Set this to nil as if we didn't find anything. // Set this to nil as if we didn't find anything.
resp = nil resp = nil
} else if isTransientNetworkError(err) {
// Transient network error, treat it as if we didn't find anything
resp = nil
} else { } else {
log.Printf("Error on AMIStateRefresh: %s", err) log.Printf("Error on AMIStateRefresh: %s", err)
return nil, "", err return nil, "", err
...@@ -64,6 +68,9 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) StateRefreshFunc { ...@@ -64,6 +68,9 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) StateRefreshFunc {
if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidInstanceID.NotFound" { if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidInstanceID.NotFound" {
// Set this to nil as if we didn't find anything. // Set this to nil as if we didn't find anything.
resp = nil resp = nil
} else if isTransientNetworkError(err) {
// Transient network error, treat it as if we didn't find anything
resp = nil
} else { } else {
log.Printf("Error on InstanceStateRefresh: %s", err) log.Printf("Error on InstanceStateRefresh: %s", err)
return nil, "", err return nil, "", err
...@@ -81,6 +88,35 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) StateRefreshFunc { ...@@ -81,6 +88,35 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) StateRefreshFunc {
} }
} }
// SpotRequestStateRefreshFunc returns a StateRefreshFunc that is used to watch
// a spot request for state changes.
func SpotRequestStateRefreshFunc(conn *ec2.EC2, spotRequestId string) StateRefreshFunc {
return func() (interface{}, string, error) {
resp, err := conn.DescribeSpotRequests([]string{spotRequestId}, ec2.NewFilter())
if err != nil {
if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidSpotInstanceRequestID.NotFound" {
// Set this to nil as if we didn't find anything.
resp = nil
} else if isTransientNetworkError(err) {
// Transient network error, treat it as if we didn't find anything
resp = nil
} else {
log.Printf("Error on SpotRequestStateRefresh: %s", err)
return nil, "", err
}
}
if resp == nil || len(resp.SpotRequestResults) == 0 {
// Sometimes AWS has consistency issues and doesn't see the
// SpotRequest. Return an empty state.
return nil, "", nil
}
i := resp.SpotRequestResults[0]
return i, i.State, nil
}
}
// WaitForState watches an object and waits for it to achieve a certain // WaitForState watches an object and waits for it to achieve a certain
// state. // state.
func WaitForState(conf *StateChangeConf) (i interface{}, err error) { func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
...@@ -125,8 +161,8 @@ func WaitForState(conf *StateChangeConf) (i interface{}, err error) { ...@@ -125,8 +161,8 @@ func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
} }
if !found { if !found {
fmt.Errorf("unexpected state '%s', wanted target '%s'", currentState, conf.Target) err := fmt.Errorf("unexpected state '%s', wanted target '%s'", currentState, conf.Target)
return return nil, err
} }
} }
...@@ -135,3 +171,11 @@ func WaitForState(conf *StateChangeConf) (i interface{}, err error) { ...@@ -135,3 +171,11 @@ func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
return return
} }
func isTransientNetworkError(err error) bool {
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
return true
}
return false
}
...@@ -2,10 +2,14 @@ package common ...@@ -2,10 +2,14 @@ package common
import ( import (
"fmt" "fmt"
"io/ioutil"
"log"
"strconv"
"time"
"github.com/mitchellh/goamz/ec2" "github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"io/ioutil"
) )
type StepRunSourceInstance struct { type StepRunSourceInstance struct {
...@@ -17,12 +21,15 @@ type StepRunSourceInstance struct { ...@@ -17,12 +21,15 @@ type StepRunSourceInstance struct {
InstanceType string InstanceType string
IamInstanceProfile string IamInstanceProfile string
SourceAMI string SourceAMI string
SpotPrice string
SpotPriceProduct string
SubnetId string SubnetId string
Tags map[string]string Tags map[string]string
UserData string UserData string
UserDataFile string UserDataFile string
instance *ec2.Instance instance *ec2.Instance
spotRequest *ec2.SpotRequestResult
} }
func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepAction { func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepAction {
...@@ -47,21 +54,6 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi ...@@ -47,21 +54,6 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi
securityGroups[n] = ec2.SecurityGroup{Id: securityGroupId} securityGroups[n] = ec2.SecurityGroup{Id: securityGroupId}
} }
runOpts := &ec2.RunInstances{
KeyName: keyName,
ImageId: s.SourceAMI,
InstanceType: s.InstanceType,
UserData: []byte(userData),
MinCount: 0,
MaxCount: 0,
SecurityGroups: securityGroups,
IamInstanceProfile: s.IamInstanceProfile,
SubnetId: s.SubnetId,
AssociatePublicIpAddress: s.AssociatePublicIpAddress,
BlockDevices: s.BlockDevices.BuildLaunchDevices(),
AvailZone: s.AvailabilityZone,
}
ui.Say("Launching a source AWS instance...") ui.Say("Launching a source AWS instance...")
imageResp, err := ec2conn.Images([]string{s.SourceAMI}, ec2.NewFilter()) imageResp, err := ec2conn.Images([]string{s.SourceAMI}, ec2.NewFilter())
if err != nil { if err != nil {
...@@ -82,28 +74,136 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi ...@@ -82,28 +74,136 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi
return multistep.ActionHalt return multistep.ActionHalt
} }
runResp, err := ec2conn.RunInstances(runOpts) spotPrice := s.SpotPrice
if err != nil { if spotPrice == "auto" {
err := fmt.Errorf("Error launching source instance: %s", err) ui.Message(fmt.Sprintf(
state.Put("error", err) "Finding spot price for %s %s...",
ui.Error(err.Error()) s.SpotPriceProduct, s.InstanceType))
return multistep.ActionHalt
// Detect the spot price
startTime := time.Now().Add(-1 * time.Hour)
resp, err := ec2conn.DescribeSpotPriceHistory(&ec2.DescribeSpotPriceHistory{
InstanceType: []string{s.InstanceType},
ProductDescription: []string{s.SpotPriceProduct},
AvailabilityZone: s.AvailabilityZone,
StartTime: startTime,
})
if err != nil {
err := fmt.Errorf("Error finding spot price: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
var price float64
for _, history := range resp.History {
log.Printf("[INFO] Candidate spot price: %s", history.SpotPrice)
current, err := strconv.ParseFloat(history.SpotPrice, 64)
if err != nil {
log.Printf("[ERR] Error parsing spot price: %s", err)
continue
}
if price == 0 || current < price {
price = current
}
}
if price == 0 {
err := fmt.Errorf("No candidate spot prices found!")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
spotPrice = strconv.FormatFloat(price, 'f', -1, 64)
} }
s.instance = &runResp.Instances[0] var instanceId string
ui.Message(fmt.Sprintf("Instance ID: %s", s.instance.InstanceId))
ec2Tags := make([]ec2.Tag, 1, len(s.Tags)+1) if spotPrice == "" {
ec2Tags[0] = ec2.Tag{"Name", "Packer Builder"} runOpts := &ec2.RunInstances{
for k, v := range s.Tags { KeyName: keyName,
ec2Tags = append(ec2Tags, ec2.Tag{k, v}) ImageId: s.SourceAMI,
InstanceType: s.InstanceType,
UserData: []byte(userData),
MinCount: 0,
MaxCount: 0,
SecurityGroups: securityGroups,
IamInstanceProfile: s.IamInstanceProfile,
SubnetId: s.SubnetId,
AssociatePublicIpAddress: s.AssociatePublicIpAddress,
BlockDevices: s.BlockDevices.BuildLaunchDevices(),
AvailZone: s.AvailabilityZone,
}
runResp, err := ec2conn.RunInstances(runOpts)
if err != nil {
err := fmt.Errorf("Error launching source instance: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
instanceId = runResp.Instances[0].InstanceId
} else {
ui.Message(fmt.Sprintf(
"Requesting spot instance '%s' for: %s",
s.InstanceType, spotPrice))
runOpts := &ec2.RequestSpotInstances{
SpotPrice: spotPrice,
KeyName: keyName,
ImageId: s.SourceAMI,
InstanceType: s.InstanceType,
UserData: []byte(userData),
SecurityGroups: securityGroups,
IamInstanceProfile: s.IamInstanceProfile,
SubnetId: s.SubnetId,
AssociatePublicIpAddress: s.AssociatePublicIpAddress,
BlockDevices: s.BlockDevices.BuildLaunchDevices(),
AvailZone: s.AvailabilityZone,
}
runSpotResp, err := ec2conn.RequestSpotInstances(runOpts)
if err != nil {
err := fmt.Errorf("Error launching source spot instance: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
s.spotRequest = &runSpotResp.SpotRequestResults[0]
spotRequestId := s.spotRequest.SpotRequestId
ui.Message(fmt.Sprintf("Waiting for spot request (%s) to become active...", spotRequestId))
stateChange := StateChangeConf{
Pending: []string{"open"},
Target: "active",
Refresh: SpotRequestStateRefreshFunc(ec2conn, spotRequestId),
StepState: state,
}
_, err = WaitForState(&stateChange)
if err != nil {
err := fmt.Errorf("Error waiting for spot request (%s) to become ready: %s", spotRequestId, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
spotResp, err := ec2conn.DescribeSpotRequests([]string{spotRequestId}, nil)
if err != nil {
err := fmt.Errorf("Error finding spot request (%s): %s", spotRequestId, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
instanceId = spotResp.SpotRequestResults[0].InstanceId
} }
_, err = ec2conn.CreateTags([]string{s.instance.InstanceId}, ec2Tags) instanceResp, err := ec2conn.Instances([]string{instanceId}, nil)
if err != nil { if err != nil {
ui.Message( err := fmt.Errorf("Error finding source instance (%s): %s", instanceId, err)
fmt.Sprintf("Failed to tag a Name on the builder instance: %s", 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.Say(fmt.Sprintf("Waiting for instance (%s) to become ready...", s.instance.InstanceId))
stateChange := StateChangeConf{ stateChange := StateChangeConf{
...@@ -122,6 +222,18 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi ...@@ -122,6 +222,18 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi
s.instance = latestInstance.(*ec2.Instance) s.instance = latestInstance.(*ec2.Instance)
ec2Tags := make([]ec2.Tag, 1, len(s.Tags)+1)
ec2Tags[0] = ec2.Tag{"Name", "Packer Builder"}
for k, v := range s.Tags {
ec2Tags = append(ec2Tags, ec2.Tag{k, v})
}
_, err = ec2conn.CreateTags([]string{s.instance.InstanceId}, ec2Tags)
if err != nil {
ui.Message(
fmt.Sprintf("Failed to tag a Name on the builder instance: %s", err))
}
if s.Debug { if s.Debug {
if s.instance.DNSName != "" { if s.instance.DNSName != "" {
ui.Message(fmt.Sprintf("Public DNS: %s", s.instance.DNSName)) ui.Message(fmt.Sprintf("Public DNS: %s", s.instance.DNSName))
...@@ -142,24 +254,41 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi ...@@ -142,24 +254,41 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi
} }
func (s *StepRunSourceInstance) Cleanup(state multistep.StateBag) { func (s *StepRunSourceInstance) Cleanup(state multistep.StateBag) {
if s.instance == nil {
return
}
ec2conn := state.Get("ec2").(*ec2.EC2) ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
ui.Say("Terminating the source AWS instance...") // Cancel the spot request if it exists
if _, err := ec2conn.TerminateInstances([]string{s.instance.InstanceId}); err != nil { if s.spotRequest != nil {
ui.Error(fmt.Sprintf("Error terminating instance, may still be around: %s", err)) ui.Say("Cancelling the spot request...")
return if _, err := ec2conn.CancelSpotRequests([]string{s.spotRequest.SpotRequestId}); err != nil {
} ui.Error(fmt.Sprintf("Error cancelling the spot request, may still be around: %s", err))
return
}
stateChange := StateChangeConf{
Pending: []string{"active", "open"},
Refresh: SpotRequestStateRefreshFunc(ec2conn, s.spotRequest.SpotRequestId),
Target: "cancelled",
}
WaitForState(&stateChange)
stateChange := StateChangeConf{
Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"},
Refresh: InstanceStateRefreshFunc(ec2conn, s.instance),
Target: "terminated",
} }
WaitForState(&stateChange) // Terminate the source instance if it exists
if s.instance != nil {
ui.Say("Terminating the source AWS instance...")
if _, err := ec2conn.TerminateInstances([]string{s.instance.InstanceId}); err != nil {
ui.Error(fmt.Sprintf("Error terminating instance, may still be around: %s", err))
return
}
stateChange := StateChangeConf{
Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"},
Refresh: InstanceStateRefreshFunc(ec2conn, s.instance),
Target: "terminated",
}
WaitForState(&stateChange)
}
} }
...@@ -50,6 +50,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { ...@@ -50,6 +50,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
// Accumulate any errors // Accumulate any errors
errs := common.CheckUnusedConfig(md) errs := common.CheckUnusedConfig(md)
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(b.config.tpl)...) errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(b.config.tpl)...)
errs = packer.MultiErrorAppend(errs, b.config.BlockDevices.Prepare(b.config.tpl)...)
errs = packer.MultiErrorAppend(errs, b.config.AMIConfig.Prepare(b.config.tpl)...) errs = packer.MultiErrorAppend(errs, b.config.AMIConfig.Prepare(b.config.tpl)...)
errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare(b.config.tpl)...) errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare(b.config.tpl)...)
...@@ -101,6 +102,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -101,6 +102,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&awscommon.StepRunSourceInstance{ &awscommon.StepRunSourceInstance{
Debug: b.config.PackerDebug, Debug: b.config.PackerDebug,
ExpectedRootDevice: "ebs", ExpectedRootDevice: "ebs",
SpotPrice: b.config.SpotPrice,
SpotPriceProduct: b.config.SpotPriceAutoProduct,
InstanceType: b.config.InstanceType, InstanceType: b.config.InstanceType,
UserData: b.config.UserData, UserData: b.config.UserData,
UserDataFile: b.config.UserDataFile, UserDataFile: b.config.UserDataFile,
...@@ -113,12 +116,14 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -113,12 +116,14 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Tags: b.config.RunTags, Tags: b.config.RunTags,
}, },
&common.StepConnectSSH{ &common.StepConnectSSH{
SSHAddress: awscommon.SSHAddress(ec2conn, b.config.SSHPort), SSHAddress: awscommon.SSHAddress(
ec2conn, b.config.SSHPort, b.config.SSHPrivateIp),
SSHConfig: awscommon.SSHConfig(b.config.SSHUsername), SSHConfig: awscommon.SSHConfig(b.config.SSHUsername),
SSHWaitTimeout: b.config.SSHTimeout(), SSHWaitTimeout: b.config.SSHTimeout(),
}, },
&common.StepProvision{}, &common.StepProvision{},
&stepStopInstance{}, &stepStopInstance{SpotPrice: b.config.SpotPrice},
// TODO(mitchellh): verify works with spots
&stepModifyInstance{}, &stepModifyInstance{},
&stepCreateAMI{}, &stepCreateAMI{},
&awscommon.StepAMIRegionCopy{ &awscommon.StepAMIRegionCopy{
......
...@@ -8,7 +8,9 @@ import ( ...@@ -8,7 +8,9 @@ import (
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
type stepCreateAMI struct{} type stepCreateAMI struct {
image *ec2.Image
}
func (s *stepCreateAMI) Run(state multistep.StateBag) multistep.StepAction { func (s *stepCreateAMI) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(config) config := state.Get("config").(config)
...@@ -54,9 +56,38 @@ func (s *stepCreateAMI) Run(state multistep.StateBag) multistep.StepAction { ...@@ -54,9 +56,38 @@ func (s *stepCreateAMI) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionHalt return multistep.ActionHalt
} }
imagesResp, err := ec2conn.Images([]string{createResp.ImageId}, nil)
if err != nil {
err := fmt.Errorf("Error searching for AMI: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
s.image = &imagesResp.Images[0]
return multistep.ActionContinue return multistep.ActionContinue
} }
func (s *stepCreateAMI) Cleanup(multistep.StateBag) { func (s *stepCreateAMI) Cleanup(state multistep.StateBag) {
// No cleanup... if s.image == nil {
return
}
_, cancelled := state.GetOk(multistep.StateCancelled)
_, halted := state.GetOk(multistep.StateHalted)
if !cancelled && !halted {
return
}
ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui)
ui.Say("Deregistering the AMI because cancelation or error...")
if resp, err := ec2conn.DeregisterImage(s.image.Id); err != nil {
ui.Error(fmt.Sprintf("Error deregistering AMI, may still be around: %s", err))
return
} else if resp.Return == false {
ui.Error(fmt.Sprintf("Error deregistering AMI, may still be around: %s", resp.Return))
return
}
} }
...@@ -8,13 +8,20 @@ import ( ...@@ -8,13 +8,20 @@ import (
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
type stepStopInstance struct{} type stepStopInstance struct {
SpotPrice string
}
func (s *stepStopInstance) Run(state multistep.StateBag) multistep.StepAction { func (s *stepStopInstance) Run(state multistep.StateBag) multistep.StepAction {
ec2conn := state.Get("ec2").(*ec2.EC2) ec2conn := state.Get("ec2").(*ec2.EC2)
instance := state.Get("instance").(*ec2.Instance) instance := state.Get("instance").(*ec2.Instance)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
// Skip when it is a spot instance
if s.SpotPrice != "" {
return multistep.ActionContinue
}
// Stop the instance so we can create an AMI from it // Stop the instance so we can create an AMI from it
ui.Say("Stopping the source instance...") ui.Say("Stopping the source instance...")
_, err := ec2conn.StopInstances(instance.InstanceId) _, err := ec2conn.StopInstances(instance.InstanceId)
......
...@@ -88,7 +88,8 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { ...@@ -88,7 +88,8 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
"-e {{.PrivatePath}}/* " + "-e {{.PrivatePath}}/* " +
"-d {{.Destination}} " + "-d {{.Destination}} " +
"-p {{.Prefix}} " + "-p {{.Prefix}} " +
"--batch" "--batch " +
"--no-filter"
} }
if b.config.X509UploadPath == "" { if b.config.X509UploadPath == "" {
...@@ -98,6 +99,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { ...@@ -98,6 +99,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
// Accumulate any errors // Accumulate any errors
errs := common.CheckUnusedConfig(md) errs := common.CheckUnusedConfig(md)
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(b.config.tpl)...) errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(b.config.tpl)...)
errs = packer.MultiErrorAppend(errs, b.config.BlockDevices.Prepare(b.config.tpl)...)
errs = packer.MultiErrorAppend(errs, b.config.AMIConfig.Prepare(b.config.tpl)...) errs = packer.MultiErrorAppend(errs, b.config.AMIConfig.Prepare(b.config.tpl)...)
errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare(b.config.tpl)...) errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare(b.config.tpl)...)
...@@ -204,6 +206,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -204,6 +206,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}, },
&awscommon.StepRunSourceInstance{ &awscommon.StepRunSourceInstance{
Debug: b.config.PackerDebug, Debug: b.config.PackerDebug,
SpotPrice: b.config.SpotPrice,
SpotPriceProduct: b.config.SpotPriceAutoProduct,
InstanceType: b.config.InstanceType, InstanceType: b.config.InstanceType,
IamInstanceProfile: b.config.IamInstanceProfile, IamInstanceProfile: b.config.IamInstanceProfile,
UserData: b.config.UserData, UserData: b.config.UserData,
...@@ -216,7 +220,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -216,7 +220,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Tags: b.config.RunTags, Tags: b.config.RunTags,
}, },
&common.StepConnectSSH{ &common.StepConnectSSH{
SSHAddress: awscommon.SSHAddress(ec2conn, b.config.SSHPort), SSHAddress: awscommon.SSHAddress(
ec2conn, b.config.SSHPort, b.config.SSHPrivateIp),
SSHConfig: awscommon.SSHConfig(b.config.SSHUsername), SSHConfig: awscommon.SSHConfig(b.config.SSHUsername),
SSHWaitTimeout: b.config.SSHTimeout(), SSHWaitTimeout: b.config.SSHTimeout(),
}, },
......
...@@ -45,5 +45,5 @@ func (s *StepUploadX509Cert) uploadSingle(comm packer.Communicator, dst, src str ...@@ -45,5 +45,5 @@ func (s *StepUploadX509Cert) uploadSingle(comm packer.Communicator, dst, src str
} }
defer f.Close() defer f.Close()
return comm.Upload(dst, f) return comm.Upload(dst, f, nil)
} }
...@@ -8,6 +8,7 @@ import ( ...@@ -8,6 +8,7 @@ import (
) )
const BuilderId = "packer.docker" const BuilderId = "packer.docker"
const BuilderIdImport = "packer.post-processor.docker-import"
type Builder struct { type Builder struct {
config *Config config *Config
...@@ -35,7 +36,12 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -35,7 +36,12 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&StepPull{}, &StepPull{},
&StepRun{}, &StepRun{},
&StepProvision{}, &StepProvision{},
&StepExport{}, }
if b.config.Commit {
steps = append(steps, new(StepCommit))
} else {
steps = append(steps, new(StepExport))
} }
// Setup the state bag and initial state for the steps // Setup the state bag and initial state for the steps
...@@ -64,8 +70,17 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -64,8 +70,17 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
return nil, rawErr.(error) return nil, rawErr.(error)
} }
var artifact packer.Artifact
// No errors, must've worked // No errors, must've worked
artifact := &ExportArtifact{path: b.config.ExportPath} if b.config.Commit {
artifact = &ImportArtifact{
IdValue: state.Get("image_id").(string),
BuilderIdValue: BuilderIdImport,
Driver: driver,
}
} else {
artifact = &ExportArtifact{path: b.config.ExportPath}
}
return artifact, nil return artifact, nil
} }
......
...@@ -3,8 +3,6 @@ package docker ...@@ -3,8 +3,6 @@ package docker
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"github.com/ActiveState/tail"
"github.com/mitchellh/packer/packer"
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
...@@ -15,6 +13,9 @@ import ( ...@@ -15,6 +13,9 @@ import (
"sync" "sync"
"syscall" "syscall"
"time" "time"
"github.com/ActiveState/tail"
"github.com/mitchellh/packer/packer"
) )
type Communicator struct { type Communicator struct {
...@@ -56,7 +57,7 @@ func (c *Communicator) Start(remote *packer.RemoteCmd) error { ...@@ -56,7 +57,7 @@ func (c *Communicator) Start(remote *packer.RemoteCmd) error {
return nil return nil
} }
func (c *Communicator) Upload(dst string, src io.Reader) error { func (c *Communicator) Upload(dst string, src io.Reader, fi *os.FileInfo) error {
// Create a temporary file to store the upload // Create a temporary file to store the upload
tempfile, err := ioutil.TempFile(c.HostDir, "upload") tempfile, err := ioutil.TempFile(c.HostDir, "upload")
if err != nil { if err != nil {
...@@ -231,20 +232,42 @@ func (c *Communicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin_w io.W ...@@ -231,20 +232,42 @@ func (c *Communicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin_w io.W
stdin_w.Write([]byte(remoteCmd + "\n")) stdin_w.Write([]byte(remoteCmd + "\n"))
}() }()
// Start a goroutine to read all the lines out of the logs // Start a goroutine to read all the lines out of the logs. These channels
// allow us to stop the go-routine and wait for it to be stopped.
stopTailCh := make(chan struct{})
doneCh := make(chan struct{})
go func() { go func() {
for line := range tail.Lines { defer close(doneCh)
if remote.Stdout != nil {
remote.Stdout.Write([]byte(line.Text + "\n")) for {
} else { select {
log.Printf("Command stdout: %#v", line.Text) case <-tail.Dead():
return
case line := <-tail.Lines:
if remote.Stdout != nil {
remote.Stdout.Write([]byte(line.Text + "\n"))
} else {
log.Printf("Command stdout: %#v", line.Text)
}
case <-time.After(2 * time.Second):
// If we're done, then return. Otherwise, keep grabbing
// data. This gives us a chance to flush all the lines
// out of the tailed file.
select {
case <-stopTailCh:
return
default:
}
} }
} }
}() }()
var exitRaw []byte
var exitStatus int
var exitStatusRaw int64
err = cmd.Wait() err = cmd.Wait()
if exitErr, ok := err.(*exec.ExitError); ok { if exitErr, ok := err.(*exec.ExitError); ok {
exitStatus := 1 exitStatus = 1
// There is no process-independent way to get the REAL // There is no process-independent way to get the REAL
// exit status so we just try to go deeper. // exit status so we just try to go deeper.
...@@ -254,8 +277,7 @@ func (c *Communicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin_w io.W ...@@ -254,8 +277,7 @@ func (c *Communicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin_w io.W
// Say that we ended, since if Docker itself failed, then // Say that we ended, since if Docker itself failed, then
// the command must've not run, or so we assume // the command must've not run, or so we assume
remote.SetExited(exitStatus) goto REMOTE_EXIT
return
} }
// Wait for the exit code to appear in our file... // Wait for the exit code to appear in our file...
...@@ -270,21 +292,27 @@ func (c *Communicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin_w io.W ...@@ -270,21 +292,27 @@ func (c *Communicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin_w io.W
} }
// Read the exit code // Read the exit code
exitRaw, err := ioutil.ReadFile(exitCodePath) exitRaw, err = ioutil.ReadFile(exitCodePath)
if err != nil { if err != nil {
log.Printf("Error executing: %s", err) log.Printf("Error executing: %s", err)
remote.SetExited(254) exitStatus = 254
return goto REMOTE_EXIT
} }
exitStatus, err := strconv.ParseInt(string(bytes.TrimSpace(exitRaw)), 10, 0) exitStatusRaw, err = strconv.ParseInt(string(bytes.TrimSpace(exitRaw)), 10, 0)
if err != nil { if err != nil {
log.Printf("Error executing: %s", err) log.Printf("Error executing: %s", err)
remote.SetExited(254) exitStatus = 254
return goto REMOTE_EXIT
} }
exitStatus = int(exitStatusRaw)
log.Printf("Executed command exit status: %d", exitStatus) log.Printf("Executed command exit status: %d", exitStatus)
// Finally, we're done REMOTE_EXIT:
remote.SetExited(int(exitStatus)) // Wait for the tail to finish
close(stopTailCh)
<-doneCh
// Set the exit status which triggers waiters
remote.SetExited(exitStatus)
} }
...@@ -9,10 +9,18 @@ import ( ...@@ -9,10 +9,18 @@ import (
type Config struct { type Config struct {
common.PackerConfig `mapstructure:",squash"` common.PackerConfig `mapstructure:",squash"`
Commit bool
ExportPath string `mapstructure:"export_path"` ExportPath string `mapstructure:"export_path"`
Image string Image string
Pull bool Pull bool
RunCommand []string `mapstructure:"run_command"` RunCommand []string `mapstructure:"run_command"`
Volumes map[string]string
Login bool
LoginEmail string `mapstructure:"login_email"`
LoginUsername string `mapstructure:"login_username"`
LoginPassword string `mapstructure:"login_password"`
LoginServer string `mapstructure:"login_server"`
tpl *packer.ConfigTemplate tpl *packer.ConfigTemplate
} }
...@@ -34,9 +42,7 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { ...@@ -34,9 +42,7 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
// Defaults // Defaults
if len(c.RunCommand) == 0 { if len(c.RunCommand) == 0 {
c.RunCommand = []string{ c.RunCommand = []string{
"run",
"-d", "-i", "-t", "-d", "-i", "-t",
"-v", "{{.Volumes}}",
"{{.Image}}", "{{.Image}}",
"/bin/bash", "/bin/bash",
} }
...@@ -58,8 +64,12 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { ...@@ -58,8 +64,12 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
errs := common.CheckUnusedConfig(md) errs := common.CheckUnusedConfig(md)
templates := map[string]*string{ templates := map[string]*string{
"export_path": &c.ExportPath, "export_path": &c.ExportPath,
"image": &c.Image, "image": &c.Image,
"login_email": &c.LoginEmail,
"login_username": &c.LoginUsername,
"login_password": &c.LoginPassword,
"login_server": &c.LoginServer,
} }
for n, ptr := range templates { for n, ptr := range templates {
...@@ -71,9 +81,15 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { ...@@ -71,9 +81,15 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
} }
} }
if c.ExportPath == "" { for k, v := range c.Volumes {
errs = packer.MultiErrorAppend(errs, var err error
fmt.Errorf("export_path must be specified")) v, err = c.tpl.Process(v, nil)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error processing volumes[%s]: %s", k, err))
}
c.Volumes[k] = v
} }
if c.Image == "" { if c.Image == "" {
...@@ -81,6 +97,11 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { ...@@ -81,6 +97,11 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
fmt.Errorf("image must be specified")) fmt.Errorf("image must be specified"))
} }
if c.ExportPath != "" && c.Commit {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("both commit and export_path cannot be set"))
}
if errs != nil && len(errs.Errors) > 0 { if errs != nil && len(errs.Errors) > 0 {
return nil, nil, errs return nil, nil, errs
} }
......
...@@ -47,7 +47,7 @@ func TestConfigPrepare_exportPath(t *testing.T) { ...@@ -47,7 +47,7 @@ func TestConfigPrepare_exportPath(t *testing.T) {
// No export path // No export path
delete(raw, "export_path") delete(raw, "export_path")
_, warns, errs := NewConfig(raw) _, warns, errs := NewConfig(raw)
testConfigErr(t, warns, errs) testConfigOk(t, warns, errs)
// Good export path // Good export path
raw["export_path"] = "good" raw["export_path"] = "good"
...@@ -55,6 +55,20 @@ func TestConfigPrepare_exportPath(t *testing.T) { ...@@ -55,6 +55,20 @@ func TestConfigPrepare_exportPath(t *testing.T) {
testConfigOk(t, warns, errs) testConfigOk(t, warns, errs)
} }
func TestConfigPrepare_exportPathAndCommit(t *testing.T) {
raw := testConfig()
raw["commit"] = true
// No export path
_, warns, errs := NewConfig(raw)
testConfigErr(t, warns, errs)
// No commit
raw["commit"] = false
_, warns, errs = NewConfig(raw)
testConfigOk(t, warns, errs)
}
func TestConfigPrepare_image(t *testing.T) { func TestConfigPrepare_image(t *testing.T) {
raw := testConfig() raw := testConfig()
......
...@@ -8,6 +8,9 @@ import ( ...@@ -8,6 +8,9 @@ import (
// Docker. The Driver interface also allows the steps to be tested since // Docker. The Driver interface also allows the steps to be tested since
// a mock driver can be shimmed in. // a mock driver can be shimmed in.
type Driver interface { type Driver interface {
// Commit the container to a tag
Commit(id string) (string, error)
// Delete an image that is imported into Docker // Delete an image that is imported into Docker
DeleteImage(id string) error DeleteImage(id string) error
...@@ -17,12 +20,22 @@ type Driver interface { ...@@ -17,12 +20,22 @@ type Driver interface {
// Import imports a container from a tar file // Import imports a container from a tar file
Import(path, repo string) (string, error) Import(path, repo string) (string, error)
// 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
// Logout. This can only be called if Login succeeded.
Logout(repo string) error
// Pull should pull down the given image. // Pull should pull down the given image.
Pull(image string) error Pull(image string) error
// Push pushes an image to a Docker index/registry. // Push pushes an image to a Docker index/registry.
Push(name string) error Push(name string) error
// Save an image with the given ID to the given writer.
SaveImage(id string, dst io.Writer) error
// StartContainer starts a container and returns the ID for that container, // StartContainer starts a container and returns the ID for that container,
// along with a potential error. // along with a potential error.
StartContainer(*ContainerConfig) (string, error) StartContainer(*ContainerConfig) (string, error)
...@@ -30,6 +43,9 @@ type Driver interface { ...@@ -30,6 +43,9 @@ type Driver interface {
// StopContainer forcibly stops a container. // StopContainer forcibly stops a container.
StopContainer(id string) error StopContainer(id string) error
// TagImage tags the image with the given ID
TagImage(id string, repo string) error
// Verify verifies that the driver can run // Verify verifies that the driver can run
Verify() error Verify() error
} }
...@@ -43,6 +59,5 @@ type ContainerConfig struct { ...@@ -43,6 +59,5 @@ type ContainerConfig struct {
// This is the template that is used for the RunCommand in the ContainerConfig. // This is the template that is used for the RunCommand in the ContainerConfig.
type startContainerTemplate struct { type startContainerTemplate struct {
Image string Image string
Volumes string
} }
...@@ -3,17 +3,21 @@ package docker ...@@ -3,17 +3,21 @@ package docker
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"github.com/mitchellh/packer/packer"
"io" "io"
"log" "log"
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
"sync"
"github.com/mitchellh/packer/packer"
) )
type DockerDriver struct { type DockerDriver struct {
Ui packer.Ui Ui packer.Ui
Tpl *packer.ConfigTemplate Tpl *packer.ConfigTemplate
l sync.Mutex
} }
func (d *DockerDriver) DeleteImage(id string) error { func (d *DockerDriver) DeleteImage(id string) error {
...@@ -35,6 +39,27 @@ func (d *DockerDriver) DeleteImage(id string) error { ...@@ -35,6 +39,27 @@ func (d *DockerDriver) DeleteImage(id string) error {
return nil return nil
} }
func (d *DockerDriver) Commit(id string) (string, error) {
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd := exec.Command("docker", "commit", id)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Start(); err != nil {
return "", err
}
if err := cmd.Wait(); err != nil {
err = fmt.Errorf("Error committing container: %s\nStderr: %s",
err, stderr.String())
return "", err
}
return strings.TrimSpace(stdout.String()), nil
}
func (d *DockerDriver) Export(id string, dst io.Writer) error { func (d *DockerDriver) Export(id string, dst io.Writer) error {
var stderr bytes.Buffer var stderr bytes.Buffer
cmd := exec.Command("docker", "export", id) cmd := exec.Command("docker", "export", id)
...@@ -88,6 +113,44 @@ func (d *DockerDriver) Import(path string, repo string) (string, error) { ...@@ -88,6 +113,44 @@ func (d *DockerDriver) Import(path string, repo string) (string, error) {
return strings.TrimSpace(stdout.String()), nil return strings.TrimSpace(stdout.String()), nil
} }
func (d *DockerDriver) Login(repo, email, user, pass string) error {
d.l.Lock()
args := []string{"login"}
if email != "" {
args = append(args, "-e", email)
}
if user != "" {
args = append(args, "-u", user)
}
if pass != "" {
args = append(args, "-p", pass)
}
if repo != "" {
args = append(args, repo)
}
cmd := exec.Command("docker", args...)
err := runAndStream(cmd, d.Ui)
if err != nil {
d.l.Unlock()
}
return err
}
func (d *DockerDriver) Logout(repo string) error {
args := []string{"logout"}
if repo != "" {
args = append(args, repo)
}
cmd := exec.Command("docker", args...)
err := runAndStream(cmd, d.Ui)
d.l.Unlock()
return err
}
func (d *DockerDriver) Pull(image string) error { func (d *DockerDriver) Pull(image string) error {
cmd := exec.Command("docker", "pull", image) cmd := exec.Command("docker", "pull", image)
return runAndStream(cmd, d.Ui) return runAndStream(cmd, d.Ui)
...@@ -98,27 +161,43 @@ func (d *DockerDriver) Push(name string) error { ...@@ -98,27 +161,43 @@ func (d *DockerDriver) Push(name string) error {
return runAndStream(cmd, d.Ui) return runAndStream(cmd, d.Ui)
} }
func (d *DockerDriver) SaveImage(id string, dst io.Writer) error {
var stderr bytes.Buffer
cmd := exec.Command("docker", "save", id)
cmd.Stdout = dst
cmd.Stderr = &stderr
log.Printf("Exporting image: %s", id)
if err := cmd.Start(); err != nil {
return err
}
if err := cmd.Wait(); err != nil {
err = fmt.Errorf("Error exporting: %s\nStderr: %s",
err, stderr.String())
return err
}
return nil
}
func (d *DockerDriver) StartContainer(config *ContainerConfig) (string, error) { func (d *DockerDriver) StartContainer(config *ContainerConfig) (string, error) {
// Build up the template data // Build up the template data
var tplData startContainerTemplate var tplData startContainerTemplate
tplData.Image = config.Image tplData.Image = config.Image
if len(config.Volumes) > 0 {
volumes := make([]string, 0, len(config.Volumes))
for host, guest := range config.Volumes {
volumes = append(volumes, fmt.Sprintf("%s:%s", host, guest))
}
tplData.Volumes = strings.Join(volumes, ",")
}
// Args that we're going to pass to Docker // Args that we're going to pass to Docker
args := config.RunCommand args := []string{"run"}
for i, v := range args { for host, guest := range config.Volumes {
var err error args = append(args, "-v", fmt.Sprintf("%s:%s", host, guest))
args[i], err = d.Tpl.Process(v, &tplData) }
for _, v := range config.RunCommand {
v, err := d.Tpl.Process(v, &tplData)
if err != nil { if err != nil {
return "", err return "", err
} }
args = append(args, v)
} }
d.Ui.Message(fmt.Sprintf( d.Ui.Message(fmt.Sprintf(
"Run command: docker %s", strings.Join(args, " "))) "Run command: docker %s", strings.Join(args, " ")))
...@@ -149,7 +228,29 @@ func (d *DockerDriver) StartContainer(config *ContainerConfig) (string, error) { ...@@ -149,7 +228,29 @@ func (d *DockerDriver) StartContainer(config *ContainerConfig) (string, error) {
} }
func (d *DockerDriver) StopContainer(id string) error { func (d *DockerDriver) StopContainer(id string) error {
return exec.Command("docker", "kill", id).Run() if err := exec.Command("docker", "kill", id).Run(); err != nil {
return err
}
return exec.Command("docker", "rm", id).Run()
}
func (d *DockerDriver) TagImage(id string, repo string) error {
var stderr bytes.Buffer
cmd := exec.Command("docker", "tag", id, repo)
cmd.Stderr = &stderr
if err := cmd.Start(); err != nil {
return err
}
if err := cmd.Wait(); err != nil {
err = fmt.Errorf("Error tagging image: %s\nStderr: %s",
err, stderr.String())
return err
}
return nil
} }
func (d *DockerDriver) Verify() error { func (d *DockerDriver) Verify() error {
......
...@@ -6,6 +6,11 @@ import ( ...@@ -6,6 +6,11 @@ import (
// MockDriver is a driver implementation that can be used for tests. // MockDriver is a driver implementation that can be used for tests.
type MockDriver struct { type MockDriver struct {
CommitCalled bool
CommitContainerId string
CommitImageId string
CommitErr error
DeleteImageCalled bool DeleteImageCalled bool
DeleteImageId string DeleteImageId string
DeleteImageErr error DeleteImageErr error
...@@ -16,10 +21,31 @@ type MockDriver struct { ...@@ -16,10 +21,31 @@ type MockDriver struct {
ImportId string ImportId string
ImportErr error ImportErr error
LoginCalled bool
LoginEmail string
LoginUsername string
LoginPassword string
LoginRepo string
LoginErr error
LogoutCalled bool
LogoutRepo string
LogoutErr error
PushCalled bool PushCalled bool
PushName string PushName string
PushErr error PushErr error
SaveImageCalled bool
SaveImageId string
SaveImageReader io.Reader
SaveImageError error
TagImageCalled bool
TagImageImageId string
TagImageRepo string
TagImageErr error
ExportReader io.Reader ExportReader io.Reader
ExportError error ExportError error
PullError error PullError error
...@@ -39,6 +65,12 @@ type MockDriver struct { ...@@ -39,6 +65,12 @@ type MockDriver struct {
VerifyCalled bool VerifyCalled bool
} }
func (d *MockDriver) Commit(id string) (string, error) {
d.CommitCalled = true
d.CommitContainerId = id
return d.CommitImageId, d.CommitErr
}
func (d *MockDriver) DeleteImage(id string) error { func (d *MockDriver) DeleteImage(id string) error {
d.DeleteImageCalled = true d.DeleteImageCalled = true
d.DeleteImageId = id d.DeleteImageId = id
...@@ -66,6 +98,21 @@ func (d *MockDriver) Import(path, repo string) (string, error) { ...@@ -66,6 +98,21 @@ func (d *MockDriver) Import(path, repo string) (string, error) {
return d.ImportId, d.ImportErr return d.ImportId, d.ImportErr
} }
func (d *MockDriver) Login(r, e, u, p string) error {
d.LoginCalled = true
d.LoginRepo = r
d.LoginEmail = e
d.LoginUsername = u
d.LoginPassword = p
return d.LoginErr
}
func (d *MockDriver) Logout(r string) error {
d.LogoutCalled = true
d.LogoutRepo = r
return d.LogoutErr
}
func (d *MockDriver) Pull(image string) error { func (d *MockDriver) Pull(image string) error {
d.PullCalled = true d.PullCalled = true
d.PullImage = image d.PullImage = image
...@@ -78,6 +125,20 @@ func (d *MockDriver) Push(name string) error { ...@@ -78,6 +125,20 @@ func (d *MockDriver) Push(name string) error {
return d.PushErr return d.PushErr
} }
func (d *MockDriver) SaveImage(id string, dst io.Writer) error {
d.SaveImageCalled = true
d.SaveImageId = id
if d.SaveImageReader != nil {
_, err := io.Copy(dst, d.SaveImageReader)
if err != nil {
return err
}
}
return d.SaveImageError
}
func (d *MockDriver) StartContainer(config *ContainerConfig) (string, error) { func (d *MockDriver) StartContainer(config *ContainerConfig) (string, error) {
d.StartCalled = true d.StartCalled = true
d.StartConfig = config d.StartConfig = config
...@@ -90,6 +151,13 @@ func (d *MockDriver) StopContainer(id string) error { ...@@ -90,6 +151,13 @@ func (d *MockDriver) StopContainer(id string) error {
return d.StopError return d.StopError
} }
func (d *MockDriver) TagImage(id string, repo string) error {
d.TagImageCalled = true
d.TagImageImageId = id
d.TagImageRepo = repo
return d.TagImageErr
}
func (d *MockDriver) Verify() error { func (d *MockDriver) Verify() error {
d.VerifyCalled = true d.VerifyCalled = true
return d.VerifyError return d.VerifyError
......
package docker
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
// StepCommit commits the container to a image.
type StepCommit struct {
imageId string
}
func (s *StepCommit) Run(state multistep.StateBag) multistep.StepAction {
driver := state.Get("driver").(Driver)
containerId := state.Get("container_id").(string)
ui := state.Get("ui").(packer.Ui)
ui.Say("Committing the container")
imageId, err := driver.Commit(containerId)
if err != nil {
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Save the container ID
s.imageId = imageId
state.Put("image_id", s.imageId)
ui.Message(fmt.Sprintf("Image ID: %s", s.imageId))
return multistep.ActionContinue
}
func (s *StepCommit) Cleanup(state multistep.StateBag) {}
package docker
import (
"errors"
"github.com/mitchellh/multistep"
"testing"
)
func testStepCommitState(t *testing.T) multistep.StateBag {
state := testState(t)
state.Put("container_id", "foo")
return state
}
func TestStepCommit_impl(t *testing.T) {
var _ multistep.Step = new(StepCommit)
}
func TestStepCommit(t *testing.T) {
state := testStepCommitState(t)
step := new(StepCommit)
defer step.Cleanup(state)
driver := state.Get("driver").(*MockDriver)
driver.CommitImageId = "bar"
// run the step
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
// verify we did the right thing
if !driver.CommitCalled {
t.Fatal("should've called")
}
// verify the ID is saved
idRaw, ok := state.GetOk("image_id")
if !ok {
t.Fatal("should've saved ID")
}
id := idRaw.(string)
if id != driver.CommitImageId {
t.Fatalf("bad: %#v", id)
}
}
func TestStepCommit_error(t *testing.T) {
state := testStepCommitState(t)
step := new(StepCommit)
defer step.Cleanup(state)
driver := state.Get("driver").(*MockDriver)
driver.CommitErr = errors.New("foo")
// run the step
if action := step.Run(state); action != multistep.ActionHalt {
t.Fatalf("bad action: %#v", action)
}
// verify the ID is not saved
if _, ok := state.GetOk("image_id"); ok {
t.Fatal("shouldn't save image ID")
}
}
...@@ -12,6 +12,7 @@ type StepExport struct{} ...@@ -12,6 +12,7 @@ type StepExport struct{}
func (s *StepExport) Run(state multistep.StateBag) multistep.StepAction { func (s *StepExport) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config) config := state.Get("config").(*Config)
driver := state.Get("driver").(Driver) driver := state.Get("driver").(Driver)
containerId := state.Get("container_id").(string) containerId := state.Get("container_id").(string)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
......
...@@ -20,6 +20,29 @@ func (s *StepPull) Run(state multistep.StateBag) multistep.StepAction { ...@@ -20,6 +20,29 @@ func (s *StepPull) Run(state multistep.StateBag) multistep.StepAction {
} }
ui.Say(fmt.Sprintf("Pulling Docker image: %s", config.Image)) ui.Say(fmt.Sprintf("Pulling Docker image: %s", config.Image))
if config.Login {
ui.Message("Logging in...")
err := driver.Login(
config.LoginServer,
config.LoginEmail,
config.LoginUsername,
config.LoginPassword)
if err != nil {
err := fmt.Errorf("Error logging in: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
defer func() {
ui.Message("Logging out...")
if err := driver.Logout(config.LoginServer); err != nil {
ui.Error(fmt.Sprintf("Error logging out: %s", err))
}
}()
}
if err := driver.Pull(config.Image); err != nil { if err := driver.Pull(config.Image); err != nil {
err := fmt.Errorf("Error pulling Docker image: %s", err) err := fmt.Errorf("Error pulling Docker image: %s", err)
state.Put("error", err) state.Put("error", err)
......
...@@ -51,6 +51,35 @@ func TestStepPull_error(t *testing.T) { ...@@ -51,6 +51,35 @@ func TestStepPull_error(t *testing.T) {
} }
} }
func TestStepPull_login(t *testing.T) {
state := testState(t)
step := new(StepPull)
defer step.Cleanup(state)
config := state.Get("config").(*Config)
driver := state.Get("driver").(*MockDriver)
config.Login = true
// run the step
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
// verify we pulled
if !driver.PullCalled {
t.Fatal("should've pulled")
}
// verify we logged in
if !driver.LoginCalled {
t.Fatal("should've logged in")
}
if !driver.LogoutCalled {
t.Fatal("should've logged out")
}
}
func TestStepPull_noPull(t *testing.T) { func TestStepPull_noPull(t *testing.T) {
state := testState(t) state := testState(t)
step := new(StepPull) step := new(StepPull)
......
...@@ -19,11 +19,14 @@ func (s *StepRun) Run(state multistep.StateBag) multistep.StepAction { ...@@ -19,11 +19,14 @@ func (s *StepRun) Run(state multistep.StateBag) multistep.StepAction {
runConfig := ContainerConfig{ runConfig := ContainerConfig{
Image: config.Image, Image: config.Image,
RunCommand: config.RunCommand, RunCommand: config.RunCommand,
Volumes: map[string]string{ Volumes: make(map[string]string),
tempDir: "/packer-files",
},
} }
for host, container := range config.Volumes {
runConfig.Volumes[host] = container
}
runConfig.Volumes[tempDir] = "/packer-files"
ui.Say("Starting docker container...") ui.Say("Starting docker container...")
containerId, err := driver.StartContainer(&runConfig) containerId, err := driver.StartContainer(&runConfig)
if err != nil { if err != nil {
......
...@@ -2,11 +2,19 @@ package googlecompute ...@@ -2,11 +2,19 @@ package googlecompute
import ( import (
"encoding/json" "encoding/json"
"io/ioutil" "os"
) )
// clientSecrets represents the client secrets of a GCE service account. // accountFile represents the structure of the account file JSON file.
type clientSecrets struct { type accountFile struct {
PrivateKeyId string `json:"private_key_id"`
PrivateKey string `json:"private_key"`
ClientEmail string `json:"client_email"`
ClientId string `json:"client_id"`
}
// clientSecretsFile represents the structure of the client secrets JSON file.
type clientSecretsFile struct {
Web struct { Web struct {
AuthURI string `json:"auth_uri"` AuthURI string `json:"auth_uri"`
ClientEmail string `json:"client_email"` ClientEmail string `json:"client_email"`
...@@ -15,18 +23,13 @@ type clientSecrets struct { ...@@ -15,18 +23,13 @@ type clientSecrets struct {
} }
} }
// loadClientSecrets loads the GCE client secrets file identified by path. func loadJSON(result interface{}, path string) error {
func loadClientSecrets(path string) (*clientSecrets, error) { f, err := os.Open(path)
var cs *clientSecrets
secretBytes, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
err = json.Unmarshal(secretBytes, &cs)
if err != nil { if err != nil {
return nil, err return err
} }
defer f.Close()
return cs, nil dec := json.NewDecoder(f)
return dec.Decode(result)
} }
...@@ -35,7 +35,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { ...@@ -35,7 +35,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
// representing a GCE machine image. // representing a GCE machine image.
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
driver, err := NewDriverGCE( driver, err := NewDriverGCE(
ui, b.config.ProjectId, b.config.clientSecrets, b.config.privateKeyBytes) ui, b.config.ProjectId, &b.config.account, &b.config.clientSecrets)
if err != nil { if err != nil {
return nil, err return nil, err
} }
......
package googlecompute
import (
"io/ioutil"
"testing"
)
func testClientSecretsFile(t *testing.T) string {
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
defer tf.Close()
if _, err := tf.Write([]byte(testClientSecretsContent)); err != nil {
t.Fatalf("err: %s", err)
}
return tf.Name()
}
func TestLoadClientSecrets(t *testing.T) {
_, err := loadClientSecrets(testClientSecretsFile(t))
if err != nil {
t.Fatalf("err: %s", err)
}
}
// This is just some dummy data that doesn't actually work (it was revoked
// a long time ago).
const testClientSecretsContent = `{"web":{"auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://accounts.google.com/o/oauth2/token","client_email":"774313886706-eorlsj0r4eqkh5e7nvea5fuf59ifr873@developer.gserviceaccount.com","client_x509_cert_url":"https://www.googleapis.com/robot/v1/metadata/x509/774313886706-eorlsj0r4eqkh5e7nvea5fuf59ifr873@developer.gserviceaccount.com","client_id":"774313886706-eorlsj0r4eqkh5e7nvea5fuf59ifr873.apps.googleusercontent.com","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs"}}`
...@@ -16,8 +16,11 @@ import ( ...@@ -16,8 +16,11 @@ import (
type Config struct { type Config struct {
common.PackerConfig `mapstructure:",squash"` common.PackerConfig `mapstructure:",squash"`
AccountFile string `mapstructure:"account_file"`
ClientSecretsFile string `mapstructure:"client_secrets_file"`
ProjectId string `mapstructure:"project_id"`
BucketName string `mapstructure:"bucket_name"` BucketName string `mapstructure:"bucket_name"`
ClientSecretsFile string `mapstructure:"client_secrets_file"`
DiskSizeGb int64 `mapstructure:"disk_size"` DiskSizeGb int64 `mapstructure:"disk_size"`
ImageName string `mapstructure:"image_name"` ImageName string `mapstructure:"image_name"`
ImageDescription string `mapstructure:"image_description"` ImageDescription string `mapstructure:"image_description"`
...@@ -25,9 +28,6 @@ type Config struct { ...@@ -25,9 +28,6 @@ type Config struct {
MachineType string `mapstructure:"machine_type"` MachineType string `mapstructure:"machine_type"`
Metadata map[string]string `mapstructure:"metadata"` Metadata map[string]string `mapstructure:"metadata"`
Network string `mapstructure:"network"` Network string `mapstructure:"network"`
Passphrase string `mapstructure:"passphrase"`
PrivateKeyFile string `mapstructure:"private_key_file"`
ProjectId string `mapstructure:"project_id"`
SourceImage string `mapstructure:"source_image"` SourceImage string `mapstructure:"source_image"`
SourceImageProjectId string `mapstructure:"source_image_project_id"` SourceImageProjectId string `mapstructure:"source_image_project_id"`
SSHUsername string `mapstructure:"ssh_username"` SSHUsername string `mapstructure:"ssh_username"`
...@@ -37,7 +37,8 @@ type Config struct { ...@@ -37,7 +37,8 @@ type Config struct {
Tags []string `mapstructure:"tags"` Tags []string `mapstructure:"tags"`
Zone string `mapstructure:"zone"` Zone string `mapstructure:"zone"`
clientSecrets *clientSecrets account accountFile
clientSecrets clientSecretsFile
instanceName string instanceName string
privateKeyBytes []byte privateKeyBytes []byte
sshTimeout time.Duration sshTimeout time.Duration
...@@ -104,15 +105,15 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { ...@@ -104,15 +105,15 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
// Process Templates // Process Templates
templates := map[string]*string{ templates := map[string]*string{
"account_file": &c.AccountFile,
"client_secrets_file": &c.ClientSecretsFile,
"bucket_name": &c.BucketName, "bucket_name": &c.BucketName,
"client_secrets_file": &c.ClientSecretsFile,
"image_name": &c.ImageName, "image_name": &c.ImageName,
"image_description": &c.ImageDescription, "image_description": &c.ImageDescription,
"instance_name": &c.InstanceName, "instance_name": &c.InstanceName,
"machine_type": &c.MachineType, "machine_type": &c.MachineType,
"network": &c.Network, "network": &c.Network,
"passphrase": &c.Passphrase,
"private_key_file": &c.PrivateKeyFile,
"project_id": &c.ProjectId, "project_id": &c.ProjectId,
"source_image": &c.SourceImage, "source_image": &c.SourceImage,
"source_image_project_id": &c.SourceImageProjectId, "source_image_project_id": &c.SourceImageProjectId,
...@@ -137,14 +138,14 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { ...@@ -137,14 +138,14 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
errs, errors.New("a bucket_name must be specified")) errs, errors.New("a bucket_name must be specified"))
} }
if c.ClientSecretsFile == "" { if c.AccountFile == "" {
errs = packer.MultiErrorAppend( errs = packer.MultiErrorAppend(
errs, errors.New("a client_secrets_file must be specified")) errs, errors.New("an account_file must be specified"))
} }
if c.PrivateKeyFile == "" { if c.ClientSecretsFile == "" {
errs = packer.MultiErrorAppend( errs = packer.MultiErrorAppend(
errs, errors.New("a private_key_file must be specified")) errs, errors.New("a client_secrets_file must be specified"))
} }
if c.ProjectId == "" { if c.ProjectId == "" {
...@@ -177,22 +178,17 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { ...@@ -177,22 +178,17 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
} }
c.stateTimeout = stateTimeout c.stateTimeout = stateTimeout
if c.ClientSecretsFile != "" { if c.AccountFile != "" {
// Load the client secrets file. if err := loadJSON(&c.account, c.AccountFile); err != nil {
cs, err := loadClientSecrets(c.ClientSecretsFile)
if err != nil {
errs = packer.MultiErrorAppend( errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing client secrets file: %s", err)) errs, fmt.Errorf("Failed parsing account file: %s", err))
} }
c.clientSecrets = cs
} }
if c.PrivateKeyFile != "" { if c.ClientSecretsFile != "" {
// Load the private key. if err := loadJSON(&c.clientSecrets, c.ClientSecretsFile); err != nil {
c.privateKeyBytes, err = processPrivateKeyFile(c.PrivateKeyFile, c.Passphrase)
if err != nil {
errs = packer.MultiErrorAppend( errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed loading private key file: %s", err)) errs, fmt.Errorf("Failed parsing client secrets file: %s", err))
} }
} }
......
package googlecompute package googlecompute
import ( import (
"io/ioutil"
"testing" "testing"
) )
func testConfig(t *testing.T) map[string]interface{} { func testConfig(t *testing.T) map[string]interface{} {
return map[string]interface{}{ return map[string]interface{}{
"account_file": testAccountFile(t),
"bucket_name": "foo", "bucket_name": "foo",
"client_secrets_file": testClientSecretsFile(t), "client_secrets_file": testClientSecretsFile(t),
"private_key_file": testPrivateKeyFile(t),
"project_id": "hashicorp", "project_id": "hashicorp",
"source_image": "foo", "source_image": "foo",
"zone": "us-east-1a", "zone": "us-east-1a",
...@@ -84,16 +85,6 @@ func TestConfigPrepare(t *testing.T) { ...@@ -84,16 +85,6 @@ func TestConfigPrepare(t *testing.T) {
true, true,
}, },
{
"private_key_file",
nil,
true,
},
{
"private_key_file",
testPrivateKeyFile(t),
false,
},
{ {
"private_key_file", "private_key_file",
"/tmp/i/should/not/exist", "/tmp/i/should/not/exist",
...@@ -174,3 +165,37 @@ func TestConfigPrepare(t *testing.T) { ...@@ -174,3 +165,37 @@ func TestConfigPrepare(t *testing.T) {
} }
} }
} }
func testAccountFile(t *testing.T) string {
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
defer tf.Close()
if _, err := tf.Write([]byte(testAccountContent)); err != nil {
t.Fatalf("err: %s", err)
}
return tf.Name()
}
func testClientSecretsFile(t *testing.T) string {
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
defer tf.Close()
if _, err := tf.Write([]byte(testClientSecretsContent)); err != nil {
t.Fatalf("err: %s", err)
}
return tf.Name()
}
// This is just some dummy data that doesn't actually work (it was revoked
// a long time ago).
const testAccountContent = `{}`
const testClientSecretsContent = `{"web":{"auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://accounts.google.com/o/oauth2/token","client_email":"774313886706-eorlsj0r4eqkh5e7nvea5fuf59ifr873@developer.gserviceaccount.com","client_x509_cert_url":"https://www.googleapis.com/robot/v1/metadata/x509/774313886706-eorlsj0r4eqkh5e7nvea5fuf59ifr873@developer.gserviceaccount.com","client_id":"774313886706-eorlsj0r4eqkh5e7nvea5fuf59ifr873.apps.googleusercontent.com","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs"}}`
...@@ -23,22 +23,27 @@ type driverGCE struct { ...@@ -23,22 +23,27 @@ type driverGCE struct {
const DriverScopes string = "https://www.googleapis.com/auth/compute " + const DriverScopes string = "https://www.googleapis.com/auth/compute " +
"https://www.googleapis.com/auth/devstorage.full_control" "https://www.googleapis.com/auth/devstorage.full_control"
func NewDriverGCE(ui packer.Ui, projectId string, c *clientSecrets, key []byte) (Driver, error) { func NewDriverGCE(ui packer.Ui, p string, a *accountFile, c *clientSecretsFile) (Driver, error) {
log.Printf("[INFO] Requesting token...") // Get the token for use in our requests
log.Printf("[INFO] -- Email: %s", c.Web.ClientEmail) log.Printf("[INFO] Requesting Google token...")
log.Printf("[INFO] -- Email: %s", a.ClientEmail)
log.Printf("[INFO] -- Scopes: %s", DriverScopes) log.Printf("[INFO] -- Scopes: %s", DriverScopes)
log.Printf("[INFO] -- Private Key Length: %d", len(key)) log.Printf("[INFO] -- Private Key Length: %d", len(a.PrivateKey))
log.Printf("[INFO] -- Token URL: %s", c.Web.TokenURI) log.Printf("[INFO] -- Token URL: %s", c.Web.TokenURI)
jwtTok := jwt.NewToken(c.Web.ClientEmail, DriverScopes, key) jwtTok := jwt.NewToken(
a.ClientEmail,
DriverScopes,
[]byte(a.PrivateKey))
jwtTok.ClaimSet.Aud = c.Web.TokenURI jwtTok.ClaimSet.Aud = c.Web.TokenURI
token, err := jwtTok.Assert(new(http.Client)) token, err := jwtTok.Assert(new(http.Client))
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("Error retrieving auth token: %s", err)
} }
// Instantiate the transport to communicate to Google
transport := &oauth.Transport{ transport := &oauth.Transport{
Config: &oauth.Config{ Config: &oauth.Config{
ClientId: c.Web.ClientId, ClientId: a.ClientId,
Scope: DriverScopes, Scope: DriverScopes,
TokenURL: c.Web.TokenURI, TokenURL: c.Web.TokenURI,
AuthURL: c.Web.AuthURI, AuthURL: c.Web.AuthURI,
...@@ -46,14 +51,14 @@ func NewDriverGCE(ui packer.Ui, projectId string, c *clientSecrets, key []byte) ...@@ -46,14 +51,14 @@ func NewDriverGCE(ui packer.Ui, projectId string, c *clientSecrets, key []byte)
Token: token, Token: token,
} }
log.Printf("[INFO] Instantiating client...") log.Printf("[INFO] Instantiating GCE client...")
service, err := compute.New(transport.Client()) service, err := compute.New(transport.Client())
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &driverGCE{ return &driverGCE{
projectId: projectId, projectId: p,
service: service, service: service,
ui: ui, ui: ui,
}, nil }, nil
......
...@@ -24,6 +24,25 @@ func (config *Config) getImage() Image { ...@@ -24,6 +24,25 @@ func (config *Config) getImage() Image {
return Image{Name: config.SourceImage, ProjectId: project} return Image{Name: config.SourceImage, ProjectId: project}
} }
func (config *Config) getInstanceMetadata(sshPublicKey string) map[string]string {
instanceMetadata := make(map[string]string)
// Copy metadata from config
for k, v := range config.Metadata {
instanceMetadata[k] = v
}
// Merge any existing ssh keys with our public key
sshMetaKey := "sshKeys"
sshKeys := fmt.Sprintf("%s:%s", config.SSHUsername, sshPublicKey)
if confSshKeys, exists := instanceMetadata[sshMetaKey]; exists {
sshKeys = fmt.Sprintf("%s\n%s", sshKeys, confSshKeys)
}
instanceMetadata[sshMetaKey] = sshKeys
return instanceMetadata
}
// Run executes the Packer build step that creates a GCE instance. // Run executes the Packer build step that creates a GCE instance.
func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction { func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config) config := state.Get("config").(*Config)
...@@ -39,9 +58,7 @@ func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction ...@@ -39,9 +58,7 @@ func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction
DiskSizeGb: config.DiskSizeGb, DiskSizeGb: config.DiskSizeGb,
Image: config.getImage(), Image: config.getImage(),
MachineType: config.MachineType, MachineType: config.MachineType,
Metadata: map[string]string{ Metadata: config.getInstanceMetadata(sshPublicKey),
"sshKeys": fmt.Sprintf("%s:%s", config.SSHUsername, sshPublicKey),
},
Name: name, Name: name,
Network: config.Network, Network: config.Network,
Tags: config.Tags, Tags: config.Tags,
......
...@@ -5,11 +5,12 @@ import ( ...@@ -5,11 +5,12 @@ import (
"fmt" "fmt"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"strings" "strings"
"github.com/mitchellh/gophercloud-fork-40444fb"
) )
// AccessConfig is for common configuration related to openstack access // AccessConfig is for common configuration related to openstack access
......
...@@ -2,8 +2,9 @@ package openstack ...@@ -2,8 +2,9 @@ package openstack
import ( import (
"fmt" "fmt"
"github.com/rackspace/gophercloud"
"log" "log"
"github.com/mitchellh/gophercloud-fork-40444fb"
) )
// Artifact is an artifact implementation that contains built images. // Artifact is an artifact implementation that contains built images.
......
...@@ -8,8 +8,9 @@ import ( ...@@ -8,8 +8,9 @@ import (
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud"
"log" "log"
"github.com/mitchellh/gophercloud-fork-40444fb"
) )
// The unique ID for this builder // The unique ID for this builder
......
...@@ -5,9 +5,10 @@ import ( ...@@ -5,9 +5,10 @@ import (
"fmt" "fmt"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/racker/perigee" "github.com/racker/perigee"
"github.com/rackspace/gophercloud"
"log" "log"
"time" "time"
"github.com/mitchellh/gophercloud-fork-40444fb"
) )
// StateRefreshFunc is a function type used for StateChangeConf that is // StateRefreshFunc is a function type used for StateChangeConf that is
......
...@@ -5,8 +5,9 @@ import ( ...@@ -5,8 +5,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/rackspace/gophercloud"
"time" "time"
"github.com/mitchellh/gophercloud-fork-40444fb"
) )
// SSHAddress returns a function that can be given to the SSH communicator // SSHAddress returns a function that can be given to the SSH communicator
......
...@@ -4,7 +4,8 @@ import ( ...@@ -4,7 +4,8 @@ import (
"fmt" "fmt"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud"
"github.com/mitchellh/gophercloud-fork-40444fb"
) )
type StepAllocateIp struct { type StepAllocateIp struct {
......
...@@ -4,9 +4,10 @@ import ( ...@@ -4,9 +4,10 @@ import (
"fmt" "fmt"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud"
"log" "log"
"time" "time"
"github.com/mitchellh/gophercloud-fork-40444fb"
) )
type stepCreateImage struct{} type stepCreateImage struct{}
......
...@@ -5,10 +5,11 @@ import ( ...@@ -5,10 +5,11 @@ import (
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common/uuid" "github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud"
"log" "log"
"os" "os"
"runtime" "runtime"
"github.com/mitchellh/gophercloud-fork-40444fb"
) )
type StepKeyPair struct { type StepKeyPair struct {
......
...@@ -4,8 +4,9 @@ import ( ...@@ -4,8 +4,9 @@ import (
"fmt" "fmt"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/rackspace/gophercloud"
"log" "log"
"github.com/mitchellh/gophercloud-fork-40444fb"
) )
type StepRunSourceServer struct { type StepRunSourceServer struct {
......
...@@ -15,8 +15,11 @@ import ( ...@@ -15,8 +15,11 @@ import (
// versions out of the builder steps, so sometimes the methods are // versions out of the builder steps, so sometimes the methods are
// extremely specific. // extremely specific.
type Driver interface { type Driver interface {
// Adds new CD/DVD drive to the VM and returns name of this device
DeviceAddCdRom(string, string) (string, error)
// Import a VM // Import a VM
Import(string, string, string) error Import(string, string, string, bool) error
// Checks if the VM with the given name is running. // Checks if the VM with the given name is running.
IsRunning(string) (bool, error) IsRunning(string) (bool, error)
...@@ -38,7 +41,7 @@ type Driver interface { ...@@ -38,7 +41,7 @@ type Driver interface {
// Version reads the version of Parallels that is installed. // Version reads the version of Parallels that is installed.
Version() (string, error) Version() (string, error)
// Send scancodes to the vm using the prltype tool. // Send scancodes to the vm using the prltype python script.
SendKeyScanCodes(string, ...string) error SendKeyScanCodes(string, ...string) error
// Finds the MAC address of the NIC nic0 // Finds the MAC address of the NIC nic0
......
...@@ -3,6 +3,7 @@ package common ...@@ -3,6 +3,7 @@ package common
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io/ioutil"
"log" "log"
"os" "os"
"os/exec" "os/exec"
...@@ -19,7 +20,7 @@ type Parallels9Driver struct { ...@@ -19,7 +20,7 @@ type Parallels9Driver struct {
PrlctlPath string PrlctlPath string
} }
func (d *Parallels9Driver) Import(name, srcPath, dstDir string) error { func (d *Parallels9Driver) Import(name, srcPath, dstDir string, reassignMac bool) error {
err := d.Prlctl("register", srcPath, "--preserve-uuid") err := d.Prlctl("register", srcPath, "--preserve-uuid")
if err != nil { if err != nil {
...@@ -31,9 +32,12 @@ func (d *Parallels9Driver) Import(name, srcPath, dstDir string) error { ...@@ -31,9 +32,12 @@ func (d *Parallels9Driver) Import(name, srcPath, dstDir string) error {
return err return err
} }
srcMac, err := getFirtsMacAddress(srcPath) srcMac := "auto"
if err != nil { if !reassignMac {
return err srcMac, err = getFirtsMacAddress(srcPath)
if err != nil {
return err
}
} }
err = d.Prlctl("clone", srcId, "--name", name, "--dst", dstDir) err = d.Prlctl("clone", srcId, "--name", name, "--dst", dstDir)
...@@ -91,6 +95,29 @@ func getAppPath(bundleId string) (string, error) { ...@@ -91,6 +95,29 @@ func getAppPath(bundleId string) (string, error) {
return pathOutput, nil return pathOutput, nil
} }
func (d *Parallels9Driver) DeviceAddCdRom(name string, image string) (string, error) {
command := []string{
"set", name,
"--device-add", "cdrom",
"--image", image,
}
out, err := exec.Command(d.PrlctlPath, command...).Output()
if err != nil {
return "", err
}
deviceRe := regexp.MustCompile(`\s+(cdrom\d+)\s+`)
matches := deviceRe.FindStringSubmatch(string(out))
if matches == nil {
return "", fmt.Errorf(
"Could not determine cdrom device name in the output:\n%s", string(out))
}
device_name := matches[1]
return device_name, nil
}
func (d *Parallels9Driver) IsRunning(name string) (bool, error) { func (d *Parallels9Driver) IsRunning(name string) (bool, error) {
var stdout bytes.Buffer var stdout bytes.Buffer
...@@ -179,11 +206,29 @@ func (d *Parallels9Driver) Version() (string, error) { ...@@ -179,11 +206,29 @@ func (d *Parallels9Driver) Version() (string, error) {
func (d *Parallels9Driver) SendKeyScanCodes(vmName string, codes ...string) error { func (d *Parallels9Driver) SendKeyScanCodes(vmName string, codes ...string) error {
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
if codes == nil || len(codes) == 0 {
log.Printf("No scan codes to send")
return nil
}
f, err := ioutil.TempFile("", "prltype")
if err != nil {
return err
}
defer os.Remove(f.Name())
script := []byte(Prltype)
_, err = f.Write(script)
if err != nil {
return err
}
args := prepend(vmName, codes) args := prepend(vmName, codes)
cmd := exec.Command("prltype", args...) args = prepend(f.Name(), args)
cmd := exec.Command("/usr/bin/python", args...)
cmd.Stdout = &stdout cmd.Stdout = &stdout
cmd.Stderr = &stderr cmd.Stderr = &stderr
err := cmd.Run() err = cmd.Run()
stdoutString := strings.TrimSpace(stdout.String()) stdoutString := strings.TrimSpace(stdout.String())
stderrString := strings.TrimSpace(stderr.String()) stderrString := strings.TrimSpace(stderr.String())
......
...@@ -5,6 +5,12 @@ import "sync" ...@@ -5,6 +5,12 @@ import "sync"
type DriverMock struct { type DriverMock struct {
sync.Mutex sync.Mutex
DeviceAddCdRomCalled bool
DeviceAddCdRomName string
DeviceAddCdRomImage string
DeviceAddCdRomResult string
DeviceAddCdRomErr error
ImportCalled bool ImportCalled bool
ImportName string ImportName string
ImportSrcPath string ImportSrcPath string
...@@ -45,7 +51,14 @@ type DriverMock struct { ...@@ -45,7 +51,14 @@ type DriverMock struct {
IpAddressError error IpAddressError error
} }
func (d *DriverMock) Import(name, srcPath, dstPath string) error { func (d *DriverMock) DeviceAddCdRom(name string, image string) (string, error) {
d.DeviceAddCdRomCalled = true
d.DeviceAddCdRomName = name
d.DeviceAddCdRomImage = image
return d.DeviceAddCdRomResult, d.DeviceAddCdRomErr
}
func (d *DriverMock) Import(name, srcPath, dstPath string, reassignMac bool) error {
d.ImportCalled = true d.ImportCalled = true
d.ImportName = name d.ImportName = name
d.ImportSrcPath = srcPath d.ImportSrcPath = srcPath
......
package common
const Prltype string = `
import sys
import prlsdkapi
##
def main():
if len(sys.argv) < 3:
print "Usage: prltype VM_NAME SCANCODE..."
sys.exit(1)
vm_name = sys.argv[1]
scancodes = sys.argv[2:]
server = login()
vm, vm_io = connect(server, vm_name)
send(scancodes, vm, vm_io)
disconnect(server, vm, vm_io)
##
def login():
prlsdkapi.prlsdk.InitializeSDK(prlsdkapi.prlsdk.consts.PAM_DESKTOP_MAC)
server = prlsdkapi.Server()
login_job=server.login_local()
login_job.wait()
return server
##
def connect(server, vm_name):
vm_list_job = server.get_vm_list()
result = vm_list_job.wait()
vm_list = [result.get_param_by_index(i) for i in range(result.get_params_count())]
vm = [vm for vm in vm_list if vm.get_name() == vm_name]
if not vm:
vm_names = [vm.get_name() for vm in vm_list]
raise Exception("%s: No such VM. Available VM's are:\n%s" % (vm_name, "\n".join(vm_names)))
vm = vm[0]
vm_io = prlsdkapi.VmIO()
vm_io.connect_to_vm(vm).wait()
return (vm, vm_io)
##
def disconnect(server, vm, vm_io):
if vm and vm_io:
vm_io.disconnect_from_vm(vm)
if server:
server.logoff()
prlsdkapi.deinit_sdk
##
def send(scancodes, vm, vm_io):
timeout = 10
consts = prlsdkapi.prlsdk.consts
for scancode in scancodes:
c = int(scancode, 16)
if (c < 128):
vm_io.send_key_event(vm, (c,), consts.PKE_PRESS, timeout)
else:
vm_io.send_key_event(vm, (c - 128,) , consts.PKE_RELEASE, timeout)
##
if __name__ == "__main__":
main()
`
package common package common
import ( import (
"code.google.com/p/go.crypto/ssh"
"fmt" "fmt"
"code.google.com/p/go.crypto/ssh"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
commonssh "github.com/mitchellh/packer/common/ssh"
packerssh "github.com/mitchellh/packer/communicator/ssh" packerssh "github.com/mitchellh/packer/communicator/ssh"
"io/ioutil"
"os"
) )
func SSHAddress(state multistep.StateBag) (string, error) { func SSHAddress(state multistep.StateBag) (string, error) {
...@@ -35,7 +35,7 @@ func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*ssh.ClientConfig ...@@ -35,7 +35,7 @@ func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*ssh.ClientConfig
} }
if config.SSHKeyPath != "" { if config.SSHKeyPath != "" {
signer, err := sshKeyToSigner(config.SSHKeyPath) signer, err := commonssh.FileSigner(config.SSHKeyPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -49,23 +49,3 @@ func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*ssh.ClientConfig ...@@ -49,23 +49,3 @@ func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*ssh.ClientConfig
}, nil }, nil
} }
} }
func sshKeyToSigner(path string) (ssh.Signer, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
keyBytes, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
signer, err := ssh.ParsePrivateKey(keyBytes)
if err != nil {
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
}
return signer, nil
}
...@@ -3,9 +3,11 @@ package common ...@@ -3,9 +3,11 @@ package common
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/mitchellh/packer/packer"
"os" "os"
"time" "time"
commonssh "github.com/mitchellh/packer/common/ssh"
"github.com/mitchellh/packer/packer"
) )
type SSHConfig struct { type SSHConfig struct {
...@@ -46,7 +48,7 @@ func (c *SSHConfig) Prepare(t *packer.ConfigTemplate) []error { ...@@ -46,7 +48,7 @@ func (c *SSHConfig) Prepare(t *packer.ConfigTemplate) []error {
if c.SSHKeyPath != "" { if c.SSHKeyPath != "" {
if _, err := os.Stat(c.SSHKeyPath); err != nil { if _, err := os.Stat(c.SSHKeyPath); err != nil {
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err)) errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
} else if _, err := sshKeyToSigner(c.SSHKeyPath); err != nil { } else if _, err := commonssh.FileSigner(c.SSHKeyPath); err != nil {
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err)) errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
} }
} }
......
...@@ -39,11 +39,10 @@ func (s *StepAttachFloppy) Run(state multistep.StateBag) multistep.StepAction { ...@@ -39,11 +39,10 @@ func (s *StepAttachFloppy) Run(state multistep.StateBag) multistep.StepAction {
"set", vmName, "set", vmName,
"--device-del", "fdd0", "--device-del", "fdd0",
} }
if err := driver.Prlctl(del_command...); err != nil { // This will almost certainly fail with 'The fdd0 device does not exist.'
state.Put("error", fmt.Errorf("Error deleting floppy: %s", err)) driver.Prlctl(del_command...)
}
ui.Say("Attaching floppy disk...")
ui.Say("Attaching floppy disk...")
// Attaching the floppy disk // Attaching the floppy disk
add_command := []string{ add_command := []string{
"set", vmName, "set", vmName,
......
...@@ -17,8 +17,8 @@ import ( ...@@ -17,8 +17,8 @@ import (
// vmName string // vmName string
// //
// Produces: // Produces:
// attachedToolsIso boolean
type StepAttachParallelsTools struct { type StepAttachParallelsTools struct {
cdromDevice string
ParallelsToolsMode string ParallelsToolsMode string
} }
...@@ -37,27 +37,25 @@ func (s *StepAttachParallelsTools) Run(state multistep.StateBag) multistep.StepA ...@@ -37,27 +37,25 @@ func (s *StepAttachParallelsTools) Run(state multistep.StateBag) multistep.StepA
parallelsToolsPath := state.Get("parallels_tools_path").(string) parallelsToolsPath := state.Get("parallels_tools_path").(string)
// Attach the guest additions to the computer // Attach the guest additions to the computer
ui.Say("Attaching Parallels Tools ISO onto IDE controller...") ui.Say("Attaching Parallels Tools ISO to the new CD/DVD drive...")
command := []string{
"set", vmName, cdrom, err := driver.DeviceAddCdRom(vmName, parallelsToolsPath)
"--device-add", "cdrom",
"--image", parallelsToolsPath, if err != nil {
} err := fmt.Errorf("Error attaching Parallels Tools ISO: %s", err)
if err := driver.Prlctl(command...); err != nil {
err := fmt.Errorf("Error attaching Parallels Tools: %s", err)
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
} }
// Set some state so we know to remove // Track the device name so that we can can delete later
state.Put("attachedToolsIso", true) s.cdromDevice = cdrom
return multistep.ActionContinue return multistep.ActionContinue
} }
func (s *StepAttachParallelsTools) Cleanup(state multistep.StateBag) { func (s *StepAttachParallelsTools) Cleanup(state multistep.StateBag) {
if _, ok := state.GetOk("attachedToolsIso"); !ok { if s.cdromDevice == "" {
return return
} }
...@@ -66,14 +64,10 @@ func (s *StepAttachParallelsTools) Cleanup(state multistep.StateBag) { ...@@ -66,14 +64,10 @@ func (s *StepAttachParallelsTools) Cleanup(state multistep.StateBag) {
vmName := state.Get("vmName").(string) vmName := state.Get("vmName").(string)
log.Println("Detaching Parallels Tools ISO...") log.Println("Detaching Parallels Tools ISO...")
cdDevice := "cdrom0"
if _, ok := state.GetOk("attachedIso"); ok {
cdDevice = "cdrom1"
}
command := []string{ command := []string{
"set", vmName, "set", vmName,
"--device-del", cdDevice, "--device-del", s.cdromDevice,
} }
if err := driver.Prlctl(command...); err != nil { if err := driver.Prlctl(command...); err != nil {
......
package common
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
// This step removes any devices (floppy disks, ISOs, etc.) from the
// machine that we may have added.
//
// Uses:
// driver Driver
// ui packer.Ui
// vmName string
//
// Produces:
type StepRemoveDevices struct{}
func (s *StepRemoveDevices) Run(state multistep.StateBag) multistep.StepAction {
driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui)
vmName := state.Get("vmName").(string)
// Remove the attached floppy disk, if it exists
if _, ok := state.GetOk("floppy_path"); ok {
ui.Message("Removing floppy drive...")
command := []string{"set", vmName, "--device-del", "fdd0"}
if err := driver.Prlctl(command...); err != nil {
err := fmt.Errorf("Error removing floppy: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
if _, ok := state.GetOk("attachedIso"); ok {
command := []string{
"set", vmName,
"--device-set", "cdrom0",
"--device", "Default CD/DVD-ROM",
}
if err := driver.Prlctl(command...); err != nil {
err := fmt.Errorf("Error detaching ISO: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
if _, ok := state.GetOk("attachedToolsIso"); ok {
command := []string{"set", vmName, "--device-del", "cdrom1"}
if err := driver.Prlctl(command...); err != nil {
err := fmt.Errorf("Error detaching ISO: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
return multistep.ActionContinue
}
func (s *StepRemoveDevices) Cleanup(state multistep.StateBag) {
}
package common
import (
"github.com/mitchellh/multistep"
"testing"
)
func TestStepRemoveDevices_impl(t *testing.T) {
var _ multistep.Step = new(StepRemoveDevices)
}
func TestStepRemoveDevices(t *testing.T) {
state := testState(t)
step := new(StepRemoveDevices)
state.Put("vmName", "foo")
driver := state.Get("driver").(*DriverMock)
// Test the run
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
if _, ok := state.GetOk("error"); ok {
t.Fatal("should NOT have error")
}
// Test that ISO was removed
if len(driver.PrlctlCalls) != 0 {
t.Fatalf("bad: %#v", driver.PrlctlCalls)
}
}
func TestStepRemoveDevices_attachedIso(t *testing.T) {
state := testState(t)
step := new(StepRemoveDevices)
state.Put("attachedIso", true)
state.Put("vmName", "foo")
driver := state.Get("driver").(*DriverMock)
// Test the run
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
if _, ok := state.GetOk("error"); ok {
t.Fatal("should NOT have error")
}
// Test that ISO was detached
if len(driver.PrlctlCalls) != 1 {
t.Fatalf("bad: %#v", driver.PrlctlCalls)
}
if driver.PrlctlCalls[0][2] != "--device-set" {
t.Fatalf("bad: %#v", driver.PrlctlCalls)
}
if driver.PrlctlCalls[0][3] != "cdrom0" {
t.Fatalf("bad: %#v", driver.PrlctlCalls)
}
if driver.PrlctlCalls[0][5] != "Default CD/DVD-ROM" {
t.Fatalf("bad: %#v", driver.PrlctlCalls)
}
}
func TestStepRemoveDevices_floppyPath(t *testing.T) {
state := testState(t)
step := new(StepRemoveDevices)
state.Put("floppy_path", "foo")
state.Put("vmName", "foo")
driver := state.Get("driver").(*DriverMock)
// Test the run
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
if _, ok := state.GetOk("error"); ok {
t.Fatal("should NOT have error")
}
// Test that both were removed
if len(driver.PrlctlCalls) != 1 {
t.Fatalf("bad: %#v", driver.PrlctlCalls)
}
if driver.PrlctlCalls[0][2] != "--device-del" {
t.Fatalf("bad: %#v", driver.PrlctlCalls)
}
if driver.PrlctlCalls[0][3] != "fdd0" {
t.Fatalf("bad: %#v", driver.PrlctlCalls)
}
}
...@@ -20,8 +20,8 @@ type bootCommandTemplateData struct { ...@@ -20,8 +20,8 @@ type bootCommandTemplateData struct {
Name string Name string
} }
// This step "types" the boot command into the VM via prltype, built on the // This step "types" the boot command into the VM via the prltype script, built on the
// Parallels Virtualization SDK - C API. // Parallels Virtualization SDK - Python API.
// //
// Uses: // Uses:
// driver Driver // driver Driver
......
...@@ -62,7 +62,7 @@ func (s *StepUploadParallelsTools) Run(state multistep.StateBag) multistep.StepA ...@@ -62,7 +62,7 @@ func (s *StepUploadParallelsTools) Run(state multistep.StateBag) multistep.StepA
ui.Say(fmt.Sprintf("Uploading Parallels Tools for '%s' to path: '%s'", ui.Say(fmt.Sprintf("Uploading Parallels Tools for '%s' to path: '%s'",
s.ParallelsToolsFlavor, s.ParallelsToolsGuestPath)) s.ParallelsToolsFlavor, s.ParallelsToolsGuestPath))
if err := comm.Upload(s.ParallelsToolsGuestPath, f); err != nil { if err := comm.Upload(s.ParallelsToolsGuestPath, f, nil); err != nil {
err := fmt.Errorf("Error uploading Parallels Tools: %s", err) err := fmt.Errorf("Error uploading Parallels Tools: %s", err)
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
......
...@@ -33,7 +33,7 @@ func (s *StepUploadVersion) Run(state multistep.StateBag) multistep.StepAction { ...@@ -33,7 +33,7 @@ func (s *StepUploadVersion) Run(state multistep.StateBag) multistep.StepAction {
ui.Say(fmt.Sprintf("Uploading Parallels version info (%s)", version)) ui.Say(fmt.Sprintf("Uploading Parallels version info (%s)", version))
var data bytes.Buffer var data bytes.Buffer
data.WriteString(version) data.WriteString(version)
if err := comm.Upload(s.Path, &data); err != nil { if err := comm.Upload(s.Path, &data, nil); err != nil {
state.Put("error", fmt.Errorf("Error uploading Parallels version: %s", err)) state.Put("error", fmt.Errorf("Error uploading Parallels version: %s", err))
return multistep.ActionHalt return multistep.ActionHalt
} }
......
...@@ -294,7 +294,6 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -294,7 +294,6 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Command: b.config.ShutdownCommand, Command: b.config.ShutdownCommand,
Timeout: b.config.ShutdownTimeout, Timeout: b.config.ShutdownTimeout,
}, },
new(parallelscommon.StepRemoveDevices),
} }
// Setup the state bag // Setup the state bag
......
...@@ -11,10 +11,14 @@ import ( ...@@ -11,10 +11,14 @@ import (
// This step attaches the ISO to the virtual machine. // This step attaches the ISO to the virtual machine.
// //
// Uses: // Uses:
// driver Driver
// isoPath string
// ui packer.Ui
// vmName string
// //
// Produces: // Produces:
type stepAttachISO struct { type stepAttachISO struct {
diskPath string cdromDevice string
} }
func (s *stepAttachISO) Run(state multistep.StateBag) multistep.StepAction { func (s *stepAttachISO) Run(state multistep.StateBag) multistep.StepAction {
...@@ -24,41 +28,78 @@ func (s *stepAttachISO) Run(state multistep.StateBag) multistep.StepAction { ...@@ -24,41 +28,78 @@ func (s *stepAttachISO) Run(state multistep.StateBag) multistep.StepAction {
vmName := state.Get("vmName").(string) vmName := state.Get("vmName").(string)
// Attach the disk to the controller // Attach the disk to the controller
ui.Say("Attaching ISO onto IDE controller...") ui.Say("Attaching ISO to the new CD/DVD drive...")
cdrom, err := driver.DeviceAddCdRom(vmName, isoPath)
if err != nil {
err := fmt.Errorf("Error attaching ISO: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Set new boot order
ui.Say("Setting the boot order...")
command := []string{ command := []string{
"set", vmName, "set", vmName,
"--device-set", "cdrom0", "--device-bootorder", fmt.Sprintf("hdd0 %s cdrom0 net0", cdrom),
"--image", isoPath,
"--enable", "--connect",
} }
if err := driver.Prlctl(command...); err != nil { if err := driver.Prlctl(command...); err != nil {
err := fmt.Errorf("Error attaching ISO: %s", err) err := fmt.Errorf("Error setting the boot order: %s", err)
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
} }
// Set some state so we know to remove // Disable 'cdrom0' device
state.Put("attachedIso", true) ui.Say("Disabling default CD/DVD drive...")
command = []string{
"set", vmName,
"--device-set", "cdrom0", "--disable",
}
if err := driver.Prlctl(command...); err != nil {
err := fmt.Errorf("Error disabling default CD/DVD drive: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Track the device name so that we can can delete later
s.cdromDevice = cdrom
return multistep.ActionContinue return multistep.ActionContinue
} }
func (s *stepAttachISO) Cleanup(state multistep.StateBag) { func (s *stepAttachISO) Cleanup(state multistep.StateBag) {
if _, ok := state.GetOk("attachedIso"); !ok {
return
}
driver := state.Get("driver").(parallelscommon.Driver) driver := state.Get("driver").(parallelscommon.Driver)
ui := state.Get("ui").(packer.Ui)
vmName := state.Get("vmName").(string) vmName := state.Get("vmName").(string)
// Enable 'cdrom0' device back
log.Println("Enabling default CD/DVD drive...")
command := []string{ command := []string{
"set", vmName, "set", vmName,
"--device-set", "cdrom0", "--device-set", "cdrom0", "--enable", "--disconnect",
"--enable", "--disconnect", }
if err := driver.Prlctl(command...); err != nil {
ui.Error(fmt.Sprintf("Error enabling default CD/DVD drive: %s", err))
}
// Detach ISO
if s.cdromDevice == "" {
return
} }
// Remove the ISO, ignore errors
log.Println("Detaching ISO...") log.Println("Detaching ISO...")
driver.Prlctl(command...) command = []string{
"set", vmName,
"--device-del", s.cdromDevice,
}
if err := driver.Prlctl(command...); err != nil {
ui.Error(fmt.Sprintf("Error detaching ISO: %s", err))
}
} }
...@@ -20,7 +20,7 @@ func (s *stepCreateDisk) Run(state multistep.StateBag) multistep.StepAction { ...@@ -20,7 +20,7 @@ func (s *stepCreateDisk) Run(state multistep.StateBag) multistep.StepAction {
command := []string{ command := []string{
"set", vmName, "set", vmName,
"--device-set", "hdd0", "--device-add", "hdd",
"--size", strconv.FormatUint(uint64(config.DiskSize), 10), "--size", strconv.FormatUint(uint64(config.DiskSize), 10),
"--iface", config.HardDriveInterface, "--iface", config.HardDriveInterface,
} }
......
...@@ -2,10 +2,10 @@ package iso ...@@ -2,10 +2,10 @@ package iso
import ( import (
"fmt" "fmt"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
parallelscommon "github.com/mitchellh/packer/builder/parallels/common" parallelscommon "github.com/mitchellh/packer/builder/parallels/common"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"path/filepath"
) )
// This step creates the actual virtual machine. // This step creates the actual virtual machine.
...@@ -21,16 +21,15 @@ func (s *stepCreateVM) Run(state multistep.StateBag) multistep.StepAction { ...@@ -21,16 +21,15 @@ func (s *stepCreateVM) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*config) config := state.Get("config").(*config)
driver := state.Get("driver").(parallelscommon.Driver) driver := state.Get("driver").(parallelscommon.Driver)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
name := config.VMName name := config.VMName
path := filepath.Join(".", config.OutputDir)
commands := make([][]string, 8) commands := make([][]string, 8)
commands[0] = []string{ commands[0] = []string{
"create", name, "create", name,
"--distribution", config.GuestOSType, "--distribution", config.GuestOSType,
"--dst", path, "--dst", config.OutputDir,
"--vmtype", "vm", "--vmtype", "vm",
"--no-hdd",
} }
commands[1] = []string{"set", name, "--cpus", "1"} commands[1] = []string{"set", name, "--cpus", "1"}
commands[2] = []string{"set", name, "--memsize", "512"} commands[2] = []string{"set", name, "--memsize", "512"}
......
...@@ -99,7 +99,6 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -99,7 +99,6 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Command: b.config.ShutdownCommand, Command: b.config.ShutdownCommand,
Timeout: b.config.ShutdownTimeout, Timeout: b.config.ShutdownTimeout,
}, },
new(parallelscommon.StepRemoveDevices),
} }
// Run the steps. // Run the steps.
......
...@@ -2,10 +2,11 @@ package pvm ...@@ -2,10 +2,11 @@ package pvm
import ( import (
"fmt" "fmt"
"os"
parallelscommon "github.com/mitchellh/packer/builder/parallels/common" parallelscommon "github.com/mitchellh/packer/builder/parallels/common"
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"os"
) )
// Config is the configuration structure for the builder. // Config is the configuration structure for the builder.
...@@ -23,6 +24,7 @@ type Config struct { ...@@ -23,6 +24,7 @@ type Config struct {
BootCommand []string `mapstructure:"boot_command"` BootCommand []string `mapstructure:"boot_command"`
SourcePath string `mapstructure:"source_path"` SourcePath string `mapstructure:"source_path"`
VMName string `mapstructure:"vm_name"` VMName string `mapstructure:"vm_name"`
ReassignMac bool `mapstructure:"reassign_mac"`
tpl *packer.ConfigTemplate tpl *packer.ConfigTemplate
} }
......
...@@ -20,7 +20,7 @@ func (s *StepImport) Run(state multistep.StateBag) multistep.StepAction { ...@@ -20,7 +20,7 @@ func (s *StepImport) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config) config := state.Get("config").(*Config)
ui.Say(fmt.Sprintf("Importing VM: %s", s.SourcePath)) ui.Say(fmt.Sprintf("Importing VM: %s", s.SourcePath))
if err := driver.Import(s.Name, s.SourcePath, config.OutputDir); err != nil { if err := driver.Import(s.Name, s.SourcePath, config.OutputDir, config.ReassignMac); err != nil {
err := fmt.Errorf("Error importing VM: %s", err) err := fmt.Errorf("Error importing VM: %s", err)
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
......
...@@ -3,15 +3,17 @@ package qemu ...@@ -3,15 +3,17 @@ package qemu
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"log" "log"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
commonssh "github.com/mitchellh/packer/common/ssh"
"github.com/mitchellh/packer/packer"
) )
const BuilderId = "transcend.qemu" const BuilderId = "transcend.qemu"
...@@ -54,6 +56,14 @@ var diskInterface = map[string]bool{ ...@@ -54,6 +56,14 @@ var diskInterface = map[string]bool{
"virtio": true, "virtio": true,
} }
var diskCache = map[string]bool{
"writethrough": true,
"writeback": true,
"none": true,
"unsafe": true,
"directsync": true,
}
type Builder struct { type Builder struct {
config config config config
runner multistep.Runner runner multistep.Runner
...@@ -66,6 +76,7 @@ type config struct { ...@@ -66,6 +76,7 @@ type config struct {
BootCommand []string `mapstructure:"boot_command"` BootCommand []string `mapstructure:"boot_command"`
DiskInterface string `mapstructure:"disk_interface"` DiskInterface string `mapstructure:"disk_interface"`
DiskSize uint `mapstructure:"disk_size"` DiskSize uint `mapstructure:"disk_size"`
DiskCache string `mapstructure:"disk_cache`
FloppyFiles []string `mapstructure:"floppy_files"` FloppyFiles []string `mapstructure:"floppy_files"`
Format string `mapstructure:"format"` Format string `mapstructure:"format"`
Headless bool `mapstructure:"headless"` Headless bool `mapstructure:"headless"`
...@@ -124,6 +135,10 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { ...@@ -124,6 +135,10 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
b.config.DiskSize = 40000 b.config.DiskSize = 40000
} }
if b.config.DiskCache == "" {
b.config.DiskCache = "writeback"
}
if b.config.Accelerator == "" { if b.config.Accelerator == "" {
b.config.Accelerator = "kvm" b.config.Accelerator = "kvm"
} }
...@@ -137,7 +152,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { ...@@ -137,7 +152,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
} }
if b.config.MachineType == "" { if b.config.MachineType == "" {
b.config.MachineType = "pc-1.0" b.config.MachineType = "pc"
} }
if b.config.OutputDir == "" { if b.config.OutputDir == "" {
...@@ -278,6 +293,11 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { ...@@ -278,6 +293,11 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
errs, errors.New("unrecognized disk interface type")) errs, errors.New("unrecognized disk interface type"))
} }
if _, ok := diskCache[b.config.DiskCache]; !ok {
errs = packer.MultiErrorAppend(
errs, errors.New("unrecognized disk cache type"))
}
if b.config.HTTPPortMin > b.config.HTTPPortMax { if b.config.HTTPPortMin > b.config.HTTPPortMax {
errs = packer.MultiErrorAppend( errs = packer.MultiErrorAppend(
errs, errors.New("http_port_min must be less than http_port_max")) errs, errors.New("http_port_min must be less than http_port_max"))
...@@ -352,7 +372,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { ...@@ -352,7 +372,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
if _, err := os.Stat(b.config.SSHKeyPath); err != nil { if _, err := os.Stat(b.config.SSHKeyPath); err != nil {
errs = packer.MultiErrorAppend( errs = packer.MultiErrorAppend(
errs, fmt.Errorf("ssh_key_path is invalid: %s", err)) errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
} else if _, err := sshKeyToSigner(b.config.SSHKeyPath); err != nil { } else if _, err := commonssh.FileSigner(b.config.SSHKeyPath); err != nil {
errs = packer.MultiErrorAppend( errs = packer.MultiErrorAppend(
errs, fmt.Errorf("ssh_key_path is invalid: %s", err)) errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
} }
......
package qemu package qemu
import ( import (
gossh "code.google.com/p/go.crypto/ssh"
"fmt" "fmt"
gossh "code.google.com/p/go.crypto/ssh"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
commonssh "github.com/mitchellh/packer/common/ssh"
"github.com/mitchellh/packer/communicator/ssh" "github.com/mitchellh/packer/communicator/ssh"
"io/ioutil"
"os"
) )
func sshAddress(state multistep.StateBag) (string, error) { func sshAddress(state multistep.StateBag) (string, error) {
...@@ -24,7 +24,7 @@ func sshConfig(state multistep.StateBag) (*gossh.ClientConfig, error) { ...@@ -24,7 +24,7 @@ func sshConfig(state multistep.StateBag) (*gossh.ClientConfig, error) {
} }
if config.SSHKeyPath != "" { if config.SSHKeyPath != "" {
signer, err := sshKeyToSigner(config.SSHKeyPath) signer, err := commonssh.FileSigner(config.SSHKeyPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -37,23 +37,3 @@ func sshConfig(state multistep.StateBag) (*gossh.ClientConfig, error) { ...@@ -37,23 +37,3 @@ func sshConfig(state multistep.StateBag) (*gossh.ClientConfig, error) {
Auth: auth, Auth: auth,
}, nil }, nil
} }
func sshKeyToSigner(path string) (gossh.Signer, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
keyBytes, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
signer, err := gossh.ParsePrivateKey(keyBytes)
if err != nil {
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
}
return signer, nil
}
...@@ -2,11 +2,12 @@ package qemu ...@@ -2,11 +2,12 @@ package qemu
import ( import (
"fmt" "fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log" "log"
"math/rand" "math/rand"
"net" "net"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
) )
// This step adds a NAT port forwarding definition so that SSH is available // This step adds a NAT port forwarding definition so that SSH is available
...@@ -23,9 +24,16 @@ func (s *stepForwardSSH) Run(state multistep.StateBag) multistep.StepAction { ...@@ -23,9 +24,16 @@ func (s *stepForwardSSH) Run(state multistep.StateBag) multistep.StepAction {
log.Printf("Looking for available SSH port between %d and %d", config.SSHHostPortMin, config.SSHHostPortMax) log.Printf("Looking for available SSH port between %d and %d", config.SSHHostPortMin, config.SSHHostPortMax)
var sshHostPort uint var sshHostPort uint
var offset uint = 0
portRange := int(config.SSHHostPortMax - config.SSHHostPortMin) portRange := int(config.SSHHostPortMax - config.SSHHostPortMin)
if portRange > 0 {
// Have to check if > 0 to avoid a panic
offset = uint(rand.Intn(portRange))
}
for { for {
sshHostPort = uint(rand.Intn(portRange)) + config.SSHHostPortMin sshHostPort = offset + config.SSHHostPortMin
log.Printf("Trying port: %d", sshHostPort) log.Printf("Trying port: %d", sshHostPort)
l, err := net.Listen("tcp", fmt.Sprintf(":%d", sshHostPort)) l, err := net.Listen("tcp", fmt.Sprintf(":%d", sshHostPort))
if err == nil { if err == nil {
......
...@@ -2,11 +2,12 @@ package qemu ...@@ -2,11 +2,12 @@ package qemu
import ( import (
"fmt" "fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log" "log"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
) )
// stepRun runs the virtual machine // stepRun runs the virtual machine
...@@ -78,13 +79,12 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error ...@@ -78,13 +79,12 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error
defaultArgs["-name"] = vmName defaultArgs["-name"] = vmName
defaultArgs["-machine"] = fmt.Sprintf("type=%s", config.MachineType) defaultArgs["-machine"] = fmt.Sprintf("type=%s", config.MachineType)
defaultArgs["-netdev"] = "user,id=user.0" defaultArgs["-netdev"] = fmt.Sprintf("user,id=user.0,hostfwd=tcp::%v-:22", sshHostPort)
defaultArgs["-device"] = fmt.Sprintf("%s,netdev=user.0", config.NetDevice) defaultArgs["-device"] = fmt.Sprintf("%s,netdev=user.0", config.NetDevice)
defaultArgs["-drive"] = fmt.Sprintf("file=%s,if=%s", imgPath, config.DiskInterface) defaultArgs["-drive"] = fmt.Sprintf("file=%s,if=%s,cache=%s", imgPath, config.DiskInterface, config.DiskCache)
defaultArgs["-cdrom"] = isoPath defaultArgs["-cdrom"] = isoPath
defaultArgs["-boot"] = bootDrive defaultArgs["-boot"] = bootDrive
defaultArgs["-m"] = "512M" defaultArgs["-m"] = "512M"
defaultArgs["-redir"] = fmt.Sprintf("tcp:%v::22", sshHostPort)
defaultArgs["-vnc"] = vnc defaultArgs["-vnc"] = vnc
// Append the accelerator to the machine type if it is specified // Append the accelerator to the machine type if it is specified
......
...@@ -23,7 +23,7 @@ type Driver interface { ...@@ -23,7 +23,7 @@ type Driver interface {
Delete(string) error Delete(string) error
// Import a VM // Import a VM
Import(string, string, string) error Import(string, string, []string) error
// The complete path to the Guest Additions ISO // The complete path to the Guest Additions ISO
Iso() (string, error) Iso() (string, error)
...@@ -55,15 +55,16 @@ func NewDriver() (Driver, error) { ...@@ -55,15 +55,16 @@ func NewDriver() (Driver, error) {
// On Windows, we check VBOX_INSTALL_PATH env var for the path // On Windows, we check VBOX_INSTALL_PATH env var for the path
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
if installPath := os.Getenv("VBOX_INSTALL_PATH"); installPath != "" { vars := []string{"VBOX_INSTALL_PATH", "VBOX_MSI_INSTALL_PATH"}
log.Printf("[DEBUG] builder/virtualbox: VBOX_INSTALL_PATH: %s", for _, key := range vars {
installPath) value := os.Getenv(key)
for _, path := range strings.Split(installPath, ";") { if value != "" {
path = filepath.Join(path, "VBoxManage.exe") log.Printf(
if _, err := os.Stat(path); err == nil { "[DEBUG] builder/virtualbox: %s = %s", key, value)
vboxmanagePath = path vboxmanagePath = findVBoxManageWindows(value)
break }
} if vboxmanagePath != "" {
break
} }
} }
} }
...@@ -84,3 +85,14 @@ func NewDriver() (Driver, error) { ...@@ -84,3 +85,14 @@ func NewDriver() (Driver, error) {
return driver, nil return driver, nil
} }
func findVBoxManageWindows(paths string) string {
for _, path := range strings.Split(paths, ";") {
path = filepath.Join(path, "VBoxManage.exe")
if _, err := os.Stat(path); err == nil {
return path
}
}
return ""
}
...@@ -49,11 +49,12 @@ func (d *VBox42Driver) Iso() (string, error) { ...@@ -49,11 +49,12 @@ func (d *VBox42Driver) Iso() (string, error) {
return "", err return "", err
} }
DefaultGuestAdditionsRe := regexp.MustCompile("Default Guest Additions ISO:(.*)") DefaultGuestAdditionsRe := regexp.MustCompile("Default Guest Additions ISO:(.+)")
for _, line := range strings.Split(stdout.String(), "\n") { for _, line := range strings.Split(stdout.String(), "\n") {
// Need to trim off CR character when running in windows // Need to trim off CR character when running in windows
line = strings.TrimRight(line, "\r") // Trimming whitespaces at this point helps to filter out empty value
line = strings.TrimRight(line, " \r")
matches := DefaultGuestAdditionsRe.FindStringSubmatch(line) matches := DefaultGuestAdditionsRe.FindStringSubmatch(line)
if matches == nil { if matches == nil {
...@@ -66,16 +67,16 @@ func (d *VBox42Driver) Iso() (string, error) { ...@@ -66,16 +67,16 @@ func (d *VBox42Driver) Iso() (string, error) {
return isoname, nil return isoname, nil
} }
return "", fmt.Errorf("Cannot find \"Default Guest Additions ISO\" in vboxmanage output") return "", fmt.Errorf("Cannot find \"Default Guest Additions ISO\" in vboxmanage output (or it is empty)")
} }
func (d *VBox42Driver) Import(name, path, opts string) error { func (d *VBox42Driver) Import(name string, path string, flags []string) error {
args := []string{ args := []string{
"import", path, "import", path,
"--vsys", "0", "--vsys", "0",
"--vmname", name, "--vmname", name,
"--options", opts,
} }
args = append(args, flags...)
return d.VBoxManage(args...) return d.VBoxManage(args...)
} }
...@@ -157,6 +158,15 @@ func (d *VBox42Driver) VBoxManage(args ...string) error { ...@@ -157,6 +158,15 @@ func (d *VBox42Driver) VBoxManage(args ...string) error {
err = fmt.Errorf("VBoxManage error: %s", stderrString) err = fmt.Errorf("VBoxManage error: %s", stderrString)
} }
if err == nil {
// Sometimes VBoxManage gives us an error with a zero exit code,
// so we also regexp match an error string.
m, _ := regexp.MatchString("VBoxManage([.a-z]+?): error:", stderrString)
if m {
err = fmt.Errorf("VBoxManage error: %s", stderrString)
}
}
log.Printf("stdout: %s", stdoutString) log.Printf("stdout: %s", stdoutString)
log.Printf("stderr: %s", stderrString) log.Printf("stderr: %s", stderrString)
...@@ -186,12 +196,12 @@ func (d *VBox42Driver) Version() (string, error) { ...@@ -186,12 +196,12 @@ func (d *VBox42Driver) Version() (string, error) {
return "", fmt.Errorf("VirtualBox is not properly setup: %s", versionOutput) return "", fmt.Errorf("VirtualBox is not properly setup: %s", versionOutput)
} }
versionRe := regexp.MustCompile("^[.0-9]+(?:_RC[0-9]+)?") versionRe := regexp.MustCompile("^([.0-9]+)(?:_(?:RC|OSEr)[0-9]+)?")
matches := versionRe.FindAllString(versionOutput, 1) matches := versionRe.FindAllStringSubmatch(versionOutput, 1)
if matches == nil { if matches == nil || len(matches[0]) != 2 {
return "", fmt.Errorf("No version found: %s", versionOutput) return "", fmt.Errorf("No version found: %s", versionOutput)
} }
log.Printf("VirtualBox version: %s", matches[0]) log.Printf("VirtualBox version: %s", matches[0][1])
return matches[0], nil return matches[0][1], nil
} }
...@@ -16,7 +16,7 @@ type DriverMock struct { ...@@ -16,7 +16,7 @@ type DriverMock struct {
ImportCalled bool ImportCalled bool
ImportName string ImportName string
ImportPath string ImportPath string
ImportOpts string ImportFlags []string
ImportErr error ImportErr error
IsoCalled bool IsoCalled bool
...@@ -55,11 +55,11 @@ func (d *DriverMock) Delete(name string) error { ...@@ -55,11 +55,11 @@ func (d *DriverMock) Delete(name string) error {
return d.DeleteErr return d.DeleteErr
} }
func (d *DriverMock) Import(name, path, opts string) error { func (d *DriverMock) Import(name string, path string, flags []string) error {
d.ImportCalled = true d.ImportCalled = true
d.ImportName = name d.ImportName = name
d.ImportPath = path d.ImportPath = path
d.ImportOpts = opts d.ImportFlags = flags
return d.ImportErr return d.ImportErr
} }
......
package common package common
import ( import (
"errors"
"fmt" "fmt"
"github.com/mitchellh/packer/packer"
"time" "time"
"github.com/mitchellh/packer/packer"
) )
type RunConfig struct { type RunConfig struct {
Headless bool `mapstructure:"headless"` Headless bool `mapstructure:"headless"`
RawBootWait string `mapstructure:"boot_wait"` RawBootWait string `mapstructure:"boot_wait"`
HTTPDir string `mapstructure:"http_directory"`
HTTPPortMin uint `mapstructure:"http_port_min"`
HTTPPortMax uint `mapstructure:"http_port_max"`
BootWait time.Duration `` BootWait time.Duration ``
} }
...@@ -18,8 +24,17 @@ func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error { ...@@ -18,8 +24,17 @@ func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error {
c.RawBootWait = "10s" c.RawBootWait = "10s"
} }
if c.HTTPPortMin == 0 {
c.HTTPPortMin = 8000
}
if c.HTTPPortMax == 0 {
c.HTTPPortMax = 9000
}
templates := map[string]*string{ templates := map[string]*string{
"boot_wait": &c.RawBootWait, "boot_wait": &c.RawBootWait,
"http_directory": &c.HTTPDir,
} }
errs := make([]error, 0) errs := make([]error, 0)
...@@ -37,5 +52,10 @@ func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error { ...@@ -37,5 +52,10 @@ func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error {
errs = append(errs, fmt.Errorf("Failed parsing boot_wait: %s", err)) errs = append(errs, fmt.Errorf("Failed parsing boot_wait: %s", err))
} }
if c.HTTPPortMin > c.HTTPPortMax {
errs = append(errs,
errors.New("http_port_min must be less than http_port_max"))
}
return errs return errs
} }
package common package common
import ( import (
gossh "code.google.com/p/go.crypto/ssh"
"fmt" "fmt"
gossh "code.google.com/p/go.crypto/ssh"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
commonssh "github.com/mitchellh/packer/common/ssh"
"github.com/mitchellh/packer/communicator/ssh" "github.com/mitchellh/packer/communicator/ssh"
"io/ioutil"
"os"
) )
func SSHAddress(state multistep.StateBag) (string, error) { func SSHAddress(state multistep.StateBag) (string, error) {
...@@ -23,7 +23,7 @@ func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*gossh.ClientConf ...@@ -23,7 +23,7 @@ func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*gossh.ClientConf
} }
if config.SSHKeyPath != "" { if config.SSHKeyPath != "" {
signer, err := sshKeyToSigner(config.SSHKeyPath) signer, err := commonssh.FileSigner(config.SSHKeyPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -37,23 +37,3 @@ func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*gossh.ClientConf ...@@ -37,23 +37,3 @@ func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*gossh.ClientConf
}, nil }, nil
} }
} }
func sshKeyToSigner(path string) (gossh.Signer, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
keyBytes, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
signer, err := gossh.ParsePrivateKey(keyBytes)
if err != nil {
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
}
return signer, nil
}
...@@ -3,9 +3,11 @@ package common ...@@ -3,9 +3,11 @@ package common
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/mitchellh/packer/packer"
"os" "os"
"time" "time"
commonssh "github.com/mitchellh/packer/common/ssh"
"github.com/mitchellh/packer/packer"
) )
type SSHConfig struct { type SSHConfig struct {
...@@ -56,7 +58,7 @@ func (c *SSHConfig) Prepare(t *packer.ConfigTemplate) []error { ...@@ -56,7 +58,7 @@ func (c *SSHConfig) Prepare(t *packer.ConfigTemplate) []error {
if c.SSHKeyPath != "" { if c.SSHKeyPath != "" {
if _, err := os.Stat(c.SSHKeyPath); err != nil { if _, err := os.Stat(c.SSHKeyPath); err != nil {
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err)) errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
} else if _, err := sshKeyToSigner(c.SSHKeyPath); err != nil { } else if _, err := commonssh.FileSigner(c.SSHKeyPath); err != nil {
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err)) errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
} }
} }
......
...@@ -91,6 +91,13 @@ func (s *StepDownloadGuestAdditions) Run(state multistep.StateBag) multistep.Ste ...@@ -91,6 +91,13 @@ func (s *StepDownloadGuestAdditions) Run(state multistep.StateBag) multistep.Ste
additionsName) additionsName)
} }
} }
if url == "" {
err := fmt.Errorf("Couldn't detect guest additions URL.\n" +
"Please specify `guest_additions_url` manually.")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if checksumType != "none" { if checksumType != "none" {
if s.GuestAdditionsSHA256 != "" { if s.GuestAdditionsSHA256 != "" {
......
package iso package common
import ( import (
"fmt" "fmt"
...@@ -15,28 +15,30 @@ import ( ...@@ -15,28 +15,30 @@ import (
// template. // template.
// //
// Uses: // Uses:
// config *config
// ui packer.Ui // ui packer.Ui
// //
// Produces: // Produces:
// http_port int - The port the HTTP server started on. // http_port int - The port the HTTP server started on.
type stepHTTPServer struct { type StepHTTPServer struct {
HTTPDir string
HTTPPortMin uint
HTTPPortMax uint
l net.Listener l net.Listener
} }
func (s *stepHTTPServer) Run(state multistep.StateBag) multistep.StepAction { func (s *StepHTTPServer) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*config)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
var httpPort uint = 0 var httpPort uint = 0
if config.HTTPDir == "" { if s.HTTPDir == "" {
state.Put("http_port", httpPort) state.Put("http_port", httpPort)
return multistep.ActionContinue return multistep.ActionContinue
} }
// Find an available TCP port for our HTTP server // Find an available TCP port for our HTTP server
var httpAddr string var httpAddr string
portRange := int(config.HTTPPortMax - config.HTTPPortMin) portRange := int(s.HTTPPortMax - s.HTTPPortMin)
for { for {
var err error var err error
var offset uint = 0 var offset uint = 0
...@@ -46,7 +48,7 @@ func (s *stepHTTPServer) Run(state multistep.StateBag) multistep.StepAction { ...@@ -46,7 +48,7 @@ func (s *stepHTTPServer) Run(state multistep.StateBag) multistep.StepAction {
offset = uint(rand.Intn(portRange)) offset = uint(rand.Intn(portRange))
} }
httpPort = offset + config.HTTPPortMin httpPort = offset + s.HTTPPortMin
httpAddr = fmt.Sprintf(":%d", httpPort) httpAddr = fmt.Sprintf(":%d", httpPort)
log.Printf("Trying port: %d", httpPort) log.Printf("Trying port: %d", httpPort)
s.l, err = net.Listen("tcp", httpAddr) s.l, err = net.Listen("tcp", httpAddr)
...@@ -58,7 +60,7 @@ func (s *stepHTTPServer) Run(state multistep.StateBag) multistep.StepAction { ...@@ -58,7 +60,7 @@ func (s *stepHTTPServer) Run(state multistep.StateBag) multistep.StepAction {
ui.Say(fmt.Sprintf("Starting HTTP server on port %d", httpPort)) ui.Say(fmt.Sprintf("Starting HTTP server on port %d", httpPort))
// Start the HTTP server and run it in the background // Start the HTTP server and run it in the background
fileServer := http.FileServer(http.Dir(config.HTTPDir)) fileServer := http.FileServer(http.Dir(s.HTTPDir))
server := &http.Server{Addr: httpAddr, Handler: fileServer} server := &http.Server{Addr: httpAddr, Handler: fileServer}
go server.Serve(s.l) go server.Serve(s.l)
...@@ -68,7 +70,7 @@ func (s *stepHTTPServer) Run(state multistep.StateBag) multistep.StepAction { ...@@ -68,7 +70,7 @@ func (s *stepHTTPServer) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionContinue return multistep.ActionContinue
} }
func (s *stepHTTPServer) Cleanup(multistep.StateBag) { func (s *StepHTTPServer) Cleanup(multistep.StateBag) {
if s.l != nil { if s.l != nil {
// Close the listener so that the HTTP server stops // Close the listener so that the HTTP server stops
s.l.Close() s.l.Close()
......
package iso package common
import ( import (
"fmt" "fmt"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
vboxcommon "github.com/mitchellh/packer/builder/virtualbox/common"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"log" "log"
"strings" "strings"
...@@ -23,7 +22,6 @@ type bootCommandTemplateData struct { ...@@ -23,7 +22,6 @@ type bootCommandTemplateData struct {
// This step "types" the boot command into the VM over VNC. // This step "types" the boot command into the VM over VNC.
// //
// Uses: // Uses:
// config *config
// driver Driver // driver Driver
// http_port int // http_port int
// ui packer.Ui // ui packer.Ui
...@@ -31,11 +29,14 @@ type bootCommandTemplateData struct { ...@@ -31,11 +29,14 @@ type bootCommandTemplateData struct {
// //
// Produces: // Produces:
// <nothing> // <nothing>
type stepTypeBootCommand struct{} type StepTypeBootCommand struct {
BootCommand []string
VMName string
Tpl *packer.ConfigTemplate
}
func (s *stepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAction { func (s *StepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*config) driver := state.Get("driver").(Driver)
driver := state.Get("driver").(vboxcommon.Driver)
httpPort := state.Get("http_port").(uint) httpPort := state.Get("http_port").(uint)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
vmName := state.Get("vmName").(string) vmName := state.Get("vmName").(string)
...@@ -43,12 +44,12 @@ func (s *stepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAction ...@@ -43,12 +44,12 @@ func (s *stepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAction
tplData := &bootCommandTemplateData{ tplData := &bootCommandTemplateData{
"10.0.2.2", "10.0.2.2",
httpPort, httpPort,
config.VMName, s.VMName,
} }
ui.Say("Typing the boot command...") ui.Say("Typing the boot command...")
for _, command := range config.BootCommand { for _, command := range s.BootCommand {
command, err := config.tpl.Process(command, tplData) command, err := s.Tpl.Process(command, tplData)
if err != nil { if err != nil {
err := fmt.Errorf("Error preparing boot command: %s", err) err := fmt.Errorf("Error preparing boot command: %s", err)
state.Put("error", err) state.Put("error", err)
...@@ -90,7 +91,7 @@ func (s *stepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAction ...@@ -90,7 +91,7 @@ func (s *stepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAction
return multistep.ActionContinue return multistep.ActionContinue
} }
func (*stepTypeBootCommand) Cleanup(multistep.StateBag) {} func (*StepTypeBootCommand) Cleanup(multistep.StateBag) {}
func scancodes(message string) []string { func scancodes(message string) []string {
// Scancodes reference: http://www.win.tue.nl/~aeb/linux/kbd/scancodes-1.html // Scancodes reference: http://www.win.tue.nl/~aeb/linux/kbd/scancodes-1.html
......
...@@ -58,7 +58,7 @@ func (s *StepUploadGuestAdditions) Run(state multistep.StateBag) multistep.StepA ...@@ -58,7 +58,7 @@ func (s *StepUploadGuestAdditions) Run(state multistep.StateBag) multistep.StepA
} }
ui.Say("Uploading VirtualBox guest additions ISO...") ui.Say("Uploading VirtualBox guest additions ISO...")
if err := comm.Upload(s.GuestAdditionsPath, f); err != nil { if err := comm.Upload(s.GuestAdditionsPath, f, nil); err != nil {
state.Put("error", fmt.Errorf("Error uploading guest additions: %s", err)) state.Put("error", fmt.Errorf("Error uploading guest additions: %s", err))
return multistep.ActionHalt return multistep.ActionHalt
} }
......
...@@ -33,7 +33,7 @@ func (s *StepUploadVersion) Run(state multistep.StateBag) multistep.StepAction { ...@@ -33,7 +33,7 @@ func (s *StepUploadVersion) Run(state multistep.StateBag) multistep.StepAction {
ui.Say(fmt.Sprintf("Uploading VirtualBox version info (%s)", version)) ui.Say(fmt.Sprintf("Uploading VirtualBox version info (%s)", version))
var data bytes.Buffer var data bytes.Buffer
data.WriteString(version) data.WriteString(version)
if err := comm.Upload(s.Path, &data); err != nil { if err := comm.Upload(s.Path, &data, nil); err != nil {
state.Put("error", fmt.Errorf("Error uploading VirtualBox version: %s", err)) state.Put("error", fmt.Errorf("Error uploading VirtualBox version: %s", err))
return multistep.ActionHalt return multistep.ActionHalt
} }
......
...@@ -42,9 +42,6 @@ type config struct { ...@@ -42,9 +42,6 @@ type config struct {
GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"` GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"`
GuestOSType string `mapstructure:"guest_os_type"` GuestOSType string `mapstructure:"guest_os_type"`
HardDriveInterface string `mapstructure:"hard_drive_interface"` HardDriveInterface string `mapstructure:"hard_drive_interface"`
HTTPDir string `mapstructure:"http_directory"`
HTTPPortMin uint `mapstructure:"http_port_min"`
HTTPPortMax uint `mapstructure:"http_port_max"`
ISOChecksum string `mapstructure:"iso_checksum"` ISOChecksum string `mapstructure:"iso_checksum"`
ISOChecksumType string `mapstructure:"iso_checksum_type"` ISOChecksumType string `mapstructure:"iso_checksum_type"`
ISOInterface string `mapstructure:"iso_interface"` ISOInterface string `mapstructure:"iso_interface"`
...@@ -103,20 +100,12 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { ...@@ -103,20 +100,12 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
b.config.GuestOSType = "Other" b.config.GuestOSType = "Other"
} }
if b.config.HTTPPortMin == 0 {
b.config.HTTPPortMin = 8000
}
if b.config.HTTPPortMax == 0 {
b.config.HTTPPortMax = 9000
}
if b.config.ISOInterface == "" { if b.config.ISOInterface == "" {
b.config.ISOInterface = "ide" b.config.ISOInterface = "ide"
} }
if b.config.VMName == "" { if b.config.VMName == "" {
b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName) b.config.VMName = fmt.Sprintf("packer-%s-{{timestamp}}", b.config.PackerBuildName)
} }
// Errors // Errors
...@@ -125,7 +114,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { ...@@ -125,7 +114,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
"guest_additions_sha256": &b.config.GuestAdditionsSHA256, "guest_additions_sha256": &b.config.GuestAdditionsSHA256,
"guest_os_type": &b.config.GuestOSType, "guest_os_type": &b.config.GuestOSType,
"hard_drive_interface": &b.config.HardDriveInterface, "hard_drive_interface": &b.config.HardDriveInterface,
"http_directory": &b.config.HTTPDir,
"iso_checksum": &b.config.ISOChecksum, "iso_checksum": &b.config.ISOChecksum,
"iso_checksum_type": &b.config.ISOChecksumType, "iso_checksum_type": &b.config.ISOChecksumType,
"iso_interface": &b.config.ISOInterface, "iso_interface": &b.config.ISOInterface,
...@@ -175,11 +163,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { ...@@ -175,11 +163,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
errs, errors.New("hard_drive_interface can only be ide or sata")) errs, errors.New("hard_drive_interface can only be ide or sata"))
} }
if b.config.HTTPPortMin > b.config.HTTPPortMax {
errs = packer.MultiErrorAppend(
errs, errors.New("http_port_min must be less than http_port_max"))
}
if b.config.ISOChecksumType == "" { if b.config.ISOChecksumType == "" {
errs = packer.MultiErrorAppend( errs = packer.MultiErrorAppend(
errs, errors.New("The iso_checksum_type must be specified.")) errs, errors.New("The iso_checksum_type must be specified."))
...@@ -298,7 +281,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -298,7 +281,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&common.StepCreateFloppy{ &common.StepCreateFloppy{
Files: b.config.FloppyFiles, Files: b.config.FloppyFiles,
}, },
new(stepHTTPServer), &vboxcommon.StepHTTPServer{
HTTPDir: b.config.HTTPDir,
HTTPPortMin: b.config.HTTPPortMin,
HTTPPortMax: b.config.HTTPPortMax,
},
new(vboxcommon.StepSuppressMessages), new(vboxcommon.StepSuppressMessages),
new(stepCreateVM), new(stepCreateVM),
new(stepCreateDisk), new(stepCreateDisk),
...@@ -320,7 +307,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -320,7 +307,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
BootWait: b.config.BootWait, BootWait: b.config.BootWait,
Headless: b.config.Headless, Headless: b.config.Headless,
}, },
new(stepTypeBootCommand), &vboxcommon.StepTypeBootCommand{
BootCommand: b.config.BootCommand,
VMName: b.config.VMName,
Tpl: b.config.tpl,
},
&common.StepConnectSSH{ &common.StepConnectSSH{
SSHAddress: vboxcommon.SSHAddress, SSHAddress: vboxcommon.SSHAddress,
SSHConfig: vboxcommon.SSHConfigFunc(b.config.SSHConfig), SSHConfig: vboxcommon.SSHConfigFunc(b.config.SSHConfig),
......
...@@ -46,7 +46,7 @@ func TestBuilderPrepare_Defaults(t *testing.T) { ...@@ -46,7 +46,7 @@ func TestBuilderPrepare_Defaults(t *testing.T) {
t.Errorf("bad guest OS type: %s", b.config.GuestOSType) t.Errorf("bad guest OS type: %s", b.config.GuestOSType)
} }
if b.config.VMName != "packer-foo" { if b.config.VMName == "" {
t.Errorf("bad vm name: %s", b.config.VMName) t.Errorf("bad vm name: %s", b.config.VMName)
} }
......
...@@ -61,6 +61,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -61,6 +61,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&common.StepCreateFloppy{ &common.StepCreateFloppy{
Files: b.config.FloppyFiles, Files: b.config.FloppyFiles,
}, },
&vboxcommon.StepHTTPServer{
HTTPDir: b.config.HTTPDir,
HTTPPortMin: b.config.HTTPPortMin,
HTTPPortMax: b.config.HTTPPortMax,
},
&vboxcommon.StepDownloadGuestAdditions{ &vboxcommon.StepDownloadGuestAdditions{
GuestAdditionsMode: b.config.GuestAdditionsMode, GuestAdditionsMode: b.config.GuestAdditionsMode,
GuestAdditionsURL: b.config.GuestAdditionsURL, GuestAdditionsURL: b.config.GuestAdditionsURL,
...@@ -68,9 +73,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -68,9 +73,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Tpl: b.config.tpl, Tpl: b.config.tpl,
}, },
&StepImport{ &StepImport{
Name: b.config.VMName, Name: b.config.VMName,
SourcePath: b.config.SourcePath, SourcePath: b.config.SourcePath,
ImportOpts: b.config.ImportOpts, ImportFlags: b.config.ImportFlags,
}, },
&vboxcommon.StepAttachGuestAdditions{ &vboxcommon.StepAttachGuestAdditions{
GuestAdditionsMode: b.config.GuestAdditionsMode, GuestAdditionsMode: b.config.GuestAdditionsMode,
...@@ -89,6 +94,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -89,6 +94,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
BootWait: b.config.BootWait, BootWait: b.config.BootWait,
Headless: b.config.Headless, Headless: b.config.Headless,
}, },
&vboxcommon.StepTypeBootCommand{
BootCommand: b.config.BootCommand,
VMName: b.config.VMName,
Tpl: b.config.tpl,
},
&common.StepConnectSSH{ &common.StepConnectSSH{
SSHAddress: vboxcommon.SSHAddress, SSHAddress: vboxcommon.SSHAddress,
SSHConfig: vboxcommon.SSHConfigFunc(b.config.SSHConfig), SSHConfig: vboxcommon.SSHConfigFunc(b.config.SSHConfig),
......
...@@ -24,13 +24,15 @@ type Config struct { ...@@ -24,13 +24,15 @@ type Config struct {
vboxcommon.VBoxManagePostConfig `mapstructure:",squash"` vboxcommon.VBoxManagePostConfig `mapstructure:",squash"`
vboxcommon.VBoxVersionConfig `mapstructure:",squash"` vboxcommon.VBoxVersionConfig `mapstructure:",squash"`
SourcePath string `mapstructure:"source_path"` BootCommand []string `mapstructure:"boot_command"`
GuestAdditionsMode string `mapstructure:"guest_additions_mode"` SourcePath string `mapstructure:"source_path"`
GuestAdditionsPath string `mapstructure:"guest_additions_path"` GuestAdditionsMode string `mapstructure:"guest_additions_mode"`
GuestAdditionsURL string `mapstructure:"guest_additions_url"` GuestAdditionsPath string `mapstructure:"guest_additions_path"`
GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"` GuestAdditionsURL string `mapstructure:"guest_additions_url"`
VMName string `mapstructure:"vm_name"` GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"`
ImportOpts string `mapstructure:"import_opts"` VMName string `mapstructure:"vm_name"`
ImportOpts string `mapstructure:"import_opts"`
ImportFlags []string `mapstructure:"import_flags"`
tpl *packer.ConfigTemplate tpl *packer.ConfigTemplate
} }
...@@ -90,6 +92,21 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { ...@@ -90,6 +92,21 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
} }
} }
sliceTemplates := map[string][]string{
"import_flags": c.ImportFlags,
}
for n, slice := range sliceTemplates {
for i, elem := range slice {
var err error
slice[i], err = c.tpl.Process(elem, nil)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error processing %s[%d]: %s", n, i, err))
}
}
}
if c.SourcePath == "" { if c.SourcePath == "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("source_path is required")) errs = packer.MultiErrorAppend(errs, fmt.Errorf("source_path is required"))
} else { } else {
...@@ -99,6 +116,13 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { ...@@ -99,6 +116,13 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
} }
} }
for i, command := range c.BootCommand {
if err := c.tpl.Validate(command); err != nil {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Error processing boot_command[%d]: %s", i, err))
}
}
validates := map[string]*string{ validates := map[string]*string{
"guest_additions_path": &c.GuestAdditionsPath, "guest_additions_path": &c.GuestAdditionsPath,
"guest_additions_url": &c.GuestAdditionsURL, "guest_additions_url": &c.GuestAdditionsURL,
...@@ -147,5 +171,10 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { ...@@ -147,5 +171,10 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
return nil, warnings, errs return nil, warnings, errs
} }
// TODO: Write a packer fix and just remove import_opts
if c.ImportOpts != "" {
c.ImportFlags = append(c.ImportFlags, "--options", c.ImportOpts)
}
return c, warnings, nil return c, warnings, nil
} }
...@@ -9,9 +9,9 @@ import ( ...@@ -9,9 +9,9 @@ import (
// This step imports an OVF VM into VirtualBox. // This step imports an OVF VM into VirtualBox.
type StepImport struct { type StepImport struct {
Name string Name string
SourcePath string SourcePath string
ImportOpts string ImportFlags []string
vmName string vmName string
} }
...@@ -21,7 +21,7 @@ func (s *StepImport) Run(state multistep.StateBag) multistep.StepAction { ...@@ -21,7 +21,7 @@ func (s *StepImport) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
ui.Say(fmt.Sprintf("Importing VM: %s", s.SourcePath)) ui.Say(fmt.Sprintf("Importing VM: %s", s.SourcePath))
if err := driver.Import(s.Name, s.SourcePath, s.ImportOpts); err != nil { if err := driver.Import(s.Name, s.SourcePath, s.ImportFlags); err != nil {
err := fmt.Errorf("Error importing VM: %s", err) err := fmt.Errorf("Error importing VM: %s", err)
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
......
...@@ -129,7 +129,12 @@ func runAndLog(cmd *exec.Cmd) (string, string, error) { ...@@ -129,7 +129,12 @@ func runAndLog(cmd *exec.Cmd) (string, string, error) {
stderrString := strings.TrimSpace(stderr.String()) stderrString := strings.TrimSpace(stderr.String())
if _, ok := err.(*exec.ExitError); ok { if _, ok := err.(*exec.ExitError); ok {
err = fmt.Errorf("VMware error: %s", stderrString) message := stderrString
if message == "" {
message = stdoutString
}
err = fmt.Errorf("VMware error: %s", message)
} }
log.Printf("stdout: %s", stdoutString) log.Printf("stdout: %s", stdoutString)
...@@ -153,7 +158,7 @@ func normalizeVersion(version string) (string, error) { ...@@ -153,7 +158,7 @@ func normalizeVersion(version string) (string, error) {
return fmt.Sprintf("%02d", i), nil return fmt.Sprintf("%02d", i), nil
} }
func compareVersions(versionFound string, versionWanted string) error { func compareVersions(versionFound string, versionWanted string, product string) error {
found, err := normalizeVersion(versionFound) found, err := normalizeVersion(versionFound)
if err != nil { if err != nil {
return err return err
...@@ -166,7 +171,7 @@ func compareVersions(versionFound string, versionWanted string) error { ...@@ -166,7 +171,7 @@ func compareVersions(versionFound string, versionWanted string) error {
if found < wanted { if found < wanted {
return fmt.Errorf( return fmt.Errorf(
"VMware WS version %s, or greater, is required. Found version: %s", versionWanted, versionFound) "VMware %s version %s, or greater, is required. Found version: %s", product, versionWanted, versionFound)
} }
return nil return nil
......
...@@ -90,6 +90,12 @@ func (d *Fusion5Driver) Start(vmxPath string, headless bool) error { ...@@ -90,6 +90,12 @@ func (d *Fusion5Driver) Start(vmxPath string, headless bool) error {
func (d *Fusion5Driver) Stop(vmxPath string) error { func (d *Fusion5Driver) Stop(vmxPath string) error {
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "stop", vmxPath, "hard") cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "stop", vmxPath, "hard")
if _, _, err := runAndLog(cmd); err != nil { if _, _, err := runAndLog(cmd); err != nil {
// Check if the VM is running. If its not, it was already stopped
running, rerr := d.IsRunning(vmxPath)
if rerr == nil && !running {
return nil
}
return err return err
} }
......
...@@ -11,7 +11,9 @@ import ( ...@@ -11,7 +11,9 @@ import (
"strings" "strings"
) )
// Fusion6Driver is a driver that can run VMware Fusion 5. const VMWARE_FUSION_VERSION = "6"
// Fusion6Driver is a driver that can run VMware Fusion 6.
type Fusion6Driver struct { type Fusion6Driver struct {
Fusion5Driver Fusion5Driver
} }
...@@ -22,6 +24,12 @@ func (d *Fusion6Driver) Clone(dst, src string) error { ...@@ -22,6 +24,12 @@ func (d *Fusion6Driver) Clone(dst, src string) error {
"clone", src, dst, "clone", src, dst,
"full") "full")
if _, _, err := runAndLog(cmd); err != nil { if _, _, err := runAndLog(cmd); err != nil {
if strings.Contains(err.Error(), "parameters was invalid") {
return fmt.Errorf(
"Clone is not supported with your version of Fusion. Packer " +
"only works with Fusion %s Professional or above. Please verify your version.", VMWARE_FUSION_VERSION)
}
return err return err
} }
...@@ -50,7 +58,7 @@ func (d *Fusion6Driver) Verify() error { ...@@ -50,7 +58,7 @@ func (d *Fusion6Driver) Verify() error {
return err return err
} }
versionRe := regexp.MustCompile(`(?i)VMware [a-z0-9-]+ (\d+\.\d+\.\d+)\s`) versionRe := regexp.MustCompile(`(?i)VMware [a-z0-9-]+ (\d+)\.`)
matches := versionRe.FindStringSubmatch(stderr.String()) matches := versionRe.FindStringSubmatch(stderr.String())
if matches == nil { if matches == nil {
return fmt.Errorf( return fmt.Errorf(
...@@ -58,10 +66,5 @@ func (d *Fusion6Driver) Verify() error { ...@@ -58,10 +66,5 @@ func (d *Fusion6Driver) Verify() error {
} }
log.Printf("Detected VMware version: %s", matches[1]) log.Printf("Detected VMware version: %s", matches[1])
if !strings.HasPrefix(matches[1], "6.") { return compareVersions(matches[1], VMWARE_FUSION_VERSION, "Fusion Professional")
return fmt.Errorf(
"Fusion 6 not detected. Got version: %s", matches[1])
}
return nil
} }
...@@ -198,4 +198,4 @@ func (d *Player5Driver) DhcpLeasesPath(device string) string { ...@@ -198,4 +198,4 @@ func (d *Player5Driver) DhcpLeasesPath(device string) string {
func (d *Player5Driver) VmnetnatConfPath() string { func (d *Player5Driver) VmnetnatConfPath() string {
return playerVmnetnatConfPath() return playerVmnetnatConfPath()
} }
\ No newline at end of file
...@@ -31,5 +31,5 @@ func playerVerifyVersion(version string) error { ...@@ -31,5 +31,5 @@ func playerVerifyVersion(version string) error {
} }
log.Printf("Detected VMware Player version: %s", matches[1]) log.Printf("Detected VMware Player version: %s", matches[1])
return compareVersions(matches[1], version) return compareVersions(matches[1], version, "Player")
} }
...@@ -64,5 +64,5 @@ func playerVerifyVersion(version string) error { ...@@ -64,5 +64,5 @@ func playerVerifyVersion(version string) error {
} }
log.Printf("Detected VMWare Player version: %s", matches[1]) log.Printf("Detected VMWare Player version: %s", matches[1])
return compareVersions(matches[1], version) return compareVersions(matches[1], version, "Player")
} }
...@@ -31,5 +31,5 @@ func workstationVerifyVersion(version string) error { ...@@ -31,5 +31,5 @@ func workstationVerifyVersion(version string) error {
} }
log.Printf("Detected VMware WS version: %s", matches[1]) log.Printf("Detected VMware WS version: %s", matches[1])
return compareVersions(matches[1], version) return compareVersions(matches[1], version, "Workstation")
} }
...@@ -75,5 +75,5 @@ func workstationVerifyVersion(version string) error { ...@@ -75,5 +75,5 @@ func workstationVerifyVersion(version string) error {
} }
log.Printf("Detected VMware WS version: %s", matches[1]) log.Printf("Detected VMware WS version: %s", matches[1])
return compareVersions(matches[1], version) return compareVersions(matches[1], version, "Workstation")
} }
package iso package common
// Interface to help find the host IP that is available from within // Interface to help find the host IP that is available from within
// the VMware virtual machines. // the VMware virtual machines.
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment