Merge branch 'master' into digitalocean
This commit is contained in:
commit
8f9e9abca1
|
@ -6,5 +6,4 @@
|
|||
/website/build
|
||||
.DS_Store
|
||||
.vagrant
|
||||
Vagrantfile
|
||||
test/.env
|
||||
|
|
|
@ -7,16 +7,9 @@ go:
|
|||
|
||||
install: make updatedeps
|
||||
script:
|
||||
- make test
|
||||
- GOMAXPROCS=2 make test
|
||||
#- go test -race ./...
|
||||
|
||||
notifications:
|
||||
irc:
|
||||
channels:
|
||||
- "chat.freenode.net#packer-tool"
|
||||
on_success: change
|
||||
on_failure: always
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: tip
|
||||
|
|
140
CHANGELOG.md
140
CHANGELOG.md
|
@ -1,42 +1,170 @@
|
|||
## 0.7.0 (unreleased)
|
||||
## 0.7.2 (unreleased)
|
||||
|
||||
FEATURES:
|
||||
|
||||
* core: Plugins are automatically discovered if they're named properly.
|
||||
Packer will look in the PWD and the directory with `packer` for
|
||||
binaries named `packer-TYPE-NAME`.
|
||||
* builder/vmware: VMware Player 6 is now supported. [GH-1168]
|
||||
* 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.
|
||||
Packer will look in the PWD and the directory with `packer` for
|
||||
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-vmx: Boot commands and the HTTP server are supported.
|
||||
[GH-1169]
|
||||
|
||||
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/digitalocean: Can set API URL endpoint. [GH-1448]
|
||||
* 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 "tcg" acceleration type. [GH-1395]
|
||||
* builder/virtualbox/all: `iso_interface` option to mount ISO with SATA. [GH-1200]
|
||||
* 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:
|
||||
|
||||
* 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: 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`
|
||||
[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: Auth works with latest formats on Google Cloud
|
||||
Console. [GH-1344]
|
||||
* builder/openstack: Region is not required. [GH-1418]
|
||||
* 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: Added some navigation keys [GH-1442]
|
||||
* builder/qemu: If headless, sdl display won't be used. [GH-1395]
|
||||
* 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: 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: 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-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]
|
||||
* 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)
|
||||
|
||||
|
|
2
Makefile
2
Makefile
|
@ -15,6 +15,6 @@ testrace:
|
|||
go test -race $(TEST) $(TESTARGS)
|
||||
|
||||
updatedeps:
|
||||
go get -u -v ./...
|
||||
go get -u -v -p 2 ./...
|
||||
|
||||
.PHONY: bin default test updatedeps
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
# -*- 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 {
|
|||
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)
|
||||
log.Printf("Uploading to chroot dir: %s", dst)
|
||||
tf, err := ioutil.TempFile("", "packer-amazon-chroot")
|
||||
|
|
|
@ -3,6 +3,8 @@ package chroot
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
|
@ -23,7 +25,9 @@ func (s *StepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
|
|||
volumeId := state.Get("volume_id").(string)
|
||||
|
||||
ui.Say("Creating snapshot...")
|
||||
createSnapResp, err := ec2conn.CreateSnapshot(volumeId, "")
|
||||
createSnapResp, err := ec2conn.CreateSnapshot(
|
||||
volumeId,
|
||||
fmt.Sprintf("Packer: %s", time.Now().String()))
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating snapshot: %s", err)
|
||||
state.Put("error", err)
|
||||
|
|
|
@ -13,6 +13,7 @@ type AccessConfig struct {
|
|||
AccessKey string `mapstructure:"access_key"`
|
||||
SecretKey string `mapstructure:"secret_key"`
|
||||
RawRegion string `mapstructure:"region"`
|
||||
Token string `mapstructure:"token"`
|
||||
}
|
||||
|
||||
// Auth returns a valid aws.Auth object for access to AWS services, or
|
||||
|
@ -23,6 +24,10 @@ func (c *AccessConfig) Auth() (aws.Auth, error) {
|
|||
// Store the accesskey and secret that we got...
|
||||
c.AccessKey = auth.AccessKey
|
||||
c.SecretKey = auth.SecretKey
|
||||
c.Token = auth.Token
|
||||
}
|
||||
if c.Token != "" {
|
||||
auth.Token = c.Token
|
||||
}
|
||||
|
||||
return auth, err
|
||||
|
|
|
@ -37,6 +37,7 @@ func (a *Artifact) Id() string {
|
|||
parts = append(parts, fmt.Sprintf("%s:%s", region, amiId))
|
||||
}
|
||||
|
||||
sort.Strings(parts)
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
|
@ -47,7 +48,7 @@ func (a *Artifact) String() string {
|
|||
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"))
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// BlockDevice
|
||||
|
@ -41,6 +44,51 @@ func buildBlockDevices(b []BlockDevice) []ec2.BlockDeviceMapping {
|
|||
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 {
|
||||
return buildBlockDevices(b.AMIMappings)
|
||||
}
|
||||
|
|
|
@ -7,38 +7,47 @@ import (
|
|||
)
|
||||
|
||||
func TestBlockDevice(t *testing.T) {
|
||||
ec2Mapping := []ec2.BlockDeviceMapping{
|
||||
ec2.BlockDeviceMapping{
|
||||
DeviceName: "/dev/sdb",
|
||||
VirtualName: "ephemeral0",
|
||||
SnapshotId: "snap-1234",
|
||||
VolumeType: "standard",
|
||||
VolumeSize: 8,
|
||||
DeleteOnTermination: true,
|
||||
IOPS: 1000,
|
||||
cases := []struct {
|
||||
Config *BlockDevice
|
||||
Result *ec2.BlockDeviceMapping
|
||||
}{
|
||||
{
|
||||
Config: &BlockDevice{
|
||||
DeviceName: "/dev/sdb",
|
||||
VirtualName: "ephemeral0",
|
||||
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{
|
||||
DeviceName: "/dev/sdb",
|
||||
VirtualName: "ephemeral0",
|
||||
SnapshotId: "snap-1234",
|
||||
VolumeType: "standard",
|
||||
VolumeSize: 8,
|
||||
DeleteOnTermination: true,
|
||||
IOPS: 1000,
|
||||
}
|
||||
for _, tc := range cases {
|
||||
blockDevices := BlockDevices{
|
||||
AMIMappings: []BlockDevice{*tc.Config},
|
||||
LaunchMappings: []BlockDevice{*tc.Config},
|
||||
}
|
||||
|
||||
blockDevices := BlockDevices{
|
||||
AMIMappings: []BlockDevice{blockDevice},
|
||||
LaunchMappings: []BlockDevice{blockDevice},
|
||||
}
|
||||
expected := []ec2.BlockDeviceMapping{*tc.Result}
|
||||
|
||||
if !reflect.DeepEqual(ec2Mapping, blockDevices.BuildAMIDevices()) {
|
||||
t.Fatalf("bad: %#v", ec2Mapping)
|
||||
}
|
||||
if !reflect.DeepEqual(expected, blockDevices.BuildAMIDevices()) {
|
||||
t.Fatalf("bad: %#v", expected)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(ec2Mapping, blockDevices.BuildLaunchDevices()) {
|
||||
t.Fatalf("bad: %#v", ec2Mapping)
|
||||
if !reflect.DeepEqual(expected, blockDevices.BuildLaunchDevices()) {
|
||||
t.Fatalf("bad: %#v", expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,9 +3,11 @@ package common
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/packer/common/uuid"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// RunConfig contains configuration for running an instance from a source
|
||||
|
@ -17,9 +19,12 @@ type RunConfig struct {
|
|||
InstanceType string `mapstructure:"instance_type"`
|
||||
RunTags map[string]string `mapstructure:"run_tags"`
|
||||
SourceAmi string `mapstructure:"source_ami"`
|
||||
SpotPrice string `mapstructure:"spot_price"`
|
||||
SpotPriceAutoProduct string `mapstructure:"spot_price_auto_product"`
|
||||
RawSSHTimeout string `mapstructure:"ssh_timeout"`
|
||||
SSHUsername string `mapstructure:"ssh_username"`
|
||||
SSHPrivateKeyFile string `mapstructure:"ssh_private_key_file"`
|
||||
SSHPrivateIp bool `mapstructure:"ssh_private_ip"`
|
||||
SSHPort int `mapstructure:"ssh_port"`
|
||||
SecurityGroupId string `mapstructure:"security_group_id"`
|
||||
SecurityGroupIds []string `mapstructure:"security_group_ids"`
|
||||
|
@ -45,6 +50,8 @@ func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error {
|
|||
templates := map[string]*string{
|
||||
"iam_instance_profile": &c.IamInstanceProfile,
|
||||
"instance_type": &c.InstanceType,
|
||||
"spot_price": &c.SpotPrice,
|
||||
"spot_price_auto_product": &c.SpotPriceAutoProduct,
|
||||
"ssh_timeout": &c.RawSSHTimeout,
|
||||
"ssh_username": &c.SSHUsername,
|
||||
"ssh_private_key_file": &c.SSHPrivateKeyFile,
|
||||
|
@ -78,7 +85,8 @@ func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error {
|
|||
}
|
||||
|
||||
if c.TemporaryKeyPairName == "" {
|
||||
c.TemporaryKeyPairName = "packer {{uuid}}"
|
||||
c.TemporaryKeyPairName = fmt.Sprintf(
|
||||
"packer %s", uuid.TimeOrderedUUID())
|
||||
}
|
||||
|
||||
// Validation
|
||||
|
@ -91,6 +99,13 @@ func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error {
|
|||
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 == "" {
|
||||
errs = append(errs, errors.New("An ssh_username must be specified"))
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
c := testConfig()
|
||||
c.SSHPort = 0
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
|
||||
// SSHAddress returns a function that can be given to the SSH communicator
|
||||
// for determining the SSH address based on the instance DNS name.
|
||||
func SSHAddress(e *ec2.EC2, port int) 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) {
|
||||
for j := 0; j < 2; j++ {
|
||||
var host string
|
||||
|
@ -19,7 +19,7 @@ func SSHAddress(e *ec2.EC2, port int) func(multistep.StateBag) (string, error) {
|
|||
if i.DNSName != "" {
|
||||
host = i.DNSName
|
||||
} else if i.VpcId != "" {
|
||||
if i.PublicIpAddress != "" {
|
||||
if i.PublicIpAddress != "" && !private {
|
||||
host = i.PublicIpAddress
|
||||
} else {
|
||||
host = i.PrivateIpAddress
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -38,6 +39,9 @@ func AMIStateRefreshFunc(conn *ec2.EC2, imageId string) StateRefreshFunc {
|
|||
if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidAMIID.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 AMIStateRefresh: %s", err)
|
||||
return nil, "", err
|
||||
|
@ -64,6 +68,9 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) StateRefreshFunc {
|
|||
if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidInstanceID.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 InstanceStateRefresh: %s", err)
|
||||
return nil, "", err
|
||||
|
@ -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
|
||||
// state.
|
||||
func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
|
||||
|
@ -125,8 +161,8 @@ func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
|
|||
}
|
||||
|
||||
if !found {
|
||||
fmt.Errorf("unexpected state '%s', wanted target '%s'", currentState, conf.Target)
|
||||
return
|
||||
err := fmt.Errorf("unexpected state '%s', wanted target '%s'", currentState, conf.Target)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -135,3 +171,11 @@ func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
|
|||
|
||||
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
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
type StepRunSourceInstance struct {
|
||||
|
@ -17,12 +21,15 @@ type StepRunSourceInstance struct {
|
|||
InstanceType string
|
||||
IamInstanceProfile string
|
||||
SourceAMI string
|
||||
SpotPrice string
|
||||
SpotPriceProduct string
|
||||
SubnetId string
|
||||
Tags map[string]string
|
||||
UserData string
|
||||
UserDataFile string
|
||||
|
||||
instance *ec2.Instance
|
||||
instance *ec2.Instance
|
||||
spotRequest *ec2.SpotRequestResult
|
||||
}
|
||||
|
||||
func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepAction {
|
||||
|
@ -47,21 +54,6 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi
|
|||
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...")
|
||||
imageResp, err := ec2conn.Images([]string{s.SourceAMI}, ec2.NewFilter())
|
||||
if err != nil {
|
||||
|
@ -82,29 +74,137 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi
|
|||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
runResp, err := ec2conn.RunInstances(runOpts)
|
||||
spotPrice := s.SpotPrice
|
||||
if spotPrice == "auto" {
|
||||
ui.Message(fmt.Sprintf(
|
||||
"Finding spot price for %s %s...",
|
||||
s.SpotPriceProduct, s.InstanceType))
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
var instanceId string
|
||||
|
||||
if spotPrice == "" {
|
||||
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,
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
instanceResp, err := ec2conn.Instances([]string{instanceId}, nil)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error launching source instance: %s", err)
|
||||
err := fmt.Errorf("Error finding source instance (%s): %s", instanceId, err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
s.instance = &runResp.Instances[0]
|
||||
s.instance = &instanceResp.Reservations[0].Instances[0]
|
||||
ui.Message(fmt.Sprintf("Instance ID: %s", s.instance.InstanceId))
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
ui.Say(fmt.Sprintf("Waiting for instance (%s) to become ready...", s.instance.InstanceId))
|
||||
stateChange := StateChangeConf{
|
||||
Pending: []string{"pending"},
|
||||
|
@ -122,6 +222,18 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi
|
|||
|
||||
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.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
|
|||
}
|
||||
|
||||
func (s *StepRunSourceInstance) Cleanup(state multistep.StateBag) {
|
||||
if s.instance == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
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
|
||||
// Cancel the spot request if it exists
|
||||
if s.spotRequest != nil {
|
||||
ui.Say("Cancelling the spot request...")
|
||||
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",
|
||||
}
|
||||
// Terminate the source instance if it exists
|
||||
if s.instance != nil {
|
||||
|
||||
WaitForState(&stateChange)
|
||||
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) {
|
|||
// Accumulate any errors
|
||||
errs := common.CheckUnusedConfig(md)
|
||||
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.RunConfig.Prepare(b.config.tpl)...)
|
||||
|
||||
|
@ -101,6 +102,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
&awscommon.StepRunSourceInstance{
|
||||
Debug: b.config.PackerDebug,
|
||||
ExpectedRootDevice: "ebs",
|
||||
SpotPrice: b.config.SpotPrice,
|
||||
SpotPriceProduct: b.config.SpotPriceAutoProduct,
|
||||
InstanceType: b.config.InstanceType,
|
||||
UserData: b.config.UserData,
|
||||
UserDataFile: b.config.UserDataFile,
|
||||
|
@ -113,12 +116,14 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
Tags: b.config.RunTags,
|
||||
},
|
||||
&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),
|
||||
SSHWaitTimeout: b.config.SSHTimeout(),
|
||||
},
|
||||
&common.StepProvision{},
|
||||
&stepStopInstance{},
|
||||
&stepStopInstance{SpotPrice: b.config.SpotPrice},
|
||||
// TODO(mitchellh): verify works with spots
|
||||
&stepModifyInstance{},
|
||||
&stepCreateAMI{},
|
||||
&awscommon.StepAMIRegionCopy{
|
||||
|
|
|
@ -8,7 +8,9 @@ import (
|
|||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type stepCreateAMI struct{}
|
||||
type stepCreateAMI struct {
|
||||
image *ec2.Image
|
||||
}
|
||||
|
||||
func (s *stepCreateAMI) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(config)
|
||||
|
@ -54,9 +56,38 @@ func (s *stepCreateAMI) Run(state multistep.StateBag) multistep.StepAction {
|
|||
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
|
||||
}
|
||||
|
||||
func (s *stepCreateAMI) Cleanup(multistep.StateBag) {
|
||||
// No cleanup...
|
||||
func (s *stepCreateAMI) Cleanup(state multistep.StateBag) {
|
||||
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 (
|
|||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type stepStopInstance struct{}
|
||||
type stepStopInstance struct {
|
||||
SpotPrice string
|
||||
}
|
||||
|
||||
func (s *stepStopInstance) Run(state multistep.StateBag) multistep.StepAction {
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
instance := state.Get("instance").(*ec2.Instance)
|
||||
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
|
||||
ui.Say("Stopping the source instance...")
|
||||
_, err := ec2conn.StopInstances(instance.InstanceId)
|
||||
|
|
|
@ -88,7 +88,8 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
"-e {{.PrivatePath}}/* " +
|
||||
"-d {{.Destination}} " +
|
||||
"-p {{.Prefix}} " +
|
||||
"--batch"
|
||||
"--batch " +
|
||||
"--no-filter"
|
||||
}
|
||||
|
||||
if b.config.X509UploadPath == "" {
|
||||
|
@ -98,6 +99,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
// Accumulate any errors
|
||||
errs := common.CheckUnusedConfig(md)
|
||||
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.RunConfig.Prepare(b.config.tpl)...)
|
||||
|
||||
|
@ -204,6 +206,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
},
|
||||
&awscommon.StepRunSourceInstance{
|
||||
Debug: b.config.PackerDebug,
|
||||
SpotPrice: b.config.SpotPrice,
|
||||
SpotPriceProduct: b.config.SpotPriceAutoProduct,
|
||||
InstanceType: b.config.InstanceType,
|
||||
IamInstanceProfile: b.config.IamInstanceProfile,
|
||||
UserData: b.config.UserData,
|
||||
|
@ -216,7 +220,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
Tags: b.config.RunTags,
|
||||
},
|
||||
&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),
|
||||
SSHWaitTimeout: b.config.SSHTimeout(),
|
||||
},
|
||||
|
|
|
@ -45,5 +45,5 @@ func (s *StepUploadX509Cert) uploadSingle(comm packer.Communicator, dst, src str
|
|||
}
|
||||
defer f.Close()
|
||||
|
||||
return comm.Upload(dst, f)
|
||||
return comm.Upload(dst, f, nil)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
)
|
||||
|
||||
const BuilderId = "packer.docker"
|
||||
const BuilderIdImport = "packer.post-processor.docker-import"
|
||||
|
||||
type Builder struct {
|
||||
config *Config
|
||||
|
@ -35,7 +36,12 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
&StepPull{},
|
||||
&StepRun{},
|
||||
&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
|
||||
|
@ -64,8 +70,17 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
return nil, rawErr.(error)
|
||||
}
|
||||
|
||||
var artifact packer.Artifact
|
||||
// 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
|
||||
}
|
||||
|
||||
|
|
|
@ -3,8 +3,6 @@ package docker
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/ActiveState/tail"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
|
@ -15,6 +13,9 @@ import (
|
|||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/ActiveState/tail"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type Communicator struct {
|
||||
|
@ -56,7 +57,7 @@ func (c *Communicator) Start(remote *packer.RemoteCmd) error {
|
|||
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
|
||||
tempfile, err := ioutil.TempFile(c.HostDir, "upload")
|
||||
if err != nil {
|
||||
|
@ -231,20 +232,42 @@ func (c *Communicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin_w io.W
|
|||
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() {
|
||||
for line := range tail.Lines {
|
||||
if remote.Stdout != nil {
|
||||
remote.Stdout.Write([]byte(line.Text + "\n"))
|
||||
} else {
|
||||
log.Printf("Command stdout: %#v", line.Text)
|
||||
defer close(doneCh)
|
||||
|
||||
for {
|
||||
select {
|
||||
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()
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
exitStatus := 1
|
||||
exitStatus = 1
|
||||
|
||||
// There is no process-independent way to get the REAL
|
||||
// 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
|
|||
|
||||
// Say that we ended, since if Docker itself failed, then
|
||||
// the command must've not run, or so we assume
|
||||
remote.SetExited(exitStatus)
|
||||
return
|
||||
goto REMOTE_EXIT
|
||||
}
|
||||
|
||||
// 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
|
|||
}
|
||||
|
||||
// Read the exit code
|
||||
exitRaw, err := ioutil.ReadFile(exitCodePath)
|
||||
exitRaw, err = ioutil.ReadFile(exitCodePath)
|
||||
if err != nil {
|
||||
log.Printf("Error executing: %s", err)
|
||||
remote.SetExited(254)
|
||||
return
|
||||
exitStatus = 254
|
||||
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 {
|
||||
log.Printf("Error executing: %s", err)
|
||||
remote.SetExited(254)
|
||||
return
|
||||
exitStatus = 254
|
||||
goto REMOTE_EXIT
|
||||
}
|
||||
exitStatus = int(exitStatusRaw)
|
||||
log.Printf("Executed command exit status: %d", exitStatus)
|
||||
|
||||
// Finally, we're done
|
||||
remote.SetExited(int(exitStatus))
|
||||
REMOTE_EXIT:
|
||||
// Wait for the tail to finish
|
||||
close(stopTailCh)
|
||||
<-doneCh
|
||||
|
||||
// Set the exit status which triggers waiters
|
||||
remote.SetExited(exitStatus)
|
||||
}
|
||||
|
|
|
@ -9,10 +9,18 @@ import (
|
|||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
|
||||
Commit bool
|
||||
ExportPath string `mapstructure:"export_path"`
|
||||
Image string
|
||||
Pull bool
|
||||
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
|
||||
}
|
||||
|
@ -34,9 +42,7 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
|||
// Defaults
|
||||
if len(c.RunCommand) == 0 {
|
||||
c.RunCommand = []string{
|
||||
"run",
|
||||
"-d", "-i", "-t",
|
||||
"-v", "{{.Volumes}}",
|
||||
"{{.Image}}",
|
||||
"/bin/bash",
|
||||
}
|
||||
|
@ -58,8 +64,12 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
|||
errs := common.CheckUnusedConfig(md)
|
||||
|
||||
templates := map[string]*string{
|
||||
"export_path": &c.ExportPath,
|
||||
"image": &c.Image,
|
||||
"export_path": &c.ExportPath,
|
||||
"image": &c.Image,
|
||||
"login_email": &c.LoginEmail,
|
||||
"login_username": &c.LoginUsername,
|
||||
"login_password": &c.LoginPassword,
|
||||
"login_server": &c.LoginServer,
|
||||
}
|
||||
|
||||
for n, ptr := range templates {
|
||||
|
@ -71,9 +81,15 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
if c.ExportPath == "" {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("export_path must be specified"))
|
||||
for k, v := range c.Volumes {
|
||||
var err error
|
||||
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 == "" {
|
||||
|
@ -81,6 +97,11 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
|||
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 {
|
||||
return nil, nil, errs
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ func TestConfigPrepare_exportPath(t *testing.T) {
|
|||
// No export path
|
||||
delete(raw, "export_path")
|
||||
_, warns, errs := NewConfig(raw)
|
||||
testConfigErr(t, warns, errs)
|
||||
testConfigOk(t, warns, errs)
|
||||
|
||||
// Good export path
|
||||
raw["export_path"] = "good"
|
||||
|
@ -55,6 +55,20 @@ func TestConfigPrepare_exportPath(t *testing.T) {
|
|||
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) {
|
||||
raw := testConfig()
|
||||
|
||||
|
|
|
@ -8,6 +8,9 @@ import (
|
|||
// Docker. The Driver interface also allows the steps to be tested since
|
||||
// a mock driver can be shimmed in.
|
||||
type Driver interface {
|
||||
// Commit the container to a tag
|
||||
Commit(id string) (string, error)
|
||||
|
||||
// Delete an image that is imported into Docker
|
||||
DeleteImage(id string) error
|
||||
|
||||
|
@ -17,12 +20,22 @@ type Driver interface {
|
|||
// Import imports a container from a tar file
|
||||
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(image string) error
|
||||
|
||||
// Push pushes an image to a Docker index/registry.
|
||||
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,
|
||||
// along with a potential error.
|
||||
StartContainer(*ContainerConfig) (string, error)
|
||||
|
@ -30,6 +43,9 @@ type Driver interface {
|
|||
// StopContainer forcibly stops a container.
|
||||
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() error
|
||||
}
|
||||
|
@ -43,6 +59,5 @@ type ContainerConfig struct {
|
|||
|
||||
// This is the template that is used for the RunCommand in the ContainerConfig.
|
||||
type startContainerTemplate struct {
|
||||
Image string
|
||||
Volumes string
|
||||
Image string
|
||||
}
|
||||
|
|
|
@ -3,17 +3,21 @@ package docker
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type DockerDriver struct {
|
||||
Ui packer.Ui
|
||||
Tpl *packer.ConfigTemplate
|
||||
|
||||
l sync.Mutex
|
||||
}
|
||||
|
||||
func (d *DockerDriver) DeleteImage(id string) error {
|
||||
|
@ -35,6 +39,27 @@ func (d *DockerDriver) DeleteImage(id string) error {
|
|||
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 {
|
||||
var stderr bytes.Buffer
|
||||
cmd := exec.Command("docker", "export", id)
|
||||
|
@ -88,6 +113,44 @@ func (d *DockerDriver) Import(path string, repo string) (string, error) {
|
|||
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 {
|
||||
cmd := exec.Command("docker", "pull", image)
|
||||
return runAndStream(cmd, d.Ui)
|
||||
|
@ -98,27 +161,43 @@ func (d *DockerDriver) Push(name string) error {
|
|||
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) {
|
||||
// Build up the template data
|
||||
var tplData startContainerTemplate
|
||||
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 := config.RunCommand
|
||||
for i, v := range args {
|
||||
var err error
|
||||
args[i], err = d.Tpl.Process(v, &tplData)
|
||||
args := []string{"run"}
|
||||
for host, guest := range config.Volumes {
|
||||
args = append(args, "-v", fmt.Sprintf("%s:%s", host, guest))
|
||||
}
|
||||
for _, v := range config.RunCommand {
|
||||
v, err := d.Tpl.Process(v, &tplData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
args = append(args, v)
|
||||
}
|
||||
d.Ui.Message(fmt.Sprintf(
|
||||
"Run command: docker %s", strings.Join(args, " ")))
|
||||
|
@ -149,7 +228,29 @@ func (d *DockerDriver) StartContainer(config *ContainerConfig) (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 {
|
||||
|
|
|
@ -6,6 +6,11 @@ import (
|
|||
|
||||
// MockDriver is a driver implementation that can be used for tests.
|
||||
type MockDriver struct {
|
||||
CommitCalled bool
|
||||
CommitContainerId string
|
||||
CommitImageId string
|
||||
CommitErr error
|
||||
|
||||
DeleteImageCalled bool
|
||||
DeleteImageId string
|
||||
DeleteImageErr error
|
||||
|
@ -16,10 +21,31 @@ type MockDriver struct {
|
|||
ImportId string
|
||||
ImportErr error
|
||||
|
||||
LoginCalled bool
|
||||
LoginEmail string
|
||||
LoginUsername string
|
||||
LoginPassword string
|
||||
LoginRepo string
|
||||
LoginErr error
|
||||
|
||||
LogoutCalled bool
|
||||
LogoutRepo string
|
||||
LogoutErr error
|
||||
|
||||
PushCalled bool
|
||||
PushName string
|
||||
PushErr error
|
||||
|
||||
SaveImageCalled bool
|
||||
SaveImageId string
|
||||
SaveImageReader io.Reader
|
||||
SaveImageError error
|
||||
|
||||
TagImageCalled bool
|
||||
TagImageImageId string
|
||||
TagImageRepo string
|
||||
TagImageErr error
|
||||
|
||||
ExportReader io.Reader
|
||||
ExportError error
|
||||
PullError error
|
||||
|
@ -39,6 +65,12 @@ type MockDriver struct {
|
|||
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 {
|
||||
d.DeleteImageCalled = true
|
||||
d.DeleteImageId = id
|
||||
|
@ -66,6 +98,21 @@ func (d *MockDriver) Import(path, repo string) (string, error) {
|
|||
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 {
|
||||
d.PullCalled = true
|
||||
d.PullImage = image
|
||||
|
@ -78,6 +125,20 @@ func (d *MockDriver) Push(name string) error {
|
|||
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) {
|
||||
d.StartCalled = true
|
||||
d.StartConfig = config
|
||||
|
@ -90,6 +151,13 @@ func (d *MockDriver) StopContainer(id string) error {
|
|||
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 {
|
||||
d.VerifyCalled = true
|
||||
return d.VerifyError
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
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) {}
|
|
@ -0,0 +1,66 @@
|
|||
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{}
|
|||
|
||||
func (s *StepExport) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
|
||||
driver := state.Get("driver").(Driver)
|
||||
containerId := state.Get("container_id").(string)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
|
|
@ -20,6 +20,29 @@ func (s *StepPull) Run(state multistep.StateBag) multistep.StepAction {
|
|||
}
|
||||
|
||||
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 {
|
||||
err := fmt.Errorf("Error pulling Docker image: %s", err)
|
||||
state.Put("error", err)
|
||||
|
|
|
@ -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) {
|
||||
state := testState(t)
|
||||
step := new(StepPull)
|
||||
|
|
|
@ -19,11 +19,14 @@ func (s *StepRun) Run(state multistep.StateBag) multistep.StepAction {
|
|||
runConfig := ContainerConfig{
|
||||
Image: config.Image,
|
||||
RunCommand: config.RunCommand,
|
||||
Volumes: map[string]string{
|
||||
tempDir: "/packer-files",
|
||||
},
|
||||
Volumes: make(map[string]string),
|
||||
}
|
||||
|
||||
for host, container := range config.Volumes {
|
||||
runConfig.Volumes[host] = container
|
||||
}
|
||||
runConfig.Volumes[tempDir] = "/packer-files"
|
||||
|
||||
ui.Say("Starting docker container...")
|
||||
containerId, err := driver.StartContainer(&runConfig)
|
||||
if err != nil {
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
package googlecompute
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
)
|
||||
|
||||
// accountFile represents the structure of the account file JSON file.
|
||||
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 {
|
||||
AuthURI string `json:"auth_uri"`
|
||||
ClientEmail string `json:"client_email"`
|
||||
ClientId string `json:"client_id"`
|
||||
TokenURI string `json:"token_uri"`
|
||||
}
|
||||
}
|
||||
|
||||
func loadJSON(result interface{}, path string) error {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
dec := json.NewDecoder(f)
|
||||
return dec.Decode(result)
|
||||
}
|
|
@ -35,7 +35,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
// representing a GCE machine image.
|
||||
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
package googlecompute
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
// clientSecrets represents the client secrets of a GCE service account.
|
||||
type clientSecrets struct {
|
||||
Web struct {
|
||||
AuthURI string `json:"auth_uri"`
|
||||
ClientEmail string `json:"client_email"`
|
||||
ClientId string `json:"client_id"`
|
||||
TokenURI string `json:"token_uri"`
|
||||
}
|
||||
}
|
||||
|
||||
// loadClientSecrets loads the GCE client secrets file identified by path.
|
||||
func loadClientSecrets(path string) (*clientSecrets, error) {
|
||||
var cs *clientSecrets
|
||||
secretBytes, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(secretBytes, &cs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cs, nil
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
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 (
|
|||
type Config struct {
|
||||
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"`
|
||||
ClientSecretsFile string `mapstructure:"client_secrets_file"`
|
||||
DiskSizeGb int64 `mapstructure:"disk_size"`
|
||||
ImageName string `mapstructure:"image_name"`
|
||||
ImageDescription string `mapstructure:"image_description"`
|
||||
|
@ -25,9 +28,6 @@ type Config struct {
|
|||
MachineType string `mapstructure:"machine_type"`
|
||||
Metadata map[string]string `mapstructure:"metadata"`
|
||||
Network string `mapstructure:"network"`
|
||||
Passphrase string `mapstructure:"passphrase"`
|
||||
PrivateKeyFile string `mapstructure:"private_key_file"`
|
||||
ProjectId string `mapstructure:"project_id"`
|
||||
SourceImage string `mapstructure:"source_image"`
|
||||
SourceImageProjectId string `mapstructure:"source_image_project_id"`
|
||||
SSHUsername string `mapstructure:"ssh_username"`
|
||||
|
@ -37,7 +37,8 @@ type Config struct {
|
|||
Tags []string `mapstructure:"tags"`
|
||||
Zone string `mapstructure:"zone"`
|
||||
|
||||
clientSecrets *clientSecrets
|
||||
account accountFile
|
||||
clientSecrets clientSecretsFile
|
||||
instanceName string
|
||||
privateKeyBytes []byte
|
||||
sshTimeout time.Duration
|
||||
|
@ -104,15 +105,15 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
|||
|
||||
// Process Templates
|
||||
templates := map[string]*string{
|
||||
"account_file": &c.AccountFile,
|
||||
"client_secrets_file": &c.ClientSecretsFile,
|
||||
|
||||
"bucket_name": &c.BucketName,
|
||||
"client_secrets_file": &c.ClientSecretsFile,
|
||||
"image_name": &c.ImageName,
|
||||
"image_description": &c.ImageDescription,
|
||||
"instance_name": &c.InstanceName,
|
||||
"machine_type": &c.MachineType,
|
||||
"network": &c.Network,
|
||||
"passphrase": &c.Passphrase,
|
||||
"private_key_file": &c.PrivateKeyFile,
|
||||
"project_id": &c.ProjectId,
|
||||
"source_image": &c.SourceImage,
|
||||
"source_image_project_id": &c.SourceImageProjectId,
|
||||
|
@ -137,16 +138,16 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
|||
errs, errors.New("a bucket_name must be specified"))
|
||||
}
|
||||
|
||||
if c.AccountFile == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("an account_file must be specified"))
|
||||
}
|
||||
|
||||
if c.ClientSecretsFile == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("a client_secrets_file must be specified"))
|
||||
}
|
||||
|
||||
if c.PrivateKeyFile == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("a private_key_file must be specified"))
|
||||
}
|
||||
|
||||
if c.ProjectId == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("a project_id must be specified"))
|
||||
|
@ -177,22 +178,17 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
|||
}
|
||||
c.stateTimeout = stateTimeout
|
||||
|
||||
if c.ClientSecretsFile != "" {
|
||||
// Load the client secrets file.
|
||||
cs, err := loadClientSecrets(c.ClientSecretsFile)
|
||||
if err != nil {
|
||||
if c.AccountFile != "" {
|
||||
if err := loadJSON(&c.account, c.AccountFile); err != nil {
|
||||
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 != "" {
|
||||
// Load the private key.
|
||||
c.privateKeyBytes, err = processPrivateKeyFile(c.PrivateKeyFile, c.Passphrase)
|
||||
if err != nil {
|
||||
if c.ClientSecretsFile != "" {
|
||||
if err := loadJSON(&c.clientSecrets, c.ClientSecretsFile); err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Failed loading private key file: %s", err))
|
||||
errs, fmt.Errorf("Failed parsing client secrets file: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
package googlecompute
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testConfig(t *testing.T) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"account_file": testAccountFile(t),
|
||||
"bucket_name": "foo",
|
||||
"client_secrets_file": testClientSecretsFile(t),
|
||||
"private_key_file": testPrivateKeyFile(t),
|
||||
"project_id": "hashicorp",
|
||||
"source_image": "foo",
|
||||
"zone": "us-east-1a",
|
||||
|
@ -84,16 +85,6 @@ func TestConfigPrepare(t *testing.T) {
|
|||
true,
|
||||
},
|
||||
|
||||
{
|
||||
"private_key_file",
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"private_key_file",
|
||||
testPrivateKeyFile(t),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"private_key_file",
|
||||
"/tmp/i/should/not/exist",
|
||||
|
@ -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 {
|
|||
const DriverScopes string = "https://www.googleapis.com/auth/compute " +
|
||||
"https://www.googleapis.com/auth/devstorage.full_control"
|
||||
|
||||
func NewDriverGCE(ui packer.Ui, projectId string, c *clientSecrets, key []byte) (Driver, error) {
|
||||
log.Printf("[INFO] Requesting token...")
|
||||
log.Printf("[INFO] -- Email: %s", c.Web.ClientEmail)
|
||||
func NewDriverGCE(ui packer.Ui, p string, a *accountFile, c *clientSecretsFile) (Driver, error) {
|
||||
// Get the token for use in our requests
|
||||
log.Printf("[INFO] Requesting Google token...")
|
||||
log.Printf("[INFO] -- Email: %s", a.ClientEmail)
|
||||
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)
|
||||
jwtTok := jwt.NewToken(c.Web.ClientEmail, DriverScopes, key)
|
||||
jwtTok := jwt.NewToken(
|
||||
a.ClientEmail,
|
||||
DriverScopes,
|
||||
[]byte(a.PrivateKey))
|
||||
jwtTok.ClaimSet.Aud = c.Web.TokenURI
|
||||
token, err := jwtTok.Assert(new(http.Client))
|
||||
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{
|
||||
Config: &oauth.Config{
|
||||
ClientId: c.Web.ClientId,
|
||||
ClientId: a.ClientId,
|
||||
Scope: DriverScopes,
|
||||
TokenURL: c.Web.TokenURI,
|
||||
AuthURL: c.Web.AuthURI,
|
||||
|
@ -46,14 +51,14 @@ func NewDriverGCE(ui packer.Ui, projectId string, c *clientSecrets, key []byte)
|
|||
Token: token,
|
||||
}
|
||||
|
||||
log.Printf("[INFO] Instantiating client...")
|
||||
log.Printf("[INFO] Instantiating GCE client...")
|
||||
service, err := compute.New(transport.Client())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &driverGCE{
|
||||
projectId: projectId,
|
||||
projectId: p,
|
||||
service: service,
|
||||
ui: ui,
|
||||
}, nil
|
||||
|
|
|
@ -24,6 +24,25 @@ func (config *Config) getImage() Image {
|
|||
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.
|
||||
func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
|
@ -39,9 +58,7 @@ func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction
|
|||
DiskSizeGb: config.DiskSizeGb,
|
||||
Image: config.getImage(),
|
||||
MachineType: config.MachineType,
|
||||
Metadata: map[string]string{
|
||||
"sshKeys": fmt.Sprintf("%s:%s", config.SSHUsername, sshPublicKey),
|
||||
},
|
||||
Metadata: config.getInstanceMetadata(sshPublicKey),
|
||||
Name: name,
|
||||
Network: config.Network,
|
||||
Tags: config.Tags,
|
||||
|
|
|
@ -5,11 +5,12 @@ import (
|
|||
"fmt"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
)
|
||||
|
||||
// AccessConfig is for common configuration related to openstack access
|
||||
|
|
|
@ -2,8 +2,9 @@ package openstack
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"log"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
)
|
||||
|
||||
// Artifact is an artifact implementation that contains built images.
|
||||
|
|
|
@ -8,8 +8,9 @@ import (
|
|||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"log"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
)
|
||||
|
||||
// The unique ID for this builder
|
||||
|
|
|
@ -5,9 +5,10 @@ import (
|
|||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/racker/perigee"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
)
|
||||
|
||||
// StateRefreshFunc is a function type used for StateChangeConf that is
|
||||
|
|
|
@ -5,8 +5,9 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
)
|
||||
|
||||
// SSHAddress returns a function that can be given to the SSH communicator
|
||||
|
|
|
@ -4,7 +4,8 @@ import (
|
|||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/rackspace/gophercloud"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
)
|
||||
|
||||
type StepAllocateIp struct {
|
||||
|
|
|
@ -4,9 +4,10 @@ import (
|
|||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
)
|
||||
|
||||
type stepCreateImage struct{}
|
||||
|
|
|
@ -5,10 +5,11 @@ import (
|
|||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common/uuid"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
)
|
||||
|
||||
type StepKeyPair struct {
|
||||
|
|
|
@ -4,8 +4,9 @@ import (
|
|||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"log"
|
||||
|
||||
"github.com/mitchellh/gophercloud-fork-40444fb"
|
||||
)
|
||||
|
||||
type StepRunSourceServer struct {
|
||||
|
|
|
@ -15,8 +15,11 @@ import (
|
|||
// versions out of the builder steps, so sometimes the methods are
|
||||
// extremely specific.
|
||||
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(string, string, string) error
|
||||
Import(string, string, string, bool) error
|
||||
|
||||
// Checks if the VM with the given name is running.
|
||||
IsRunning(string) (bool, error)
|
||||
|
@ -38,7 +41,7 @@ type Driver interface {
|
|||
// Version reads the version of Parallels that is installed.
|
||||
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
|
||||
|
||||
// Finds the MAC address of the NIC nic0
|
||||
|
|
|
@ -3,6 +3,7 @@ package common
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
@ -19,7 +20,7 @@ type Parallels9Driver struct {
|
|||
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")
|
||||
if err != nil {
|
||||
|
@ -31,9 +32,12 @@ func (d *Parallels9Driver) Import(name, srcPath, dstDir string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
srcMac, err := getFirtsMacAddress(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
srcMac := "auto"
|
||||
if !reassignMac {
|
||||
srcMac, err = getFirtsMacAddress(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = d.Prlctl("clone", srcId, "--name", name, "--dst", dstDir)
|
||||
|
@ -91,6 +95,29 @@ func getAppPath(bundleId string) (string, error) {
|
|||
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) {
|
||||
var stdout bytes.Buffer
|
||||
|
||||
|
@ -179,11 +206,29 @@ func (d *Parallels9Driver) Version() (string, error) {
|
|||
func (d *Parallels9Driver) SendKeyScanCodes(vmName string, codes ...string) error {
|
||||
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)
|
||||
cmd := exec.Command("prltype", args...)
|
||||
args = prepend(f.Name(), args)
|
||||
cmd := exec.Command("/usr/bin/python", args...)
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
err = cmd.Run()
|
||||
|
||||
stdoutString := strings.TrimSpace(stdout.String())
|
||||
stderrString := strings.TrimSpace(stderr.String())
|
||||
|
|
|
@ -5,6 +5,12 @@ import "sync"
|
|||
type DriverMock struct {
|
||||
sync.Mutex
|
||||
|
||||
DeviceAddCdRomCalled bool
|
||||
DeviceAddCdRomName string
|
||||
DeviceAddCdRomImage string
|
||||
DeviceAddCdRomResult string
|
||||
DeviceAddCdRomErr error
|
||||
|
||||
ImportCalled bool
|
||||
ImportName string
|
||||
ImportSrcPath string
|
||||
|
@ -45,7 +51,14 @@ type DriverMock struct {
|
|||
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.ImportName = name
|
||||
d.ImportSrcPath = srcPath
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
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()
|
||||
`
|
|
@ -1,12 +1,12 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"code.google.com/p/go.crypto/ssh"
|
||||
"fmt"
|
||||
|
||||
"code.google.com/p/go.crypto/ssh"
|
||||
"github.com/mitchellh/multistep"
|
||||
commonssh "github.com/mitchellh/packer/common/ssh"
|
||||
packerssh "github.com/mitchellh/packer/communicator/ssh"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
func SSHAddress(state multistep.StateBag) (string, error) {
|
||||
|
@ -35,7 +35,7 @@ func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*ssh.ClientConfig
|
|||
}
|
||||
|
||||
if config.SSHKeyPath != "" {
|
||||
signer, err := sshKeyToSigner(config.SSHKeyPath)
|
||||
signer, err := commonssh.FileSigner(config.SSHKeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -49,23 +49,3 @@ func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*ssh.ClientConfig
|
|||
}, 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
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
commonssh "github.com/mitchellh/packer/common/ssh"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type SSHConfig struct {
|
||||
|
@ -46,7 +48,7 @@ func (c *SSHConfig) Prepare(t *packer.ConfigTemplate) []error {
|
|||
if c.SSHKeyPath != "" {
|
||||
if _, err := os.Stat(c.SSHKeyPath); err != nil {
|
||||
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
|
||||
} else if _, err := 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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,11 +39,10 @@ func (s *StepAttachFloppy) Run(state multistep.StateBag) multistep.StepAction {
|
|||
"set", vmName,
|
||||
"--device-del", "fdd0",
|
||||
}
|
||||
if err := driver.Prlctl(del_command...); err != nil {
|
||||
state.Put("error", fmt.Errorf("Error deleting floppy: %s", err))
|
||||
}
|
||||
ui.Say("Attaching floppy disk...")
|
||||
// This will almost certainly fail with 'The fdd0 device does not exist.'
|
||||
driver.Prlctl(del_command...)
|
||||
|
||||
ui.Say("Attaching floppy disk...")
|
||||
// Attaching the floppy disk
|
||||
add_command := []string{
|
||||
"set", vmName,
|
||||
|
|
|
@ -17,8 +17,8 @@ import (
|
|||
// vmName string
|
||||
//
|
||||
// Produces:
|
||||
// attachedToolsIso boolean
|
||||
type StepAttachParallelsTools struct {
|
||||
cdromDevice string
|
||||
ParallelsToolsMode string
|
||||
}
|
||||
|
||||
|
@ -37,27 +37,25 @@ func (s *StepAttachParallelsTools) Run(state multistep.StateBag) multistep.StepA
|
|||
parallelsToolsPath := state.Get("parallels_tools_path").(string)
|
||||
|
||||
// Attach the guest additions to the computer
|
||||
ui.Say("Attaching Parallels Tools ISO onto IDE controller...")
|
||||
command := []string{
|
||||
"set", vmName,
|
||||
"--device-add", "cdrom",
|
||||
"--image", parallelsToolsPath,
|
||||
}
|
||||
if err := driver.Prlctl(command...); err != nil {
|
||||
err := fmt.Errorf("Error attaching Parallels Tools: %s", err)
|
||||
ui.Say("Attaching Parallels Tools ISO to the new CD/DVD drive...")
|
||||
|
||||
cdrom, err := driver.DeviceAddCdRom(vmName, parallelsToolsPath)
|
||||
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error attaching Parallels Tools ISO: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Set some state so we know to remove
|
||||
state.Put("attachedToolsIso", true)
|
||||
// Track the device name so that we can can delete later
|
||||
s.cdromDevice = cdrom
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepAttachParallelsTools) Cleanup(state multistep.StateBag) {
|
||||
if _, ok := state.GetOk("attachedToolsIso"); !ok {
|
||||
if s.cdromDevice == "" {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -66,14 +64,10 @@ func (s *StepAttachParallelsTools) Cleanup(state multistep.StateBag) {
|
|||
vmName := state.Get("vmName").(string)
|
||||
|
||||
log.Println("Detaching Parallels Tools ISO...")
|
||||
cdDevice := "cdrom0"
|
||||
if _, ok := state.GetOk("attachedIso"); ok {
|
||||
cdDevice = "cdrom1"
|
||||
}
|
||||
|
||||
command := []string{
|
||||
"set", vmName,
|
||||
"--device-del", cdDevice,
|
||||
"--device-del", s.cdromDevice,
|
||||
}
|
||||
|
||||
if err := driver.Prlctl(command...); err != nil {
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
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) {
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
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 {
|
|||
Name string
|
||||
}
|
||||
|
||||
// This step "types" the boot command into the VM via prltype, built on the
|
||||
// Parallels Virtualization SDK - C API.
|
||||
// This step "types" the boot command into the VM via the prltype script, built on the
|
||||
// Parallels Virtualization SDK - Python API.
|
||||
//
|
||||
// Uses:
|
||||
// driver Driver
|
||||
|
|
|
@ -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'",
|
||||
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)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
|
|
|
@ -33,7 +33,7 @@ func (s *StepUploadVersion) Run(state multistep.StateBag) multistep.StepAction {
|
|||
ui.Say(fmt.Sprintf("Uploading Parallels version info (%s)", version))
|
||||
var data bytes.Buffer
|
||||
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))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
|
|
@ -294,7 +294,6 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
Command: b.config.ShutdownCommand,
|
||||
Timeout: b.config.ShutdownTimeout,
|
||||
},
|
||||
new(parallelscommon.StepRemoveDevices),
|
||||
}
|
||||
|
||||
// Setup the state bag
|
||||
|
|
|
@ -11,10 +11,14 @@ import (
|
|||
// This step attaches the ISO to the virtual machine.
|
||||
//
|
||||
// Uses:
|
||||
// driver Driver
|
||||
// isoPath string
|
||||
// ui packer.Ui
|
||||
// vmName string
|
||||
//
|
||||
// Produces:
|
||||
type stepAttachISO struct {
|
||||
diskPath string
|
||||
cdromDevice string
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// Attach the disk to the controller
|
||||
ui.Say("Attaching ISO onto IDE controller...")
|
||||
command := []string{
|
||||
"set", vmName,
|
||||
"--device-set", "cdrom0",
|
||||
"--image", isoPath,
|
||||
"--enable", "--connect",
|
||||
}
|
||||
if err := driver.Prlctl(command...); err != nil {
|
||||
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 some state so we know to remove
|
||||
state.Put("attachedIso", true)
|
||||
// Set new boot order
|
||||
ui.Say("Setting the boot order...")
|
||||
command := []string{
|
||||
"set", vmName,
|
||||
"--device-bootorder", fmt.Sprintf("hdd0 %s cdrom0 net0", cdrom),
|
||||
}
|
||||
|
||||
if err := driver.Prlctl(command...); err != nil {
|
||||
err := fmt.Errorf("Error setting the boot order: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Disable 'cdrom0' device
|
||||
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
|
||||
}
|
||||
|
||||
func (s *stepAttachISO) Cleanup(state multistep.StateBag) {
|
||||
if _, ok := state.GetOk("attachedIso"); !ok {
|
||||
driver := state.Get("driver").(parallelscommon.Driver)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
vmName := state.Get("vmName").(string)
|
||||
|
||||
// Enable 'cdrom0' device back
|
||||
log.Println("Enabling default CD/DVD drive...")
|
||||
command := []string{
|
||||
"set", vmName,
|
||||
"--device-set", "cdrom0", "--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
|
||||
}
|
||||
|
||||
driver := state.Get("driver").(parallelscommon.Driver)
|
||||
vmName := state.Get("vmName").(string)
|
||||
|
||||
command := []string{
|
||||
log.Println("Detaching ISO...")
|
||||
command = []string{
|
||||
"set", vmName,
|
||||
"--device-set", "cdrom0",
|
||||
"--enable", "--disconnect",
|
||||
"--device-del", s.cdromDevice,
|
||||
}
|
||||
|
||||
// Remove the ISO, ignore errors
|
||||
log.Println("Detaching ISO...")
|
||||
driver.Prlctl(command...)
|
||||
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 {
|
|||
|
||||
command := []string{
|
||||
"set", vmName,
|
||||
"--device-set", "hdd0",
|
||||
"--device-add", "hdd",
|
||||
"--size", strconv.FormatUint(uint64(config.DiskSize), 10),
|
||||
"--iface", config.HardDriveInterface,
|
||||
}
|
||||
|
|
|
@ -2,10 +2,10 @@ package iso
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
parallelscommon "github.com/mitchellh/packer/builder/parallels/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// This step creates the actual virtual machine.
|
||||
|
@ -21,16 +21,15 @@ func (s *stepCreateVM) Run(state multistep.StateBag) multistep.StepAction {
|
|||
config := state.Get("config").(*config)
|
||||
driver := state.Get("driver").(parallelscommon.Driver)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
name := config.VMName
|
||||
path := filepath.Join(".", config.OutputDir)
|
||||
|
||||
commands := make([][]string, 8)
|
||||
commands[0] = []string{
|
||||
"create", name,
|
||||
"--distribution", config.GuestOSType,
|
||||
"--dst", path,
|
||||
"--dst", config.OutputDir,
|
||||
"--vmtype", "vm",
|
||||
"--no-hdd",
|
||||
}
|
||||
commands[1] = []string{"set", name, "--cpus", "1"}
|
||||
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
|
|||
Command: b.config.ShutdownCommand,
|
||||
Timeout: b.config.ShutdownTimeout,
|
||||
},
|
||||
new(parallelscommon.StepRemoveDevices),
|
||||
}
|
||||
|
||||
// Run the steps.
|
||||
|
|
|
@ -2,10 +2,11 @@ package pvm
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
parallelscommon "github.com/mitchellh/packer/builder/parallels/common"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Config is the configuration structure for the builder.
|
||||
|
@ -23,6 +24,7 @@ type Config struct {
|
|||
BootCommand []string `mapstructure:"boot_command"`
|
||||
SourcePath string `mapstructure:"source_path"`
|
||||
VMName string `mapstructure:"vm_name"`
|
||||
ReassignMac bool `mapstructure:"reassign_mac"`
|
||||
|
||||
tpl *packer.ConfigTemplate
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ func (s *StepImport) Run(state multistep.StateBag) multistep.StepAction {
|
|||
config := state.Get("config").(*Config)
|
||||
|
||||
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)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
|
|
|
@ -3,15 +3,17 @@ package qemu
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"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"
|
||||
|
@ -54,6 +56,14 @@ var diskInterface = map[string]bool{
|
|||
"virtio": true,
|
||||
}
|
||||
|
||||
var diskCache = map[string]bool{
|
||||
"writethrough": true,
|
||||
"writeback": true,
|
||||
"none": true,
|
||||
"unsafe": true,
|
||||
"directsync": true,
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
config config
|
||||
runner multistep.Runner
|
||||
|
@ -66,6 +76,7 @@ type config struct {
|
|||
BootCommand []string `mapstructure:"boot_command"`
|
||||
DiskInterface string `mapstructure:"disk_interface"`
|
||||
DiskSize uint `mapstructure:"disk_size"`
|
||||
DiskCache string `mapstructure:"disk_cache`
|
||||
FloppyFiles []string `mapstructure:"floppy_files"`
|
||||
Format string `mapstructure:"format"`
|
||||
Headless bool `mapstructure:"headless"`
|
||||
|
@ -124,6 +135,10 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
b.config.DiskSize = 40000
|
||||
}
|
||||
|
||||
if b.config.DiskCache == "" {
|
||||
b.config.DiskCache = "writeback"
|
||||
}
|
||||
|
||||
if b.config.Accelerator == "" {
|
||||
b.config.Accelerator = "kvm"
|
||||
}
|
||||
|
@ -137,7 +152,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
}
|
||||
|
||||
if b.config.MachineType == "" {
|
||||
b.config.MachineType = "pc-1.0"
|
||||
b.config.MachineType = "pc"
|
||||
}
|
||||
|
||||
if b.config.OutputDir == "" {
|
||||
|
@ -278,6 +293,11 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
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 {
|
||||
errs = packer.MultiErrorAppend(
|
||||
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) {
|
|||
if _, err := os.Stat(b.config.SSHKeyPath); err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
|
||||
} else if _, err := sshKeyToSigner(b.config.SSHKeyPath); err != nil {
|
||||
} else if _, err := commonssh.FileSigner(b.config.SSHKeyPath); err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
package qemu
|
||||
|
||||
import (
|
||||
gossh "code.google.com/p/go.crypto/ssh"
|
||||
"fmt"
|
||||
|
||||
gossh "code.google.com/p/go.crypto/ssh"
|
||||
"github.com/mitchellh/multistep"
|
||||
commonssh "github.com/mitchellh/packer/common/ssh"
|
||||
"github.com/mitchellh/packer/communicator/ssh"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
func sshAddress(state multistep.StateBag) (string, error) {
|
||||
|
@ -24,7 +24,7 @@ func sshConfig(state multistep.StateBag) (*gossh.ClientConfig, error) {
|
|||
}
|
||||
|
||||
if config.SSHKeyPath != "" {
|
||||
signer, err := sshKeyToSigner(config.SSHKeyPath)
|
||||
signer, err := commonssh.FileSigner(config.SSHKeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -37,23 +37,3 @@ func sshConfig(state multistep.StateBag) (*gossh.ClientConfig, error) {
|
|||
Auth: auth,
|
||||
}, 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
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// 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 {
|
|||
|
||||
log.Printf("Looking for available SSH port between %d and %d", config.SSHHostPortMin, config.SSHHostPortMax)
|
||||
var sshHostPort uint
|
||||
var offset uint = 0
|
||||
|
||||
portRange := int(config.SSHHostPortMax - config.SSHHostPortMin)
|
||||
if portRange > 0 {
|
||||
// Have to check if > 0 to avoid a panic
|
||||
offset = uint(rand.Intn(portRange))
|
||||
}
|
||||
|
||||
for {
|
||||
sshHostPort = uint(rand.Intn(portRange)) + config.SSHHostPortMin
|
||||
sshHostPort = offset + config.SSHHostPortMin
|
||||
log.Printf("Trying port: %d", sshHostPort)
|
||||
l, err := net.Listen("tcp", fmt.Sprintf(":%d", sshHostPort))
|
||||
if err == nil {
|
||||
|
|
|
@ -2,11 +2,12 @@ package qemu
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// stepRun runs the virtual machine
|
||||
|
@ -78,13 +79,12 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error
|
|||
|
||||
defaultArgs["-name"] = vmName
|
||||
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["-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["-boot"] = bootDrive
|
||||
defaultArgs["-m"] = "512M"
|
||||
defaultArgs["-redir"] = fmt.Sprintf("tcp:%v::22", sshHostPort)
|
||||
defaultArgs["-vnc"] = vnc
|
||||
|
||||
// Append the accelerator to the machine type if it is specified
|
||||
|
|
|
@ -23,7 +23,7 @@ type Driver interface {
|
|||
Delete(string) error
|
||||
|
||||
// Import a VM
|
||||
Import(string, string, string) error
|
||||
Import(string, string, []string) error
|
||||
|
||||
// The complete path to the Guest Additions ISO
|
||||
Iso() (string, error)
|
||||
|
@ -55,15 +55,16 @@ func NewDriver() (Driver, error) {
|
|||
|
||||
// On Windows, we check VBOX_INSTALL_PATH env var for the path
|
||||
if runtime.GOOS == "windows" {
|
||||
if installPath := os.Getenv("VBOX_INSTALL_PATH"); installPath != "" {
|
||||
log.Printf("[DEBUG] builder/virtualbox: VBOX_INSTALL_PATH: %s",
|
||||
installPath)
|
||||
for _, path := range strings.Split(installPath, ";") {
|
||||
path = filepath.Join(path, "VBoxManage.exe")
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
vboxmanagePath = path
|
||||
break
|
||||
}
|
||||
vars := []string{"VBOX_INSTALL_PATH", "VBOX_MSI_INSTALL_PATH"}
|
||||
for _, key := range vars {
|
||||
value := os.Getenv(key)
|
||||
if value != "" {
|
||||
log.Printf(
|
||||
"[DEBUG] builder/virtualbox: %s = %s", key, value)
|
||||
vboxmanagePath = findVBoxManageWindows(value)
|
||||
}
|
||||
if vboxmanagePath != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -84,3 +85,14 @@ func NewDriver() (Driver, error) {
|
|||
|
||||
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) {
|
|||
return "", err
|
||||
}
|
||||
|
||||
DefaultGuestAdditionsRe := regexp.MustCompile("Default Guest Additions ISO:(.*)")
|
||||
DefaultGuestAdditionsRe := regexp.MustCompile("Default Guest Additions ISO:(.+)")
|
||||
|
||||
for _, line := range strings.Split(stdout.String(), "\n") {
|
||||
// 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)
|
||||
if matches == nil {
|
||||
|
@ -66,16 +67,16 @@ func (d *VBox42Driver) Iso() (string, error) {
|
|||
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{
|
||||
"import", path,
|
||||
"--vsys", "0",
|
||||
"--vmname", name,
|
||||
"--options", opts,
|
||||
}
|
||||
args = append(args, flags...)
|
||||
|
||||
return d.VBoxManage(args...)
|
||||
}
|
||||
|
@ -157,6 +158,15 @@ func (d *VBox42Driver) VBoxManage(args ...string) error {
|
|||
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("stderr: %s", stderrString)
|
||||
|
||||
|
@ -186,12 +196,12 @@ func (d *VBox42Driver) Version() (string, error) {
|
|||
return "", fmt.Errorf("VirtualBox is not properly setup: %s", versionOutput)
|
||||
}
|
||||
|
||||
versionRe := regexp.MustCompile("^[.0-9]+(?:_RC[0-9]+)?")
|
||||
matches := versionRe.FindAllString(versionOutput, 1)
|
||||
if matches == nil {
|
||||
versionRe := regexp.MustCompile("^([.0-9]+)(?:_(?:RC|OSEr)[0-9]+)?")
|
||||
matches := versionRe.FindAllStringSubmatch(versionOutput, 1)
|
||||
if matches == nil || len(matches[0]) != 2 {
|
||||
return "", fmt.Errorf("No version found: %s", versionOutput)
|
||||
}
|
||||
|
||||
log.Printf("VirtualBox version: %s", matches[0])
|
||||
return matches[0], nil
|
||||
log.Printf("VirtualBox version: %s", matches[0][1])
|
||||
return matches[0][1], nil
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ type DriverMock struct {
|
|||
ImportCalled bool
|
||||
ImportName string
|
||||
ImportPath string
|
||||
ImportOpts string
|
||||
ImportFlags []string
|
||||
ImportErr error
|
||||
|
||||
IsoCalled bool
|
||||
|
@ -55,11 +55,11 @@ func (d *DriverMock) Delete(name string) error {
|
|||
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.ImportName = name
|
||||
d.ImportPath = path
|
||||
d.ImportOpts = opts
|
||||
d.ImportFlags = flags
|
||||
return d.ImportErr
|
||||
}
|
||||
|
||||
|
|
|
@ -1,15 +1,21 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type RunConfig struct {
|
||||
Headless bool `mapstructure:"headless"`
|
||||
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 ``
|
||||
}
|
||||
|
||||
|
@ -18,8 +24,17 @@ func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error {
|
|||
c.RawBootWait = "10s"
|
||||
}
|
||||
|
||||
if c.HTTPPortMin == 0 {
|
||||
c.HTTPPortMin = 8000
|
||||
}
|
||||
|
||||
if c.HTTPPortMax == 0 {
|
||||
c.HTTPPortMax = 9000
|
||||
}
|
||||
|
||||
templates := map[string]*string{
|
||||
"boot_wait": &c.RawBootWait,
|
||||
"boot_wait": &c.RawBootWait,
|
||||
"http_directory": &c.HTTPDir,
|
||||
}
|
||||
|
||||
errs := make([]error, 0)
|
||||
|
@ -37,5 +52,10 @@ func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error {
|
|||
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
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
gossh "code.google.com/p/go.crypto/ssh"
|
||||
"fmt"
|
||||
|
||||
gossh "code.google.com/p/go.crypto/ssh"
|
||||
"github.com/mitchellh/multistep"
|
||||
commonssh "github.com/mitchellh/packer/common/ssh"
|
||||
"github.com/mitchellh/packer/communicator/ssh"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
func SSHAddress(state multistep.StateBag) (string, error) {
|
||||
|
@ -23,7 +23,7 @@ func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*gossh.ClientConf
|
|||
}
|
||||
|
||||
if config.SSHKeyPath != "" {
|
||||
signer, err := sshKeyToSigner(config.SSHKeyPath)
|
||||
signer, err := commonssh.FileSigner(config.SSHKeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -37,23 +37,3 @@ func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*gossh.ClientConf
|
|||
}, 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
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
commonssh "github.com/mitchellh/packer/common/ssh"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type SSHConfig struct {
|
||||
|
@ -56,7 +58,7 @@ func (c *SSHConfig) Prepare(t *packer.ConfigTemplate) []error {
|
|||
if c.SSHKeyPath != "" {
|
||||
if _, err := os.Stat(c.SSHKeyPath); err != nil {
|
||||
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
|
||||
} else if _, err := 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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -91,6 +91,13 @@ func (s *StepDownloadGuestAdditions) Run(state multistep.StateBag) multistep.Ste
|
|||
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 s.GuestAdditionsSHA256 != "" {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package iso
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -15,28 +15,30 @@ import (
|
|||
// template.
|
||||
//
|
||||
// Uses:
|
||||
// config *config
|
||||
// ui packer.Ui
|
||||
//
|
||||
// Produces:
|
||||
// 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
|
||||
}
|
||||
|
||||
func (s *stepHTTPServer) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*config)
|
||||
func (s *StepHTTPServer) Run(state multistep.StateBag) multistep.StepAction {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
var httpPort uint = 0
|
||||
if config.HTTPDir == "" {
|
||||
if s.HTTPDir == "" {
|
||||
state.Put("http_port", httpPort)
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
// Find an available TCP port for our HTTP server
|
||||
var httpAddr string
|
||||
portRange := int(config.HTTPPortMax - config.HTTPPortMin)
|
||||
portRange := int(s.HTTPPortMax - s.HTTPPortMin)
|
||||
for {
|
||||
var err error
|
||||
var offset uint = 0
|
||||
|
@ -46,7 +48,7 @@ func (s *stepHTTPServer) Run(state multistep.StateBag) multistep.StepAction {
|
|||
offset = uint(rand.Intn(portRange))
|
||||
}
|
||||
|
||||
httpPort = offset + config.HTTPPortMin
|
||||
httpPort = offset + s.HTTPPortMin
|
||||
httpAddr = fmt.Sprintf(":%d", httpPort)
|
||||
log.Printf("Trying port: %d", httpPort)
|
||||
s.l, err = net.Listen("tcp", httpAddr)
|
||||
|
@ -58,7 +60,7 @@ func (s *stepHTTPServer) Run(state multistep.StateBag) multistep.StepAction {
|
|||
ui.Say(fmt.Sprintf("Starting HTTP server on port %d", httpPort))
|
||||
|
||||
// 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}
|
||||
go server.Serve(s.l)
|
||||
|
||||
|
@ -68,7 +70,7 @@ func (s *stepHTTPServer) Run(state multistep.StateBag) multistep.StepAction {
|
|||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepHTTPServer) Cleanup(multistep.StateBag) {
|
||||
func (s *StepHTTPServer) Cleanup(multistep.StateBag) {
|
||||
if s.l != nil {
|
||||
// Close the listener so that the HTTP server stops
|
||||
s.l.Close()
|
|
@ -1,9 +1,8 @@
|
|||
package iso
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
vboxcommon "github.com/mitchellh/packer/builder/virtualbox/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"strings"
|
||||
|
@ -23,7 +22,6 @@ type bootCommandTemplateData struct {
|
|||
// This step "types" the boot command into the VM over VNC.
|
||||
//
|
||||
// Uses:
|
||||
// config *config
|
||||
// driver Driver
|
||||
// http_port int
|
||||
// ui packer.Ui
|
||||
|
@ -31,11 +29,14 @@ type bootCommandTemplateData struct {
|
|||
//
|
||||
// Produces:
|
||||
// <nothing>
|
||||
type stepTypeBootCommand struct{}
|
||||
type StepTypeBootCommand struct {
|
||||
BootCommand []string
|
||||
VMName string
|
||||
Tpl *packer.ConfigTemplate
|
||||
}
|
||||
|
||||
func (s *stepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*config)
|
||||
driver := state.Get("driver").(vboxcommon.Driver)
|
||||
func (s *StepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAction {
|
||||
driver := state.Get("driver").(Driver)
|
||||
httpPort := state.Get("http_port").(uint)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
vmName := state.Get("vmName").(string)
|
||||
|
@ -43,12 +44,12 @@ func (s *stepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAction
|
|||
tplData := &bootCommandTemplateData{
|
||||
"10.0.2.2",
|
||||
httpPort,
|
||||
config.VMName,
|
||||
s.VMName,
|
||||
}
|
||||
|
||||
ui.Say("Typing the boot command...")
|
||||
for _, command := range config.BootCommand {
|
||||
command, err := config.tpl.Process(command, tplData)
|
||||
for _, command := range s.BootCommand {
|
||||
command, err := s.Tpl.Process(command, tplData)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error preparing boot command: %s", err)
|
||||
state.Put("error", err)
|
||||
|
@ -90,7 +91,7 @@ func (s *stepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAction
|
|||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (*stepTypeBootCommand) Cleanup(multistep.StateBag) {}
|
||||
func (*StepTypeBootCommand) Cleanup(multistep.StateBag) {}
|
||||
|
||||
func scancodes(message string) []string {
|
||||
// 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
|
|||
}
|
||||
|
||||
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))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ func (s *StepUploadVersion) Run(state multistep.StateBag) multistep.StepAction {
|
|||
ui.Say(fmt.Sprintf("Uploading VirtualBox version info (%s)", version))
|
||||
var data bytes.Buffer
|
||||
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))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
|
|
@ -42,9 +42,6 @@ type config struct {
|
|||
GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"`
|
||||
GuestOSType string `mapstructure:"guest_os_type"`
|
||||
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"`
|
||||
ISOChecksumType string `mapstructure:"iso_checksum_type"`
|
||||
ISOInterface string `mapstructure:"iso_interface"`
|
||||
|
@ -103,20 +100,12 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
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 == "" {
|
||||
b.config.ISOInterface = "ide"
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -125,7 +114,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
"guest_additions_sha256": &b.config.GuestAdditionsSHA256,
|
||||
"guest_os_type": &b.config.GuestOSType,
|
||||
"hard_drive_interface": &b.config.HardDriveInterface,
|
||||
"http_directory": &b.config.HTTPDir,
|
||||
"iso_checksum": &b.config.ISOChecksum,
|
||||
"iso_checksum_type": &b.config.ISOChecksumType,
|
||||
"iso_interface": &b.config.ISOInterface,
|
||||
|
@ -175,11 +163,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
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 == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
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
|
|||
&common.StepCreateFloppy{
|
||||
Files: b.config.FloppyFiles,
|
||||
},
|
||||
new(stepHTTPServer),
|
||||
&vboxcommon.StepHTTPServer{
|
||||
HTTPDir: b.config.HTTPDir,
|
||||
HTTPPortMin: b.config.HTTPPortMin,
|
||||
HTTPPortMax: b.config.HTTPPortMax,
|
||||
},
|
||||
new(vboxcommon.StepSuppressMessages),
|
||||
new(stepCreateVM),
|
||||
new(stepCreateDisk),
|
||||
|
@ -320,7 +307,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
BootWait: b.config.BootWait,
|
||||
Headless: b.config.Headless,
|
||||
},
|
||||
new(stepTypeBootCommand),
|
||||
&vboxcommon.StepTypeBootCommand{
|
||||
BootCommand: b.config.BootCommand,
|
||||
VMName: b.config.VMName,
|
||||
Tpl: b.config.tpl,
|
||||
},
|
||||
&common.StepConnectSSH{
|
||||
SSHAddress: vboxcommon.SSHAddress,
|
||||
SSHConfig: vboxcommon.SSHConfigFunc(b.config.SSHConfig),
|
||||
|
|
|
@ -46,7 +46,7 @@ func TestBuilderPrepare_Defaults(t *testing.T) {
|
|||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -61,6 +61,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
&common.StepCreateFloppy{
|
||||
Files: b.config.FloppyFiles,
|
||||
},
|
||||
&vboxcommon.StepHTTPServer{
|
||||
HTTPDir: b.config.HTTPDir,
|
||||
HTTPPortMin: b.config.HTTPPortMin,
|
||||
HTTPPortMax: b.config.HTTPPortMax,
|
||||
},
|
||||
&vboxcommon.StepDownloadGuestAdditions{
|
||||
GuestAdditionsMode: b.config.GuestAdditionsMode,
|
||||
GuestAdditionsURL: b.config.GuestAdditionsURL,
|
||||
|
@ -68,9 +73,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
Tpl: b.config.tpl,
|
||||
},
|
||||
&StepImport{
|
||||
Name: b.config.VMName,
|
||||
SourcePath: b.config.SourcePath,
|
||||
ImportOpts: b.config.ImportOpts,
|
||||
Name: b.config.VMName,
|
||||
SourcePath: b.config.SourcePath,
|
||||
ImportFlags: b.config.ImportFlags,
|
||||
},
|
||||
&vboxcommon.StepAttachGuestAdditions{
|
||||
GuestAdditionsMode: b.config.GuestAdditionsMode,
|
||||
|
@ -89,6 +94,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
BootWait: b.config.BootWait,
|
||||
Headless: b.config.Headless,
|
||||
},
|
||||
&vboxcommon.StepTypeBootCommand{
|
||||
BootCommand: b.config.BootCommand,
|
||||
VMName: b.config.VMName,
|
||||
Tpl: b.config.tpl,
|
||||
},
|
||||
&common.StepConnectSSH{
|
||||
SSHAddress: vboxcommon.SSHAddress,
|
||||
SSHConfig: vboxcommon.SSHConfigFunc(b.config.SSHConfig),
|
||||
|
|
|
@ -24,13 +24,15 @@ type Config struct {
|
|||
vboxcommon.VBoxManagePostConfig `mapstructure:",squash"`
|
||||
vboxcommon.VBoxVersionConfig `mapstructure:",squash"`
|
||||
|
||||
SourcePath string `mapstructure:"source_path"`
|
||||
GuestAdditionsMode string `mapstructure:"guest_additions_mode"`
|
||||
GuestAdditionsPath string `mapstructure:"guest_additions_path"`
|
||||
GuestAdditionsURL string `mapstructure:"guest_additions_url"`
|
||||
GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"`
|
||||
VMName string `mapstructure:"vm_name"`
|
||||
ImportOpts string `mapstructure:"import_opts"`
|
||||
BootCommand []string `mapstructure:"boot_command"`
|
||||
SourcePath string `mapstructure:"source_path"`
|
||||
GuestAdditionsMode string `mapstructure:"guest_additions_mode"`
|
||||
GuestAdditionsPath string `mapstructure:"guest_additions_path"`
|
||||
GuestAdditionsURL string `mapstructure:"guest_additions_url"`
|
||||
GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"`
|
||||
VMName string `mapstructure:"vm_name"`
|
||||
ImportOpts string `mapstructure:"import_opts"`
|
||||
ImportFlags []string `mapstructure:"import_flags"`
|
||||
|
||||
tpl *packer.ConfigTemplate
|
||||
}
|
||||
|
@ -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 == "" {
|
||||
errs = packer.MultiErrorAppend(errs, fmt.Errorf("source_path is required"))
|
||||
} else {
|
||||
|
@ -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{
|
||||
"guest_additions_path": &c.GuestAdditionsPath,
|
||||
"guest_additions_url": &c.GuestAdditionsURL,
|
||||
|
@ -147,5 +171,10 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
|||
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
|
||||
}
|
||||
|
|
|
@ -9,9 +9,9 @@ import (
|
|||
|
||||
// This step imports an OVF VM into VirtualBox.
|
||||
type StepImport struct {
|
||||
Name string
|
||||
SourcePath string
|
||||
ImportOpts string
|
||||
Name string
|
||||
SourcePath string
|
||||
ImportFlags []string
|
||||
|
||||
vmName string
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ func (s *StepImport) Run(state multistep.StateBag) multistep.StepAction {
|
|||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
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)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
|
|
|
@ -129,7 +129,12 @@ func runAndLog(cmd *exec.Cmd) (string, string, error) {
|
|||
stderrString := strings.TrimSpace(stderr.String())
|
||||
|
||||
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)
|
||||
|
@ -153,7 +158,7 @@ func normalizeVersion(version string) (string, error) {
|
|||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -166,7 +171,7 @@ func compareVersions(versionFound string, versionWanted string) error {
|
|||
|
||||
if found < wanted {
|
||||
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
|
||||
|
|
|
@ -90,6 +90,12 @@ func (d *Fusion5Driver) Start(vmxPath string, headless bool) error {
|
|||
func (d *Fusion5Driver) Stop(vmxPath string) error {
|
||||
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "stop", vmxPath, "hard")
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,9 @@ import (
|
|||
"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 {
|
||||
Fusion5Driver
|
||||
}
|
||||
|
@ -22,6 +24,12 @@ func (d *Fusion6Driver) Clone(dst, src string) error {
|
|||
"clone", src, dst,
|
||||
"full")
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -50,7 +58,7 @@ func (d *Fusion6Driver) Verify() error {
|
|||
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())
|
||||
if matches == nil {
|
||||
return fmt.Errorf(
|
||||
|
@ -58,10 +66,5 @@ func (d *Fusion6Driver) Verify() error {
|
|||
}
|
||||
log.Printf("Detected VMware version: %s", matches[1])
|
||||
|
||||
if !strings.HasPrefix(matches[1], "6.") {
|
||||
return fmt.Errorf(
|
||||
"Fusion 6 not detected. Got version: %s", matches[1])
|
||||
}
|
||||
|
||||
return nil
|
||||
return compareVersions(matches[1], VMWARE_FUSION_VERSION, "Fusion Professional")
|
||||
}
|
||||
|
|
|
@ -198,4 +198,4 @@ func (d *Player5Driver) DhcpLeasesPath(device string) string {
|
|||
|
||||
func (d *Player5Driver) VmnetnatConfPath() string {
|
||||
return playerVmnetnatConfPath()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,5 +31,5 @@ func playerVerifyVersion(version string) error {
|
|||
}
|
||||
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 {
|
|||
}
|
||||
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 {
|
|||
}
|
||||
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 {
|
|||
}
|
||||
log.Printf("Detected VMware WS version: %s", matches[1])
|
||||
|
||||
return compareVersions(matches[1], version)
|
||||
return compareVersions(matches[1], version, "Workstation")
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package iso
|
||||
package common
|
||||
|
||||
// Interface to help find the host IP that is available from within
|
||||
// the VMware virtual machines.
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue