Merge branch 'master' of https://github.com/mitchellh/packer
This commit is contained in:
commit
331003c809
|
@ -6,5 +6,4 @@
|
|||
/website/build
|
||||
.DS_Store
|
||||
.vagrant
|
||||
Vagrantfile
|
||||
test/.env
|
||||
|
|
12
.travis.yml
12
.travis.yml
|
@ -2,20 +2,14 @@ language: go
|
|||
|
||||
go:
|
||||
- 1.2
|
||||
- 1.3
|
||||
- tip
|
||||
|
||||
install: make deps
|
||||
install: make updatedeps
|
||||
script:
|
||||
- go test ./...
|
||||
- make test
|
||||
#- go test -race ./...
|
||||
|
||||
notifications:
|
||||
irc:
|
||||
channels:
|
||||
- "chat.freenode.net#packer-tool"
|
||||
on_success: change
|
||||
on_failure: always
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: tip
|
||||
|
|
181
CHANGELOG.md
181
CHANGELOG.md
|
@ -1,16 +1,187 @@
|
|||
## 0.6.1 (unreleased)
|
||||
## 0.7.2 (unreleased)
|
||||
|
||||
IMPROVEMENTS:
|
||||
FEATURES:
|
||||
|
||||
* builder/openstack: skip certificate verification [GH-1121]
|
||||
* builder/virtualbox/all: Attempt to use local guest additions ISO
|
||||
before downloading from internet. [GH-1123]
|
||||
* builder/parallels: Don't depend on _prl-utils_ [GH-1499]
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* 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]
|
||||
|
||||
## 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/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)
|
||||
|
||||
FEATURES:
|
||||
|
||||
* **New post processor:** `vagrant-cloud` - Push box files generated by
|
||||
vagrant post processor to Vagrant Cloud. [GH-1289]
|
||||
* Vagrant post-processor can now packer Hyper-V boxes.
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* builder/amazon: Support for enhanced networking on HVM images. [GH-1228]
|
||||
* builder/amazon-ebs: Support encrypted EBS volumes [GH-1194]
|
||||
* builder/ansible: Add `playbook_dir` option. [GH-1000]
|
||||
* builder/openstack: Add ability to configure networks. [GH-1261]
|
||||
* builder/openstack: Skip certificate verification. [GH-1121]
|
||||
* builder/parallels/all: Add ability to select interface to connect to.
|
||||
* builder/parallels/pvm: Support `boot_command`. [GH-1082]
|
||||
* builder/virtualbox/all: Attempt to use local guest additions ISO
|
||||
before downloading from internet. [GH-1123]
|
||||
* builder/virtualbox/ovf: Supports `guest_additions_mode` [GH-1035]
|
||||
* builder/vmware/all: Increase cleanup timeout to 120 seconds [GH-1167]
|
||||
* builder/vmware/all: Add `vmx_data_post` for modifying VMX data
|
||||
after shutdown. [GH-1149]
|
||||
* builder/vmware/vmx: Supports tools uploading. [GH-1154]
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* core: `isotime` is the same time during the entire build. [GH-1153]
|
||||
* builder/amazon-common: Sort AMI strings before outputting [GH-1305]
|
||||
* builder/amazon: User data can use templates/variables. [GH-1343]
|
||||
* builder/amazon: Can now build AMIs in GovCloud.
|
||||
* builder/null: SSH info can use templates/variables. [GH-1343]
|
||||
* builder/openstack: Workaround for gophercloud.ServerById crashing [GH-1257]
|
||||
* builder/openstack: Force IPv4 addresses from address pools [GH-1258]
|
||||
* builder/parallels: Do not delete entire CDROM device. [GH-1115]
|
||||
* builder/parallels: Errors while creating floppy disk. [GH-1225]
|
||||
* builder/parallels: Errors while removing floppy drive. [GH-1226]
|
||||
* builder/virtualbox-ovf: Supports guest additions options. [GH-1120]
|
||||
* builder/vmware-iso: Fix esx5 path separator in windows. [GH-1316]
|
||||
* builder/vmware: Remote ESXi builder now uploads floppy. [GH-1106]
|
||||
* builder/vmware: Remote ESXi builder no longer re-uploads ISO every
|
||||
time. [GH-1244]
|
||||
* post-processor/vsphere: Accept DOMAIN\account usernames [GH-1178]
|
||||
* provisioner/chef-*: Fix remotePaths for Windows [GH-394]
|
||||
|
||||
## 0.6.0 (May 2, 2014)
|
||||
|
||||
|
|
|
@ -56,19 +56,32 @@ following steps in order to be able to compile and test Packer.
|
|||
1. Install Go. Make sure the Go version is at least Go 1.2. Packer will not work with anything less than
|
||||
Go 1.2. On a Mac, you can `brew install go` to install Go 1.2.
|
||||
|
||||
2. Set and export the `GOPATH` environment variable. For example, you can
|
||||
add `export GOPATH=$HOME/Documents/golang` to your `.bash_profile`.
|
||||
2. Set and export the `GOPATH` environment variable and update your `PATH`.
|
||||
For example, you can add to your `.bash_profile`.
|
||||
|
||||
3. Download the Packer source (and its dependencies) by running
|
||||
```
|
||||
export GOPATH=$HOME/Documents/golang
|
||||
export PATH=$PATH:$GOPATH/bin
|
||||
```
|
||||
|
||||
3. Install and build `gox` with
|
||||
|
||||
```
|
||||
go get github.com/mitchellh/gox
|
||||
cd $GOPATH/src/github.com/mitchellh/gox
|
||||
go build
|
||||
```
|
||||
|
||||
4. Download the Packer source (and its dependencies) by running
|
||||
`go get github.com/mitchellh/packer`. This will download the Packer
|
||||
source to `$GOPATH/src/github.com/mitchellh/packer`.
|
||||
|
||||
4. Make your changes to the Packer source. You can run `make` from the main
|
||||
5. Make your changes to the Packer source. You can run `make` from the main
|
||||
source directory to recompile all the binaries. Any compilation errors
|
||||
will be shown when the binaries are rebuilding.
|
||||
|
||||
5. Test your changes by running `make test` and then running
|
||||
6. Test your changes by running `make test` and then running
|
||||
`$GOPATH/src/github.com/mitchellh/packer/bin/packer` to build a machine.
|
||||
|
||||
6. If everything works well and the tests pass, run `go fmt` on your code
|
||||
7. If everything works well and the tests pass, run `go fmt` on your code
|
||||
before submitting a pull request.
|
||||
|
|
48
Makefile
48
Makefile
|
@ -1,38 +1,20 @@
|
|||
NO_COLOR=\033[0m
|
||||
OK_COLOR=\033[32;01m
|
||||
ERROR_COLOR=\033[31;01m
|
||||
WARN_COLOR=\033[33;01m
|
||||
DEPS = $(go list -f '{{range .TestImports}}{{.}} {{end}}' ./...)
|
||||
UNAME := $(shell uname -s)
|
||||
ifeq ($(UNAME),Darwin)
|
||||
ECHO=echo
|
||||
else
|
||||
ECHO=/bin/echo -e
|
||||
endif
|
||||
TEST?=./...
|
||||
|
||||
all: deps
|
||||
@mkdir -p bin/
|
||||
@$(ECHO) "$(OK_COLOR)==> Building$(NO_COLOR)"
|
||||
@bash --norc -i ./scripts/devcompile.sh
|
||||
default: test
|
||||
|
||||
deps:
|
||||
@$(ECHO) "$(OK_COLOR)==> Installing dependencies$(NO_COLOR)"
|
||||
@go get -d -v ./...
|
||||
@echo $(DEPS) | xargs -n1 go get -d
|
||||
bin:
|
||||
@sh -c "$(CURDIR)/scripts/build.sh"
|
||||
|
||||
dev:
|
||||
@TF_DEV=1 sh -c "$(CURDIR)/scripts/build.sh"
|
||||
|
||||
test:
|
||||
go test $(TEST) $(TESTARGS) -timeout=10s
|
||||
|
||||
testrace:
|
||||
go test -race $(TEST) $(TESTARGS)
|
||||
|
||||
updatedeps:
|
||||
@$(ECHO) "$(OK_COLOR)==> Updating all dependencies$(NO_COLOR)"
|
||||
@go get -d -v -u ./...
|
||||
@echo $(DEPS) | xargs -n1 go get -d -u
|
||||
go get -u -v -p 2 ./...
|
||||
|
||||
clean:
|
||||
@rm -rf bin/ local/ pkg/ src/ website/.sass-cache website/build
|
||||
|
||||
format:
|
||||
go fmt ./...
|
||||
|
||||
test: deps
|
||||
@$(ECHO) "$(OK_COLOR)==> Testing Packer...$(NO_COLOR)"
|
||||
go test ./...
|
||||
|
||||
.PHONY: all clean deps format test updatedeps
|
||||
.PHONY: bin default test updatedeps
|
||||
|
|
58
README.md
58
README.md
|
@ -78,40 +78,44 @@ http://www.packer.io/docs
|
|||
|
||||
## Developing Packer
|
||||
|
||||
If you wish to work on Packer itself, you'll first need [Go](http://golang.org)
|
||||
installed (version 1.2+ is _required_). Make sure you have Go properly installed,
|
||||
including setting up your [GOPATH](http://golang.org/doc/code.html#GOPATH).
|
||||
If you wish to work on Packer itself or any of its built-in providers,
|
||||
you'll first need [Go](http://www.golang.org) installed (version 1.2+ is
|
||||
_required_). Make sure Go is properly installed, including setting up
|
||||
a [GOPATH](http://golang.org/doc/code.html#GOPATH).
|
||||
|
||||
For some additional dependencies, Go needs [Mercurial](http://mercurial.selenic.com/)
|
||||
and [Bazaar](http://bazaar.canonical.com/en/) to be installed.
|
||||
Packer itself doesn't require these, but a dependency of a dependency does.
|
||||
Next, install the following software packages, which are needed for some dependencies:
|
||||
|
||||
You'll also need [`gox`](https://github.com/mitchellh/gox)
|
||||
to compile packer. You can install that with:
|
||||
- [Bazaar](http://bazaar.canonical.com/en/)
|
||||
- [Git](http://git-scm.com/)
|
||||
- [Mercurial](http://mercurial.selenic.com/)
|
||||
|
||||
```
|
||||
$ go get -u github.com/mitchellh/gox
|
||||
```
|
||||
Then, install [Gox](https://github.com/mitchellh/gox), which is used
|
||||
as a compilation tool on top of Go:
|
||||
|
||||
Next, clone this repository into `$GOPATH/src/github.com/mitchellh/packer` and
|
||||
then just type `make`. In a few moments, you'll have a working `packer` executable:
|
||||
$ go get -u github.com/mitchellh/gox
|
||||
|
||||
```
|
||||
$ make
|
||||
...
|
||||
$ bin/packer
|
||||
...
|
||||
```
|
||||
Next, clone this repository into `$GOPATH/src/github.com/mitchellh/packer`.
|
||||
Install the necessary dependencies by running `make updatedeps` and then just
|
||||
type `make`. This will compile some more dependencies and then run the tests. If
|
||||
this exits with exit status 0, then everything is working!
|
||||
|
||||
If you need to cross-compile Packer for other platforms, take a look at
|
||||
`scripts/dist.sh`.
|
||||
$ make updatedeps
|
||||
...
|
||||
$ make
|
||||
...
|
||||
|
||||
You can run tests by typing `make test`.
|
||||
To compile a development version of Packer and the built-in plugins,
|
||||
run `make dev`. This will put Packer binaries in the `bin` folder:
|
||||
|
||||
This will run tests for Packer core along with all the core builders and commands and such that come with Packer.
|
||||
$ make dev
|
||||
...
|
||||
$ bin/packer
|
||||
...
|
||||
|
||||
If you make any changes to the code, run `make format` in order to automatically
|
||||
format the code according to Go standards.
|
||||
|
||||
When new dependencies are added to packer you can use `make updatedeps` to
|
||||
get the latest and subsequently use `make` to compile and generate the `packer` binary.
|
||||
If you're developing a specific package, you can run tests for just that
|
||||
package by specifying the `TEST` variable. For example below, only
|
||||
`packer` package tests will be run.
|
||||
|
||||
$ make test TEST=./packer
|
||||
...
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
# -*- mode: ruby -*-
|
||||
# vi: set ft=ruby :
|
||||
|
||||
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
|
||||
VAGRANTFILE_API_VERSION = "2"
|
||||
|
||||
$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(VAGRANTFILE_API_VERSION) 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
|
|
@ -7,13 +7,14 @@ package chroot
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"runtime"
|
||||
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// The unique ID for this builder
|
||||
|
@ -182,7 +183,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
// Build the steps
|
||||
steps := []multistep.Step{
|
||||
&StepInstanceInfo{},
|
||||
&StepSourceAMIInfo{},
|
||||
&awscommon.StepSourceAMIInfo{
|
||||
SourceAmi: b.config.SourceAmi,
|
||||
EnhancedNetworking: b.config.AMIEnhancedNetworking,
|
||||
},
|
||||
&StepCheckRootDevice{},
|
||||
&StepFlock{},
|
||||
&StepPrepareDevice{},
|
||||
&StepCreateVolume{},
|
||||
|
|
|
@ -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")
|
||||
|
@ -79,8 +79,17 @@ func (c *Communicator) Upload(dst string, r io.Reader) error {
|
|||
}
|
||||
|
||||
func (c *Communicator) UploadDir(dst string, src string, exclude []string) error {
|
||||
// If src ends with a trailing "/", copy from "src/." so that
|
||||
// directory contents (including hidden files) are copied, but the
|
||||
// directory "src" is omitted. BSD does this automatically when
|
||||
// the source contains a trailing slash, but linux does not.
|
||||
if src[len(src)-1] == '/' {
|
||||
src = src + "."
|
||||
}
|
||||
|
||||
// TODO: remove any file copied if it appears in `exclude`
|
||||
chrootDest := filepath.Join(c.Chroot, dst)
|
||||
|
||||
log.Printf("Uploading directory '%s' to '%s'", src, chrootDest)
|
||||
cpCmd, err := c.CmdWrapper(fmt.Sprintf("cp -R '%s' %s", src, chrootDest))
|
||||
if err != nil {
|
||||
|
|
|
@ -27,11 +27,12 @@ func AvailableDevice() (string, error) {
|
|||
continue
|
||||
}
|
||||
|
||||
for i := 1; i < 16; i++ {
|
||||
device := fmt.Sprintf("/dev/%s%c%d", prefix, letter, i)
|
||||
if _, err := os.Stat(device); err != nil {
|
||||
return device, nil
|
||||
}
|
||||
// To be able to build both Paravirtual and HVM images, the unnumbered
|
||||
// device and the first numbered one must be available.
|
||||
// E.g. /dev/xvdf and /dev/xvdf1
|
||||
numbered_device := fmt.Sprintf("%s%d", device, 1)
|
||||
if _, err := os.Stat(numbered_device); err != nil {
|
||||
return device, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
package chroot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// StepCheckRootDevice makes sure the root device on the AMI is EBS-backed.
|
||||
type StepCheckRootDevice struct{}
|
||||
|
||||
func (s *StepCheckRootDevice) Run(state multistep.StateBag) multistep.StepAction {
|
||||
image := state.Get("source_image").(*ec2.Image)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say("Checking the root device on source AMI...")
|
||||
|
||||
// It must be EBS-backed otherwise the build won't work
|
||||
if image.RootDeviceType != "ebs" {
|
||||
err := fmt.Errorf("The root device of the source AMI must be EBS-backed.")
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepCheckRootDevice) Cleanup(multistep.StateBag) {}
|
|
@ -3,6 +3,7 @@ package chroot
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
|
@ -26,6 +27,7 @@ type StepMountDevice struct {
|
|||
func (s *StepMountDevice) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
image := state.Get("source_image").(*ec2.Image)
|
||||
device := state.Get("device").(string)
|
||||
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
|
||||
|
||||
|
@ -57,10 +59,17 @@ func (s *StepMountDevice) Run(state multistep.StateBag) multistep.StepAction {
|
|||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
log.Printf("Source image virtualization type is: %s", image.VirtualizationType)
|
||||
deviceMount := device
|
||||
if image.VirtualizationType == "hvm" {
|
||||
deviceMount = fmt.Sprintf("%s%d", device, 1)
|
||||
}
|
||||
state.Put("deviceMount", deviceMount)
|
||||
|
||||
ui.Say("Mounting the root device...")
|
||||
stderr := new(bytes.Buffer)
|
||||
mountCommand, err := wrappedCommand(
|
||||
fmt.Sprintf("mount %s %s", device, mountPath))
|
||||
fmt.Sprintf("mount %s %s", deviceMount, mountPath))
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating mount command: %s", err)
|
||||
state.Put("error", err)
|
||||
|
|
|
@ -2,6 +2,7 @@ package chroot
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
|
@ -29,13 +30,11 @@ func (s *StepRegisterAMI) Run(state multistep.StateBag) multistep.StepAction {
|
|||
blockDevices[i] = newDevice
|
||||
}
|
||||
|
||||
registerOpts := &ec2.RegisterImage{
|
||||
Name: config.AMIName,
|
||||
Architecture: image.Architecture,
|
||||
KernelId: image.KernelId,
|
||||
RamdiskId: image.RamdiskId,
|
||||
RootDeviceName: image.RootDeviceName,
|
||||
BlockDevices: blockDevices,
|
||||
registerOpts := buildRegisterOpts(config, image, blockDevices)
|
||||
|
||||
// Set SriovNetSupport to "simple". See http://goo.gl/icuXh5
|
||||
if config.AMIEnhancedNetworking {
|
||||
registerOpts.SriovNetSupport = "simple"
|
||||
}
|
||||
|
||||
registerResp, err := ec2conn.RegisterImage(registerOpts)
|
||||
|
@ -71,3 +70,20 @@ func (s *StepRegisterAMI) Run(state multistep.StateBag) multistep.StepAction {
|
|||
}
|
||||
|
||||
func (s *StepRegisterAMI) Cleanup(state multistep.StateBag) {}
|
||||
|
||||
func buildRegisterOpts(config *Config, image *ec2.Image, blockDevices []ec2.BlockDeviceMapping) *ec2.RegisterImage {
|
||||
registerOpts := &ec2.RegisterImage{
|
||||
Name: config.AMIName,
|
||||
Architecture: image.Architecture,
|
||||
RootDeviceName: image.RootDeviceName,
|
||||
BlockDevices: blockDevices,
|
||||
VirtType: config.AMIVirtType,
|
||||
}
|
||||
|
||||
if config.AMIVirtType != "hvm" {
|
||||
registerOpts.KernelId = image.KernelId
|
||||
registerOpts.RamdiskId = image.RamdiskId
|
||||
}
|
||||
|
||||
return registerOpts
|
||||
}
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
package chroot
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testImage() ec2.Image {
|
||||
return ec2.Image{
|
||||
Id: "ami-abcd1234",
|
||||
Name: "ami_test_name",
|
||||
Architecture: "x86_64",
|
||||
KernelId: "aki-abcd1234",
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepRegisterAmi_buildRegisterOpts_pv(t *testing.T) {
|
||||
config := Config{}
|
||||
config.AMIName = "test_ami_name"
|
||||
config.AMIDescription = "test_ami_description"
|
||||
config.AMIVirtType = "paravirtual"
|
||||
|
||||
image := testImage()
|
||||
|
||||
blockDevices := []ec2.BlockDeviceMapping{}
|
||||
|
||||
opts := buildRegisterOpts(&config, &image, blockDevices)
|
||||
|
||||
expected := config.AMIVirtType
|
||||
if opts.VirtType != expected {
|
||||
t.Fatalf("Unexpected VirtType value: expected %s got %s\n", expected, opts.VirtType)
|
||||
}
|
||||
|
||||
expected = config.AMIName
|
||||
if opts.Name != expected {
|
||||
t.Fatalf("Unexpected Name value: expected %s got %s\n", expected, opts.Name)
|
||||
}
|
||||
|
||||
expected = image.KernelId
|
||||
if opts.KernelId != expected {
|
||||
t.Fatalf("Unexpected KernelId value: expected %s got %s\n", expected, opts.KernelId)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestStepRegisterAmi_buildRegisterOpts_hvm(t *testing.T) {
|
||||
config := Config{}
|
||||
config.AMIName = "test_ami_name"
|
||||
config.AMIDescription = "test_ami_description"
|
||||
config.AMIVirtType = "hvm"
|
||||
|
||||
image := testImage()
|
||||
|
||||
blockDevices := []ec2.BlockDeviceMapping{}
|
||||
|
||||
opts := buildRegisterOpts(&config, &image, blockDevices)
|
||||
|
||||
expected := config.AMIVirtType
|
||||
if opts.VirtType != expected {
|
||||
t.Fatalf("Unexpected VirtType value: expected %s got %s\n", expected, opts.VirtType)
|
||||
}
|
||||
|
||||
expected = config.AMIName
|
||||
if opts.Name != expected {
|
||||
t.Fatalf("Unexpected Name value: expected %s got %s\n", expected, opts.Name)
|
||||
}
|
||||
|
||||
expected = ""
|
||||
if opts.KernelId != expected {
|
||||
t.Fatalf("Unexpected KernelId value: expected %s got %s\n", expected, opts.KernelId)
|
||||
}
|
||||
|
||||
}
|
|
@ -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 auth.Token == "" && c.Token != "" {
|
||||
auth.Token = c.Token
|
||||
}
|
||||
|
||||
return auth, err
|
||||
|
|
|
@ -2,20 +2,22 @@ package common
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/goamz/aws"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// AMIConfig is for common configuration related to creating AMIs.
|
||||
type AMIConfig struct {
|
||||
AMIName string `mapstructure:"ami_name"`
|
||||
AMIDescription string `mapstructure:"ami_description"`
|
||||
AMIVirtType string `mapstructure:"ami_virtualization_type"`
|
||||
AMIUsers []string `mapstructure:"ami_users"`
|
||||
AMIGroups []string `mapstructure:"ami_groups"`
|
||||
AMIProductCodes []string `mapstructure:"ami_product_codes"`
|
||||
AMIRegions []string `mapstructure:"ami_regions"`
|
||||
AMITags map[string]string `mapstructure:"tags"`
|
||||
AMIName string `mapstructure:"ami_name"`
|
||||
AMIDescription string `mapstructure:"ami_description"`
|
||||
AMIVirtType string `mapstructure:"ami_virtualization_type"`
|
||||
AMIUsers []string `mapstructure:"ami_users"`
|
||||
AMIGroups []string `mapstructure:"ami_groups"`
|
||||
AMIProductCodes []string `mapstructure:"ami_product_codes"`
|
||||
AMIRegions []string `mapstructure:"ami_regions"`
|
||||
AMITags map[string]string `mapstructure:"tags"`
|
||||
AMIEnhancedNetworking bool `mapstructure:"enhanced_networking"`
|
||||
}
|
||||
|
||||
func (c *AMIConfig) Prepare(t *packer.ConfigTemplate) []error {
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -36,6 +37,7 @@ func (a *Artifact) Id() string {
|
|||
parts = append(parts, fmt.Sprintf("%s:%s", region, amiId))
|
||||
}
|
||||
|
||||
sort.Strings(parts)
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
|
@ -46,6 +48,7 @@ func (a *Artifact) String() string {
|
|||
amiStrings = append(amiStrings, single)
|
||||
}
|
||||
|
||||
sort.Strings(amiStrings)
|
||||
return fmt.Sprintf("AMIs were created:\n\n%s", strings.Join(amiStrings, "\n"))
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// BlockDevice
|
||||
type BlockDevice struct {
|
||||
DeleteOnTermination bool `mapstructure:"delete_on_termination"`
|
||||
DeviceName string `mapstructure:"device_name"`
|
||||
Encrypted bool `mapstructure:"encrypted"`
|
||||
IOPS int64 `mapstructure:"iops"`
|
||||
NoDevice bool `mapstructure:"no_device"`
|
||||
SnapshotId string `mapstructure:"snapshot_id"`
|
||||
|
@ -34,11 +38,57 @@ func buildBlockDevices(b []BlockDevice) []ec2.BlockDeviceMapping {
|
|||
DeleteOnTermination: blockDevice.DeleteOnTermination,
|
||||
IOPS: blockDevice.IOPS,
|
||||
NoDevice: blockDevice.NoDevice,
|
||||
Encrypted: blockDevice.Encrypted,
|
||||
})
|
||||
}
|
||||
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"`
|
||||
|
@ -42,6 +47,34 @@ 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,
|
||||
"source_ami": &c.SourceAmi,
|
||||
"subnet_id": &c.SubnetId,
|
||||
"temporary_key_pair_name": &c.TemporaryKeyPairName,
|
||||
"vpc_id": &c.VpcId,
|
||||
"availability_zone": &c.AvailabilityZone,
|
||||
"user_data": &c.UserData,
|
||||
"user_data_file": &c.UserDataFile,
|
||||
"security_group_id": &c.SecurityGroupId,
|
||||
}
|
||||
|
||||
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: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
// Defaults
|
||||
if c.SSHPort == 0 {
|
||||
c.SSHPort = 22
|
||||
|
@ -52,12 +85,12 @@ func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error {
|
|||
}
|
||||
|
||||
if c.TemporaryKeyPairName == "" {
|
||||
c.TemporaryKeyPairName = "packer {{uuid}}"
|
||||
c.TemporaryKeyPairName = fmt.Sprintf(
|
||||
"packer %s", uuid.TimeOrderedUUID())
|
||||
}
|
||||
|
||||
// Validation
|
||||
var err error
|
||||
errs := make([]error, 0)
|
||||
if c.SourceAmi == "" {
|
||||
errs = append(errs, errors.New("A source_ami must be specified"))
|
||||
}
|
||||
|
@ -66,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"))
|
||||
}
|
||||
|
@ -87,28 +127,6 @@ func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error {
|
|||
}
|
||||
}
|
||||
|
||||
templates := map[string]*string{
|
||||
"iam_instance_profile": &c.IamInstanceProfile,
|
||||
"instance_type": &c.InstanceType,
|
||||
"ssh_timeout": &c.RawSSHTimeout,
|
||||
"ssh_username": &c.SSHUsername,
|
||||
"ssh_private_key_file": &c.SSHPrivateKeyFile,
|
||||
"source_ami": &c.SourceAmi,
|
||||
"subnet_id": &c.SubnetId,
|
||||
"temporary_key_pair_name": &c.TemporaryKeyPairName,
|
||||
"vpc_id": &c.VpcId,
|
||||
"availability_zone": &c.AvailabilityZone,
|
||||
}
|
||||
|
||||
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: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
sliceTemplates := map[string][]string{
|
||||
"security_group_ids": c.SecurityGroupIds,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -81,6 +81,32 @@ 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 {
|
||||
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 +151,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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
package chroot
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
|
@ -12,15 +13,17 @@ import (
|
|||
//
|
||||
// Produces:
|
||||
// source_image *ec2.Image - the source AMI info
|
||||
type StepSourceAMIInfo struct{}
|
||||
type StepSourceAMIInfo struct {
|
||||
SourceAmi string
|
||||
EnhancedNetworking bool
|
||||
}
|
||||
|
||||
func (s *StepSourceAMIInfo) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say("Inspecting the source AMI...")
|
||||
imageResp, err := ec2conn.Images([]string{config.SourceAmi}, ec2.NewFilter())
|
||||
imageResp, err := ec2conn.Images([]string{s.SourceAmi}, ec2.NewFilter())
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error querying AMI: %s", err)
|
||||
state.Put("error", err)
|
||||
|
@ -29,7 +32,7 @@ func (s *StepSourceAMIInfo) Run(state multistep.StateBag) multistep.StepAction {
|
|||
}
|
||||
|
||||
if len(imageResp.Images) == 0 {
|
||||
err := fmt.Errorf("Source AMI '%s' was not found!", config.SourceAmi)
|
||||
err := fmt.Errorf("Source AMI '%s' was not found!", s.SourceAmi)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
|
@ -37,9 +40,10 @@ func (s *StepSourceAMIInfo) Run(state multistep.StateBag) multistep.StepAction {
|
|||
|
||||
image := &imageResp.Images[0]
|
||||
|
||||
// It must be EBS-backed otherwise the build won't work
|
||||
if image.RootDeviceType != "ebs" {
|
||||
err := fmt.Errorf("The root device of the source AMI must be EBS-backed.")
|
||||
// Enhanced Networking (SriovNetSupport) can only be enabled on HVM AMIs.
|
||||
// See http://goo.gl/icuXh5
|
||||
if s.EnhancedNetworking && image.VirtualizationType != "hvm" {
|
||||
err := fmt.Errorf("Cannot enable enhanced networking, source AMI '%s' is not HVM", s.SourceAmi)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
|
@ -7,12 +7,13 @@ package ebs
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
)
|
||||
|
||||
// The unique ID for this builder
|
||||
|
@ -49,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)...)
|
||||
|
||||
|
@ -82,6 +84,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
|
||||
// Build the steps
|
||||
steps := []multistep.Step{
|
||||
&awscommon.StepSourceAMIInfo{
|
||||
SourceAmi: b.config.SourceAmi,
|
||||
EnhancedNetworking: b.config.AMIEnhancedNetworking,
|
||||
},
|
||||
&awscommon.StepKeyPair{
|
||||
Debug: b.config.PackerDebug,
|
||||
DebugKeyPath: fmt.Sprintf("ec2_%s.pem", b.config.PackerBuildName),
|
||||
|
@ -96,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,
|
||||
|
@ -108,12 +116,15 @@ 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{
|
||||
Regions: b.config.AMIRegions,
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
package ebs
|
||||
|
||||
// This hook is fired prior to launching the EC2 instance.
|
||||
const HookPreLaunch = "amazonebs_pre_launch"
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
package ebs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type stepModifyInstance struct{}
|
||||
|
||||
func (s *stepModifyInstance) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(config)
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
instance := state.Get("instance").(*ec2.Instance)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
// Set SriovNetSupport to "simple". See http://goo.gl/icuXh5
|
||||
if config.AMIEnhancedNetworking {
|
||||
ui.Say("Enabling Enhanced Networking...")
|
||||
_, err := ec2conn.ModifyInstance(
|
||||
instance.InstanceId,
|
||||
&ec2.ModifyInstance{SriovNetSupport: true},
|
||||
)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error enabling Enhanced Networking on %s: %s", instance.InstanceId, err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepModifyInstance) Cleanup(state multistep.StateBag) {
|
||||
// No cleanup...
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -5,14 +5,15 @@ package instance
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The unique ID for this builder
|
||||
|
@ -74,7 +75,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
"-s {{.SecretKey}} " +
|
||||
"-d {{.BundleDirectory}} " +
|
||||
"--batch " +
|
||||
"--url {{.S3Endpoint}} " +
|
||||
"--region {{.Region}} " +
|
||||
"--retry"
|
||||
}
|
||||
|
||||
|
@ -87,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 == "" {
|
||||
|
@ -97,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)...)
|
||||
|
||||
|
@ -186,6 +189,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
|
||||
// Build the steps
|
||||
steps := []multistep.Step{
|
||||
&awscommon.StepSourceAMIInfo{
|
||||
SourceAmi: b.config.SourceAmi,
|
||||
EnhancedNetworking: b.config.AMIEnhancedNetworking,
|
||||
},
|
||||
&awscommon.StepKeyPair{
|
||||
Debug: b.config.PackerDebug,
|
||||
DebugKeyPath: fmt.Sprintf("ec2_%s.pem", b.config.PackerBuildName),
|
||||
|
@ -199,7 +206,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
},
|
||||
&awscommon.StepRunSourceInstance{
|
||||
Debug: b.config.PackerDebug,
|
||||
ExpectedRootDevice: "instance-store",
|
||||
SpotPrice: b.config.SpotPrice,
|
||||
SpotPriceProduct: b.config.SpotPriceAutoProduct,
|
||||
InstanceType: b.config.InstanceType,
|
||||
IamInstanceProfile: b.config.IamInstanceProfile,
|
||||
UserData: b.config.UserData,
|
||||
|
@ -212,14 +220,19 @@ 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{},
|
||||
&StepUploadX509Cert{},
|
||||
&StepBundleVolume{},
|
||||
&StepUploadBundle{},
|
||||
&StepBundleVolume{
|
||||
Debug: b.config.PackerDebug,
|
||||
},
|
||||
&StepUploadBundle{
|
||||
Debug: b.config.PackerDebug,
|
||||
},
|
||||
&StepRegisterAMI{},
|
||||
&awscommon.StepAMIRegionCopy{
|
||||
Regions: b.config.AMIRegions,
|
||||
|
|
|
@ -2,6 +2,7 @@ package instance
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
|
@ -17,7 +18,9 @@ type bundleCmdData struct {
|
|||
PrivatePath string
|
||||
}
|
||||
|
||||
type StepBundleVolume struct{}
|
||||
type StepBundleVolume struct {
|
||||
Debug bool
|
||||
}
|
||||
|
||||
func (s *StepBundleVolume) Run(state multistep.StateBag) multistep.StepAction {
|
||||
comm := state.Get("communicator").(packer.Communicator)
|
||||
|
@ -48,6 +51,11 @@ func (s *StepBundleVolume) Run(state multistep.StateBag) multistep.StepAction {
|
|||
ui.Say("Bundling the volume...")
|
||||
cmd := new(packer.RemoteCmd)
|
||||
cmd.Command = config.BundleVolCommand
|
||||
|
||||
if s.Debug {
|
||||
ui.Say(fmt.Sprintf("Running: %s", config.BundleVolCommand))
|
||||
}
|
||||
|
||||
if err := cmd.StartWithUi(comm, ui); err != nil {
|
||||
state.Put("error", fmt.Errorf("Error bundling volume: %s", err))
|
||||
ui.Error(state.Get("error").(error).Error())
|
||||
|
|
|
@ -2,6 +2,7 @@ package instance
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
|
@ -24,6 +25,11 @@ func (s *StepRegisterAMI) Run(state multistep.StateBag) multistep.StepAction {
|
|||
VirtType: config.AMIVirtType,
|
||||
}
|
||||
|
||||
// Set SriovNetSupport to "simple". See http://goo.gl/icuXh5
|
||||
if config.AMIEnhancedNetworking {
|
||||
registerOpts.SriovNetSupport = "simple"
|
||||
}
|
||||
|
||||
registerResp, err := ec2conn.RegisterImage(registerOpts)
|
||||
if err != nil {
|
||||
state.Put("error", fmt.Errorf("Error registering AMI: %s", err))
|
||||
|
|
|
@ -2,6 +2,7 @@ package instance
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
@ -11,11 +12,13 @@ type uploadCmdData struct {
|
|||
BucketName string
|
||||
BundleDirectory string
|
||||
ManifestPath string
|
||||
S3Endpoint string
|
||||
Region string
|
||||
SecretKey string
|
||||
}
|
||||
|
||||
type StepUploadBundle struct{}
|
||||
type StepUploadBundle struct {
|
||||
Debug bool
|
||||
}
|
||||
|
||||
func (s *StepUploadBundle) Run(state multistep.StateBag) multistep.StepAction {
|
||||
comm := state.Get("communicator").(packer.Communicator)
|
||||
|
@ -37,7 +40,7 @@ func (s *StepUploadBundle) Run(state multistep.StateBag) multistep.StepAction {
|
|||
BucketName: config.S3Bucket,
|
||||
BundleDirectory: config.BundleDestination,
|
||||
ManifestPath: manifestPath,
|
||||
S3Endpoint: region.S3Endpoint,
|
||||
Region: region.Name,
|
||||
SecretKey: config.SecretKey,
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -49,6 +52,11 @@ func (s *StepUploadBundle) Run(state multistep.StateBag) multistep.StepAction {
|
|||
|
||||
ui.Say("Uploading the bundle...")
|
||||
cmd := &packer.RemoteCmd{Command: config.BundleUploadCommand}
|
||||
|
||||
if s.Debug {
|
||||
ui.Say(fmt.Sprintf("Running: %s", config.BundleUploadCommand))
|
||||
}
|
||||
|
||||
if err := cmd.StartWithUi(comm, ui); err != nil {
|
||||
state.Put("error", fmt.Errorf("Error uploading volume: %s", err))
|
||||
ui.Error(state.Get("error").(error).Error())
|
||||
|
|
|
@ -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,7 +8,6 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
|
@ -16,9 +15,9 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const DIGITALOCEAN_API_URL = "https://api.digitalocean.com"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
type Image struct {
|
||||
Id uint
|
||||
|
@ -55,23 +54,23 @@ type DigitalOceanClient struct {
|
|||
// The http client for communicating
|
||||
client *http.Client
|
||||
|
||||
// The base URL of the API
|
||||
BaseURL string
|
||||
|
||||
// Credentials
|
||||
ClientID string
|
||||
APIKey string
|
||||
|
||||
// The base URL of the API
|
||||
APIURL string
|
||||
}
|
||||
|
||||
// Creates a new client for communicating with DO
|
||||
func (d DigitalOceanClient) New(client string, key string) *DigitalOceanClient {
|
||||
func (d DigitalOceanClient) New(client string, key string, url string) *DigitalOceanClient {
|
||||
c := &DigitalOceanClient{
|
||||
client: &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
},
|
||||
},
|
||||
BaseURL: DIGITALOCEAN_API_URL,
|
||||
APIURL: url,
|
||||
ClientID: client,
|
||||
APIKey: key,
|
||||
}
|
||||
|
@ -229,7 +228,7 @@ func NewRequest(d DigitalOceanClient, path string, params url.Values) (map[strin
|
|||
params.Set("client_id", d.ClientID)
|
||||
params.Set("api_key", d.APIKey)
|
||||
|
||||
url := fmt.Sprintf("%s/%s?%s", DIGITALOCEAN_API_URL, path, params.Encode())
|
||||
url := fmt.Sprintf("%s/%s?%s", d.APIURL, path, params.Encode())
|
||||
|
||||
// Do some basic scrubbing so sensitive information doesn't appear in logs
|
||||
scrubbedUrl := strings.Replace(url, d.ClientID, "CLIENT_ID", -1)
|
||||
|
|
|
@ -6,13 +6,14 @@ package digitalocean
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/common/uuid"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// see https://api.digitalocean.com/images/?client_id=[client_id]&api_key=[api_key]
|
||||
|
@ -38,6 +39,7 @@ type config struct {
|
|||
|
||||
ClientID string `mapstructure:"client_id"`
|
||||
APIKey string `mapstructure:"api_key"`
|
||||
APIURL string `mapstructure:"api_url"`
|
||||
RegionID uint `mapstructure:"region_id"`
|
||||
SizeID uint `mapstructure:"size_id"`
|
||||
ImageID uint `mapstructure:"image_id"`
|
||||
|
@ -94,6 +96,11 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
b.config.ClientID = os.Getenv("DIGITALOCEAN_CLIENT_ID")
|
||||
}
|
||||
|
||||
if b.config.APIURL == "" {
|
||||
// Default to environment variable for api_url, if it exists
|
||||
b.config.APIURL = os.Getenv("DIGITALOCEAN_API_URL")
|
||||
}
|
||||
|
||||
if b.config.Region == "" {
|
||||
if b.config.RegionID != 0 {
|
||||
b.config.Region = fmt.Sprintf("%v", b.config.RegionID)
|
||||
|
@ -151,8 +158,12 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
}
|
||||
|
||||
templates := map[string]*string{
|
||||
"region": &b.config.Region,
|
||||
"size": &b.config.Size,
|
||||
"image": &b.config.Image,
|
||||
"client_id": &b.config.ClientID,
|
||||
"api_key": &b.config.APIKey,
|
||||
"api_url": &b.config.APIURL,
|
||||
"snapshot_name": &b.config.SnapshotName,
|
||||
"droplet_name": &b.config.DropletName,
|
||||
"ssh_username": &b.config.SSHUsername,
|
||||
|
@ -175,6 +186,10 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
errs, errors.New("a client_id must be specified"))
|
||||
}
|
||||
|
||||
if b.config.APIURL == "" {
|
||||
b.config.APIURL = "https://api.digitalocean.com"
|
||||
}
|
||||
|
||||
if b.config.APIKey == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("an api_key must be specified"))
|
||||
|
@ -204,7 +219,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
|
||||
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
||||
// Initialize the DO API client
|
||||
client := DigitalOceanClient{}.New(b.config.ClientID, b.config.APIKey)
|
||||
client := DigitalOceanClient{}.New(b.config.ClientID, b.config.APIKey, b.config.APIURL)
|
||||
|
||||
// Set up the state
|
||||
state := new(multistep.BasicStateBag)
|
||||
|
|
|
@ -2,6 +2,7 @@ package digitalocean
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
@ -53,7 +54,7 @@ func (s *stepCreateDroplet) Cleanup(state multistep.StateBag) {
|
|||
err := client.DestroyDroplet(s.dropletId)
|
||||
if err != nil {
|
||||
curlstr := fmt.Sprintf("curl '%v/droplets/%v/destroy?client_id=%v&api_key=%v'",
|
||||
DIGITALOCEAN_API_URL, s.dropletId, c.ClientID, c.APIKey)
|
||||
c.APIURL, s.dropletId, c.ClientID, c.APIKey)
|
||||
|
||||
ui.Error(fmt.Sprintf(
|
||||
"Error destroying droplet. Please destroy it manually: %v", curlstr))
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
package digitalocean
|
||||
|
||||
import (
|
||||
"code.google.com/p/gosshold/ssh"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"code.google.com/p/gosshold/ssh"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common/uuid"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
)
|
||||
|
||||
type stepCreateSSHKey struct {
|
||||
|
@ -78,7 +79,7 @@ func (s *stepCreateSSHKey) Cleanup(state multistep.StateBag) {
|
|||
err := client.DestroyKey(s.keyId)
|
||||
|
||||
curlstr := fmt.Sprintf("curl '%v/ssh_keys/%v/destroy?client_id=%v&api_key=%v'",
|
||||
DIGITALOCEAN_API_URL, s.keyId, c.ClientID, c.APIKey)
|
||||
c.APIURL, s.keyId, c.ClientID, c.APIKey)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error cleaning up ssh key: %v", err.Error())
|
||||
|
|
|
@ -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 {
|
||||
|
@ -157,5 +258,10 @@ func (d *DockerDriver) Verify() error {
|
|||
return err
|
||||
}
|
||||
|
||||
if v := os.Getenv("DOCKER_HOST"); v != "" {
|
||||
return fmt.Errorf(
|
||||
"DOCKER_HOST cannot be set with the Packer Docker builder.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
SSHWaitTimeout: 5 * time.Minute,
|
||||
},
|
||||
new(common.StepProvision),
|
||||
new(StepUpdateGsutil),
|
||||
new(StepUpdateGcloud),
|
||||
new(StepCreateImage),
|
||||
new(StepUploadImage),
|
||||
new(StepRegisterImage),
|
||||
|
|
|
@ -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,26 +16,29 @@ import (
|
|||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
|
||||
BucketName string `mapstructure:"bucket_name"`
|
||||
ClientSecretsFile string `mapstructure:"client_secrets_file"`
|
||||
ImageName string `mapstructure:"image_name"`
|
||||
ImageDescription string `mapstructure:"image_description"`
|
||||
InstanceName string `mapstructure:"instance_name"`
|
||||
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"`
|
||||
SSHUsername string `mapstructure:"ssh_username"`
|
||||
SSHPort uint `mapstructure:"ssh_port"`
|
||||
RawSSHTimeout string `mapstructure:"ssh_timeout"`
|
||||
RawStateTimeout string `mapstructure:"state_timeout"`
|
||||
Tags []string `mapstructure:"tags"`
|
||||
Zone string `mapstructure:"zone"`
|
||||
AccountFile string `mapstructure:"account_file"`
|
||||
ClientSecretsFile string `mapstructure:"client_secrets_file"`
|
||||
ProjectId string `mapstructure:"project_id"`
|
||||
|
||||
clientSecrets *clientSecrets
|
||||
BucketName string `mapstructure:"bucket_name"`
|
||||
DiskSizeGb int64 `mapstructure:"disk_size"`
|
||||
ImageName string `mapstructure:"image_name"`
|
||||
ImageDescription string `mapstructure:"image_description"`
|
||||
InstanceName string `mapstructure:"instance_name"`
|
||||
MachineType string `mapstructure:"machine_type"`
|
||||
Metadata map[string]string `mapstructure:"metadata"`
|
||||
Network string `mapstructure:"network"`
|
||||
SourceImage string `mapstructure:"source_image"`
|
||||
SourceImageProjectId string `mapstructure:"source_image_project_id"`
|
||||
SSHUsername string `mapstructure:"ssh_username"`
|
||||
SSHPort uint `mapstructure:"ssh_port"`
|
||||
RawSSHTimeout string `mapstructure:"ssh_timeout"`
|
||||
RawStateTimeout string `mapstructure:"state_timeout"`
|
||||
Tags []string `mapstructure:"tags"`
|
||||
Zone string `mapstructure:"zone"`
|
||||
|
||||
account accountFile
|
||||
clientSecrets clientSecretsFile
|
||||
instanceName string
|
||||
privateKeyBytes []byte
|
||||
sshTimeout time.Duration
|
||||
|
@ -64,6 +67,10 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
|||
c.Network = "default"
|
||||
}
|
||||
|
||||
if c.DiskSizeGb == 0 {
|
||||
c.DiskSizeGb = 10
|
||||
}
|
||||
|
||||
if c.ImageDescription == "" {
|
||||
c.ImageDescription = "Created by Packer"
|
||||
}
|
||||
|
@ -98,21 +105,22 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
|||
|
||||
// Process Templates
|
||||
templates := map[string]*string{
|
||||
"bucket_name": &c.BucketName,
|
||||
"account_file": &c.AccountFile,
|
||||
"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,
|
||||
"ssh_username": &c.SSHUsername,
|
||||
"ssh_timeout": &c.RawSSHTimeout,
|
||||
"state_timeout": &c.RawStateTimeout,
|
||||
"zone": &c.Zone,
|
||||
|
||||
"bucket_name": &c.BucketName,
|
||||
"image_name": &c.ImageName,
|
||||
"image_description": &c.ImageDescription,
|
||||
"instance_name": &c.InstanceName,
|
||||
"machine_type": &c.MachineType,
|
||||
"network": &c.Network,
|
||||
"project_id": &c.ProjectId,
|
||||
"source_image": &c.SourceImage,
|
||||
"source_image_project_id": &c.SourceImageProjectId,
|
||||
"ssh_username": &c.SSHUsername,
|
||||
"ssh_timeout": &c.RawSSHTimeout,
|
||||
"state_timeout": &c.RawStateTimeout,
|
||||
"zone": &c.Zone,
|
||||
}
|
||||
|
||||
for n, ptr := range templates {
|
||||
|
@ -130,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"))
|
||||
|
@ -170,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,9 +23,15 @@ type Driver interface {
|
|||
WaitForInstance(state, zone, name string) <-chan error
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
Name string
|
||||
ProjectId string
|
||||
}
|
||||
|
||||
type InstanceConfig struct {
|
||||
Description string
|
||||
Image string
|
||||
DiskSizeGb int64
|
||||
Image Image
|
||||
MachineType string
|
||||
Metadata map[string]string
|
||||
Name string
|
||||
|
|
|
@ -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
|
||||
|
@ -134,7 +139,7 @@ func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) {
|
|||
}
|
||||
|
||||
// Get the image
|
||||
d.ui.Message(fmt.Sprintf("Loading image: %s", c.Image))
|
||||
d.ui.Message(fmt.Sprintf("Loading image: %s in project %s", c.Image.Name, c.Image.ProjectId))
|
||||
image, err := d.getImage(c.Image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -177,6 +182,7 @@ func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) {
|
|||
AutoDelete: true,
|
||||
InitializeParams: &compute.AttachedDiskInitializeParams{
|
||||
SourceImage: image.SelfLink,
|
||||
DiskSizeGb: c.DiskSizeGb,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -228,20 +234,17 @@ func (d *driverGCE) WaitForInstance(state, zone, name string) <-chan error {
|
|||
return errCh
|
||||
}
|
||||
|
||||
func (d *driverGCE) getImage(name string) (image *compute.Image, err error) {
|
||||
projects := []string{d.projectId, "debian-cloud", "centos-cloud"}
|
||||
func (d *driverGCE) getImage(img Image) (image *compute.Image, err error) {
|
||||
projects := []string{img.ProjectId, "centos-cloud", "coreos-cloud", "debian-cloud", "google-containers", "opensuse-cloud", "rhel-cloud", "suse-cloud", "windows-cloud"}
|
||||
for _, project := range projects {
|
||||
image, err = d.service.Images.Get(project, name).Do()
|
||||
image, err = d.service.Images.Get(project, img.Name).Do()
|
||||
if err == nil && image != nil && image.SelfLink != "" {
|
||||
return
|
||||
}
|
||||
image = nil
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
err = fmt.Errorf("Image could not be found: %s", name)
|
||||
}
|
||||
|
||||
err = fmt.Errorf("Image %s could not be found in any of these projects: %s", img.Name, projects)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -31,8 +31,8 @@ func (s *StepCreateImage) Run(state multistep.StateBag) multistep.StepAction {
|
|||
|
||||
ui.Say("Creating image...")
|
||||
cmd := new(packer.RemoteCmd)
|
||||
cmd.Command = fmt.Sprintf("%s%s --output_file_name %s",
|
||||
sudoPrefix, imageBundleCmd, imageFilename)
|
||||
cmd.Command = fmt.Sprintf("%s%s --output_file_name %s --fssize %d",
|
||||
sudoPrefix, imageBundleCmd, imageFilename, config.DiskSizeGb*1024*1024*1024)
|
||||
err := cmd.StartWithUi(comm, ui)
|
||||
if err == nil && cmd.ExitStatus != 0 {
|
||||
err = fmt.Errorf(
|
||||
|
|
|
@ -16,6 +16,14 @@ type StepCreateInstance struct {
|
|||
instanceName string
|
||||
}
|
||||
|
||||
func (config *Config) getImage() Image {
|
||||
project := config.ProjectId
|
||||
if config.SourceImageProjectId != "" {
|
||||
project = config.SourceImageProjectId
|
||||
}
|
||||
return Image{Name: config.SourceImage, ProjectId: project}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
@ -28,7 +36,8 @@ func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction
|
|||
|
||||
errCh, err := driver.RunInstance(&InstanceConfig{
|
||||
Description: "New instance created by Packer",
|
||||
Image: config.SourceImage,
|
||||
DiskSizeGb: config.DiskSizeGb,
|
||||
Image: config.getImage(),
|
||||
MachineType: config.MachineType,
|
||||
Metadata: map[string]string{
|
||||
"sshKeys": fmt.Sprintf("%s:%s", config.SSHUsername, sshPublicKey),
|
||||
|
|
|
@ -7,9 +7,9 @@ import (
|
|||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// StepUpdateGsutil represents a Packer build step that updates the gsutil
|
||||
// StepUpdateGcloud represents a Packer build step that updates the gsutil
|
||||
// utility to the latest version available.
|
||||
type StepUpdateGsutil int
|
||||
type StepUpdateGcloud int
|
||||
|
||||
// Run executes the Packer build step that updates the gsutil utility to the
|
||||
// latest version available.
|
||||
|
@ -17,7 +17,7 @@ type StepUpdateGsutil int
|
|||
// This step is required to prevent the image creation process from hanging;
|
||||
// the image creation process utilizes the gcimagebundle cli tool which will
|
||||
// prompt to update gsutil if a newer version is available.
|
||||
func (s *StepUpdateGsutil) Run(state multistep.StateBag) multistep.StepAction {
|
||||
func (s *StepUpdateGcloud) Run(state multistep.StateBag) multistep.StepAction {
|
||||
comm := state.Get("communicator").(packer.Communicator)
|
||||
config := state.Get("config").(*Config)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
@ -28,18 +28,18 @@ func (s *StepUpdateGsutil) Run(state multistep.StateBag) multistep.StepAction {
|
|||
sudoPrefix = "sudo "
|
||||
}
|
||||
|
||||
gsutilUpdateCmd := "/usr/local/bin/gsutil update -n -f"
|
||||
gsutilUpdateCmd := "/usr/local/bin/gcloud -q components update"
|
||||
cmd := new(packer.RemoteCmd)
|
||||
cmd.Command = fmt.Sprintf("%s%s", sudoPrefix, gsutilUpdateCmd)
|
||||
|
||||
ui.Say("Updating gsutil...")
|
||||
ui.Say("Updating gcloud components...")
|
||||
err := cmd.StartWithUi(comm, ui)
|
||||
if err == nil && cmd.ExitStatus != 0 {
|
||||
err = fmt.Errorf(
|
||||
"gsutil update exited with non-zero exit status: %d", cmd.ExitStatus)
|
||||
"gcloud components update exited with non-zero exit status: %d", cmd.ExitStatus)
|
||||
}
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error updating gsutil: %s", err)
|
||||
err := fmt.Errorf("Error updating gcloud components: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
|
@ -49,4 +49,4 @@ func (s *StepUpdateGsutil) Run(state multistep.StateBag) multistep.StepAction {
|
|||
}
|
||||
|
||||
// Cleanup.
|
||||
func (s *StepUpdateGsutil) Cleanup(state multistep.StateBag) {}
|
||||
func (s *StepUpdateGcloud) Cleanup(state multistep.StateBag) {}
|
|
@ -8,13 +8,13 @@ import (
|
|||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
func TestStepUpdateGsutil_impl(t *testing.T) {
|
||||
var _ multistep.Step = new(StepUpdateGsutil)
|
||||
func TestStepUpdateGcloud_impl(t *testing.T) {
|
||||
var _ multistep.Step = new(StepUpdateGcloud)
|
||||
}
|
||||
|
||||
func TestStepUpdateGsutil(t *testing.T) {
|
||||
func TestStepUpdateGcloud(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := new(StepUpdateGsutil)
|
||||
step := new(StepUpdateGcloud)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
comm := new(packer.MockCommunicator)
|
||||
|
@ -32,14 +32,14 @@ func TestStepUpdateGsutil(t *testing.T) {
|
|||
if strings.HasPrefix(comm.StartCmd.Command, "sudo") {
|
||||
t.Fatal("should not sudo")
|
||||
}
|
||||
if !strings.Contains(comm.StartCmd.Command, "gsutil update") {
|
||||
if !strings.Contains(comm.StartCmd.Command, "gcloud -q components update") {
|
||||
t.Fatalf("bad command: %#v", comm.StartCmd.Command)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepUpdateGsutil_badExitStatus(t *testing.T) {
|
||||
func TestStepUpdateGcloud_badExitStatus(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := new(StepUpdateGsutil)
|
||||
step := new(StepUpdateGcloud)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
comm := new(packer.MockCommunicator)
|
||||
|
@ -56,9 +56,9 @@ func TestStepUpdateGsutil_badExitStatus(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestStepUpdateGsutil_nonRoot(t *testing.T) {
|
||||
func TestStepUpdateGcloud_nonRoot(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := new(StepUpdateGsutil)
|
||||
step := new(StepUpdateGcloud)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
comm := new(packer.MockCommunicator)
|
||||
|
@ -79,7 +79,7 @@ func TestStepUpdateGsutil_nonRoot(t *testing.T) {
|
|||
if !strings.HasPrefix(comm.StartCmd.Command, "sudo") {
|
||||
t.Fatal("should sudo")
|
||||
}
|
||||
if !strings.Contains(comm.StartCmd.Command, "gsutil update") {
|
||||
if !strings.Contains(comm.StartCmd.Command, "gcloud -q components update") {
|
||||
t.Fatalf("bad command: %#v", comm.StartCmd.Command)
|
||||
}
|
||||
}
|
|
@ -32,14 +32,28 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
|||
|
||||
c.tpl.UserVars = c.PackerUserVars
|
||||
|
||||
// Defaults
|
||||
if c.Port == 0 {
|
||||
c.Port = 22
|
||||
}
|
||||
// (none so far)
|
||||
|
||||
errs := common.CheckUnusedConfig(md)
|
||||
|
||||
templates := map[string]*string{
|
||||
"host": &c.Host,
|
||||
"ssh_username": &c.SSHUsername,
|
||||
"ssh_password": &c.SSHPassword,
|
||||
"ssh_private_key_file": &c.SSHPrivateKeyFile,
|
||||
}
|
||||
|
||||
for n, ptr := range templates {
|
||||
var err error
|
||||
*ptr, err = c.tpl.Process(*ptr, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Error processing %s: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
if c.Host == "" {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("host must be specified"))
|
||||
|
|
|
@ -115,8 +115,10 @@ func (c *AccessConfig) Prepare(t *packer.ConfigTemplate) []error {
|
|||
}
|
||||
}
|
||||
|
||||
if c.Region() == "" {
|
||||
errs = append(errs, fmt.Errorf("region must be specified"))
|
||||
if strings.HasPrefix(c.Provider, "rackspace") {
|
||||
if c.Region() == "" {
|
||||
errs = append(errs, fmt.Errorf("region must be specified when using rackspace"))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
|
|
|
@ -8,13 +8,22 @@ func testAccessConfig() *AccessConfig {
|
|||
return &AccessConfig{}
|
||||
}
|
||||
|
||||
func TestAccessConfigPrepare_NoRegion(t *testing.T) {
|
||||
func TestAccessConfigPrepare_NoRegion_Rackspace(t *testing.T) {
|
||||
c := testAccessConfig()
|
||||
c.Provider = "rackspace-us"
|
||||
if err := c.Prepare(nil); err == nil {
|
||||
t.Fatalf("shouldn't have err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccessConfigPrepare_NoRegion_PrivateCloud(t *testing.T) {
|
||||
c := testAccessConfig()
|
||||
c.Provider = "http://some-keystone-server:5000/v2.0"
|
||||
if err := c.Prepare(nil); err != nil {
|
||||
t.Fatalf("shouldn't have err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccessConfigPrepare_Region(t *testing.T) {
|
||||
dfw := "DFW"
|
||||
c := testAccessConfig()
|
||||
|
|
|
@ -92,6 +92,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
Flavor: b.config.Flavor,
|
||||
SourceImage: b.config.SourceImage,
|
||||
SecurityGroups: b.config.SecurityGroups,
|
||||
Networks: b.config.Networks,
|
||||
},
|
||||
&StepAllocateIp{
|
||||
FloatingIpPool: b.config.FloatingIpPool,
|
||||
|
|
|
@ -20,6 +20,7 @@ type RunConfig struct {
|
|||
FloatingIpPool string `mapstructure:"floating_ip_pool"`
|
||||
FloatingIp string `mapstructure:"floating_ip"`
|
||||
SecurityGroups []string `mapstructure:"security_groups"`
|
||||
Networks []string `mapstructure:"networks"`
|
||||
|
||||
// Unexported fields that are calculated from others
|
||||
sshTimeout time.Duration
|
||||
|
@ -67,7 +68,7 @@ func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error {
|
|||
}
|
||||
|
||||
templates := map[string]*string{
|
||||
"flavlor": &c.Flavor,
|
||||
"flavor": &c.Flavor,
|
||||
"ssh_timeout": &c.RawSSHTimeout,
|
||||
"ssh_username": &c.SSHUsername,
|
||||
"source_image": &c.SourceImage,
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/racker/perigee"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"log"
|
||||
"time"
|
||||
|
@ -30,15 +31,21 @@ type StateChangeConf struct {
|
|||
}
|
||||
|
||||
// ServerStateRefreshFunc returns a StateRefreshFunc that is used to watch
|
||||
// an openstacn server.
|
||||
// an openstack server.
|
||||
func ServerStateRefreshFunc(csp gophercloud.CloudServersProvider, s *gophercloud.Server) StateRefreshFunc {
|
||||
return func() (interface{}, string, int, error) {
|
||||
resp, err := csp.ServerById(s.Id)
|
||||
if err != nil {
|
||||
log.Printf("Error on ServerStateRefresh: %s", err)
|
||||
return nil, "", 0, err
|
||||
}
|
||||
urce, ok := err.(*perigee.UnexpectedResponseCodeError)
|
||||
if ok && (urce.Actual == 404) {
|
||||
log.Printf("404 on ServerStateRefresh, returning DELETED")
|
||||
|
||||
return nil, "DELETED", 0, nil
|
||||
} else {
|
||||
log.Printf("Error on ServerStateRefresh: %s", err)
|
||||
return nil, "", 0, err
|
||||
}
|
||||
}
|
||||
return resp, resp.Status, resp.Progress, nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ func SSHAddress(csp gophercloud.CloudServersProvider, port int) func(multistep.S
|
|||
for pool, addresses := range ip_pools {
|
||||
if pool != "" {
|
||||
for _, address := range addresses {
|
||||
if address.Addr != "" {
|
||||
if address.Addr != "" && address.Version == 4 {
|
||||
return fmt.Sprintf("%s:%d", address.Addr, port), nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,10 @@ func (s *StepKeyPair) Run(state multistep.StateBag) multistep.StepAction {
|
|||
state.Put("error", fmt.Errorf("Error creating temporary keypair: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
if keyResp.PrivateKey == "" {
|
||||
state.Put("error", fmt.Errorf("The temporary keypair returned was blank"))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// If we're in debug mode, output the private key to the working
|
||||
// directory.
|
||||
|
|
|
@ -13,6 +13,7 @@ type StepRunSourceServer struct {
|
|||
Name string
|
||||
SourceImage string
|
||||
SecurityGroups []string
|
||||
Networks []string
|
||||
|
||||
server *gophercloud.Server
|
||||
}
|
||||
|
@ -30,12 +31,18 @@ func (s *StepRunSourceServer) Run(state multistep.StateBag) multistep.StepAction
|
|||
securityGroups[i]["name"] = groupName
|
||||
}
|
||||
|
||||
networks := make([]gophercloud.NetworkConfig, len(s.Networks))
|
||||
for i, networkUuid := range s.Networks {
|
||||
networks[i].Uuid = networkUuid
|
||||
}
|
||||
|
||||
server := gophercloud.NewServer{
|
||||
Name: s.Name,
|
||||
ImageRef: s.SourceImage,
|
||||
FlavorRef: s.Flavor,
|
||||
KeyPairName: keyName,
|
||||
SecurityGroup: securityGroups,
|
||||
Networks: networks,
|
||||
}
|
||||
|
||||
serverResp, err := csp.CreateServer(server)
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A driver is able to talk to Parallels and perform certain
|
||||
|
@ -12,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)
|
||||
|
@ -24,6 +30,9 @@ type Driver interface {
|
|||
// Prlctl executes the given Prlctl command
|
||||
Prlctl(...string) error
|
||||
|
||||
// Get the path to the Parallels Tools ISO for the given flavor.
|
||||
ToolsIsoPath(string) (string, error)
|
||||
|
||||
// Verify checks to make sure that this driver should function
|
||||
// properly. If there is any indication the driver can't function,
|
||||
// this will return an error.
|
||||
|
@ -32,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
|
||||
|
@ -43,7 +52,14 @@ type Driver interface {
|
|||
}
|
||||
|
||||
func NewDriver() (Driver, error) {
|
||||
var drivers map[string]Driver
|
||||
var prlctlPath string
|
||||
var supportedVersions []string
|
||||
|
||||
if runtime.GOOS != "darwin" {
|
||||
return nil, fmt.Errorf(
|
||||
"Parallels builder works only on \"darwin\" platform!")
|
||||
}
|
||||
|
||||
if prlctlPath == "" {
|
||||
var err error
|
||||
|
@ -54,10 +70,27 @@ func NewDriver() (Driver, error) {
|
|||
}
|
||||
|
||||
log.Printf("prlctl path: %s", prlctlPath)
|
||||
driver := &Parallels9Driver{prlctlPath}
|
||||
if err := driver.Verify(); err != nil {
|
||||
return nil, err
|
||||
|
||||
drivers = map[string]Driver{
|
||||
"10": &Parallels10Driver{
|
||||
Parallels9Driver: Parallels9Driver{
|
||||
PrlctlPath: prlctlPath,
|
||||
},
|
||||
},
|
||||
"9": &Parallels9Driver{
|
||||
PrlctlPath: prlctlPath,
|
||||
},
|
||||
}
|
||||
|
||||
return driver, nil
|
||||
for v, d := range drivers {
|
||||
version, _ := d.Version()
|
||||
if strings.HasPrefix(version, v) {
|
||||
return d, nil
|
||||
}
|
||||
supportedVersions = append(supportedVersions, v)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf(
|
||||
"Unable to initialize any driver. Supported Parallels Desktop versions: "+
|
||||
"%s\n", strings.Join(supportedVersions, ", "))
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package common
|
||||
|
||||
// Parallels10Driver are inherited from Parallels9Driver.
|
||||
type Parallels10Driver struct {
|
||||
Parallels9Driver
|
||||
}
|
|
@ -3,13 +3,16 @@ package common
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/going/toolkit/xmlpath"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/going/toolkit/xmlpath"
|
||||
)
|
||||
|
||||
type Parallels9Driver struct {
|
||||
|
@ -17,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 {
|
||||
|
@ -29,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)
|
||||
|
@ -70,6 +76,48 @@ func getConfigValueFromXpath(path, xpath string) (string, error) {
|
|||
return value, nil
|
||||
}
|
||||
|
||||
// Finds an application bundle by identifier (for "darwin" platform only)
|
||||
func getAppPath(bundleId string) (string, error) {
|
||||
var stdout bytes.Buffer
|
||||
|
||||
cmd := exec.Command("mdfind", "kMDItemCFBundleIdentifier ==", bundleId)
|
||||
cmd.Stdout = &stdout
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pathOutput := strings.TrimSpace(stdout.String())
|
||||
if pathOutput == "" {
|
||||
return "", fmt.Errorf(
|
||||
"Could not detect Parallels Desktop! Make sure it is properly installed.")
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
@ -79,6 +127,8 @@ func (d *Parallels9Driver) IsRunning(name string) (bool, error) {
|
|||
return false, err
|
||||
}
|
||||
|
||||
log.Printf("Checking VM state: %s\n", strings.TrimSpace(stdout.String()))
|
||||
|
||||
for _, line := range strings.Split(stdout.String(), "\n") {
|
||||
if line == "running" {
|
||||
return true, nil
|
||||
|
@ -90,6 +140,9 @@ func (d *Parallels9Driver) IsRunning(name string) (bool, error) {
|
|||
if line == "paused" {
|
||||
return true, nil
|
||||
}
|
||||
if line == "stopping" {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
|
@ -129,43 +182,53 @@ func (d *Parallels9Driver) Prlctl(args ...string) error {
|
|||
}
|
||||
|
||||
func (d *Parallels9Driver) Verify() error {
|
||||
version, _ := d.Version()
|
||||
if !strings.HasPrefix(version, "9.") {
|
||||
return fmt.Errorf("The packer-parallels builder plugin only supports Parallels Desktop v. 9. You have: %s!\n", version)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Parallels9Driver) Version() (string, error) {
|
||||
var stdout bytes.Buffer
|
||||
|
||||
cmd := exec.Command(d.PrlctlPath, "--version")
|
||||
cmd.Stdout = &stdout
|
||||
if err := cmd.Run(); err != nil {
|
||||
out, err := exec.Command(d.PrlctlPath, "--version").Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
versionOutput := strings.TrimSpace(stdout.String())
|
||||
re := regexp.MustCompile("prlctl version ([0-9\\.]+)")
|
||||
verMatch := re.FindAllStringSubmatch(versionOutput, 1)
|
||||
|
||||
if len(verMatch) != 1 {
|
||||
return "", fmt.Errorf("prlctl version not found!\n")
|
||||
versionRe := regexp.MustCompile(`prlctl version (\d+\.\d+.\d+)`)
|
||||
matches := versionRe.FindStringSubmatch(string(out))
|
||||
if matches == nil {
|
||||
return "", fmt.Errorf(
|
||||
"Could not find Parallels Desktop version in output:\n%s", string(out))
|
||||
}
|
||||
|
||||
version := verMatch[0][1]
|
||||
log.Printf("prlctl version: %s\n", version)
|
||||
version := matches[1]
|
||||
log.Printf("Parallels Desktop version: %s", version)
|
||||
return version, nil
|
||||
}
|
||||
|
||||
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())
|
||||
|
@ -239,3 +302,14 @@ func (d *Parallels9Driver) IpAddress(mac string) (string, error) {
|
|||
log.Printf("Found IP lease: %s for MAC address %s\n", ip, mac)
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
func (d *Parallels9Driver) ToolsIsoPath(k string) (string, error) {
|
||||
appPath, err := getAppPath("com.parallels.desktop.console")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
toolsPath := filepath.Join(appPath, "Contents", "Resources", "Tools", "prl-tools-"+k+".iso")
|
||||
log.Printf("Parallels Tools path: '%s'", toolsPath)
|
||||
return toolsPath, nil
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -31,6 +37,11 @@ type DriverMock struct {
|
|||
SendKeyScanCodesCalls [][]string
|
||||
SendKeyScanCodesErrs []error
|
||||
|
||||
ToolsIsoPathCalled bool
|
||||
ToolsIsoPathFlavor string
|
||||
ToolsIsoPathResult string
|
||||
ToolsIsoPathErr error
|
||||
|
||||
MacName string
|
||||
MacReturn string
|
||||
MacError error
|
||||
|
@ -40,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
|
||||
|
@ -98,3 +116,9 @@ func (d *DriverMock) IpAddress(mac string) (string, error) {
|
|||
d.IpAddressMac = mac
|
||||
return d.IpAddressReturn, d.IpAddressError
|
||||
}
|
||||
|
||||
func (d *DriverMock) ToolsIsoPath(flavor string) (string, error) {
|
||||
d.ToolsIsoPathCalled = true
|
||||
d.ToolsIsoPathFlavor = flavor
|
||||
return d.ToolsIsoPathResult, d.ToolsIsoPathErr
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package iso
|
||||
package common
|
||||
|
||||
// Interface to help find the host IP that is available from within
|
||||
// the Parallels virtual machines.
|
|
@ -1,4 +1,4 @@
|
|||
package iso
|
||||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
|
@ -1,4 +1,4 @@
|
|||
package iso
|
||||
package common
|
||||
|
||||
import "testing"
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,15 +33,24 @@ func (s *StepAttachFloppy) Run(state multistep.StateBag) multistep.StepAction {
|
|||
ui := state.Get("ui").(packer.Ui)
|
||||
vmName := state.Get("vmName").(string)
|
||||
|
||||
ui.Say("Attaching floppy disk...")
|
||||
ui.Say("Deleting any current floppy disk...")
|
||||
// Delete the floppy disk controller
|
||||
del_command := []string{
|
||||
"set", vmName,
|
||||
"--device-del", "fdd0",
|
||||
}
|
||||
// This will almost certainly fail with 'The fdd0 device does not exist.'
|
||||
driver.Prlctl(del_command...)
|
||||
|
||||
// Create the floppy disk controller
|
||||
command := []string{
|
||||
ui.Say("Attaching floppy disk...")
|
||||
// Attaching the floppy disk
|
||||
add_command := []string{
|
||||
"set", vmName,
|
||||
"--device-add", "fdd",
|
||||
"--image", floppyPath,
|
||||
"--connect",
|
||||
}
|
||||
if err := driver.Prlctl(command...); err != nil {
|
||||
if err := driver.Prlctl(add_command...); err != nil {
|
||||
state.Put("error", fmt.Errorf("Error adding floppy: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
@ -52,4 +61,18 @@ func (s *StepAttachFloppy) Run(state multistep.StateBag) multistep.StepAction {
|
|||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepAttachFloppy) Cleanup(state multistep.StateBag) {}
|
||||
func (s *StepAttachFloppy) Cleanup(state multistep.StateBag) {
|
||||
driver := state.Get("driver").(Driver)
|
||||
vmName := state.Get("vmName").(string)
|
||||
|
||||
if s.floppyPath == "" {
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("Detaching floppy disk...")
|
||||
command := []string{
|
||||
"set", vmName,
|
||||
"--device-del", "fdd0",
|
||||
}
|
||||
driver.Prlctl(command...)
|
||||
}
|
||||
|
|
|
@ -36,17 +36,30 @@ func TestStepAttachFloppy(t *testing.T) {
|
|||
t.Fatal("should NOT have error")
|
||||
}
|
||||
|
||||
if len(driver.PrlctlCalls) != 1 {
|
||||
if len(driver.PrlctlCalls) != 2 {
|
||||
t.Fatal("not enough calls to prlctl")
|
||||
}
|
||||
|
||||
if driver.PrlctlCalls[0][0] != "set" {
|
||||
t.Fatal("bad call")
|
||||
}
|
||||
if driver.PrlctlCalls[0][2] != "--device-add" {
|
||||
if driver.PrlctlCalls[0][2] != "--device-del" {
|
||||
t.Fatal("bad call")
|
||||
}
|
||||
if driver.PrlctlCalls[0][3] != "fdd" {
|
||||
if driver.PrlctlCalls[0][3] != "fdd0" {
|
||||
t.Fatal("bad call")
|
||||
}
|
||||
|
||||
if driver.PrlctlCalls[1][0] != "set" {
|
||||
t.Fatal("bad call")
|
||||
}
|
||||
if driver.PrlctlCalls[1][2] != "--device-add" {
|
||||
t.Fatal("bad call")
|
||||
}
|
||||
if driver.PrlctlCalls[1][3] != "fdd" {
|
||||
t.Fatal("bad call")
|
||||
}
|
||||
if driver.PrlctlCalls[1][6] != "--connect" {
|
||||
t.Fatal("bad call")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,19 +7,19 @@ import (
|
|||
"log"
|
||||
)
|
||||
|
||||
// This step attaches the Parallels Tools as a inserted CD onto
|
||||
// This step attaches the Parallels Tools as an inserted CD onto
|
||||
// the virtual machine.
|
||||
//
|
||||
// Uses:
|
||||
// driver Driver
|
||||
// toolsPath string
|
||||
// parallels_tools_path string
|
||||
// ui packer.Ui
|
||||
// vmName string
|
||||
//
|
||||
// Produces:
|
||||
type StepAttachParallelsTools struct {
|
||||
ParallelsToolsHostPath string
|
||||
ParallelsToolsMode string
|
||||
cdromDevice string
|
||||
ParallelsToolsMode string
|
||||
}
|
||||
|
||||
func (s *StepAttachParallelsTools) Run(state multistep.StateBag) multistep.StepAction {
|
||||
|
@ -33,24 +33,44 @@ func (s *StepAttachParallelsTools) Run(state multistep.StateBag) multistep.StepA
|
|||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
// Get the Paralells Tools path on the host machine
|
||||
parallelsToolsPath := state.Get("parallels_tools_path").(string)
|
||||
|
||||
// Attach the guest additions to the computer
|
||||
log.Println("Attaching Parallels Tools ISO onto IDE controller...")
|
||||
command := []string{
|
||||
"set", vmName,
|
||||
"--device-add", "cdrom",
|
||||
"--image", s.ParallelsToolsHostPath,
|
||||
}
|
||||
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) {}
|
||||
func (s *StepAttachParallelsTools) Cleanup(state multistep.StateBag) {
|
||||
if s.cdromDevice == "" {
|
||||
return
|
||||
}
|
||||
|
||||
driver := state.Get("driver").(Driver)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
vmName := state.Get("vmName").(string)
|
||||
|
||||
log.Println("Detaching Parallels Tools ISO...")
|
||||
|
||||
command := []string{
|
||||
"set", vmName,
|
||||
"--device-del", s.cdromDevice,
|
||||
}
|
||||
|
||||
if err := driver.Prlctl(command...); err != nil {
|
||||
ui.Error(fmt.Sprintf("Error detaching Parallels Tools ISO: %s", err))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"os"
|
||||
)
|
||||
|
||||
// This step prepares parameters related to Parallels Tools.
|
||||
//
|
||||
// Uses:
|
||||
// driver Driver
|
||||
//
|
||||
// Produces:
|
||||
// parallels_tools_path string
|
||||
type StepPrepareParallelsTools struct {
|
||||
ParallelsToolsFlavor string
|
||||
ParallelsToolsMode string
|
||||
}
|
||||
|
||||
func (s *StepPrepareParallelsTools) Run(state multistep.StateBag) multistep.StepAction {
|
||||
driver := state.Get("driver").(Driver)
|
||||
|
||||
if s.ParallelsToolsMode == ParallelsToolsModeDisable {
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
path, err := driver.ToolsIsoPath(s.ParallelsToolsFlavor)
|
||||
|
||||
if err != nil {
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
state.Put("error", fmt.Errorf(
|
||||
"Couldn't find Parallels Tools for the '%s' flavor! Please, check the\n"+
|
||||
"value of 'parallels_tools_flavor'. Valid flavors are: 'win', 'lin',\n"+
|
||||
"'mac', 'os2' and 'other'", s.ParallelsToolsFlavor))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state.Put("parallels_tools_path", path)
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepPrepareParallelsTools) Cleanup(multistep.StateBag) {}
|
|
@ -0,0 +1,115 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
)
|
||||
|
||||
func TestStepPrepareParallelsTools_impl(t *testing.T) {
|
||||
var _ multistep.Step = new(StepPrepareParallelsTools)
|
||||
}
|
||||
|
||||
func TestStepPrepareParallelsTools(t *testing.T) {
|
||||
tf, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
tf.Close()
|
||||
defer os.Remove(tf.Name())
|
||||
|
||||
state := testState(t)
|
||||
step := &StepPrepareParallelsTools{
|
||||
ParallelsToolsMode: "",
|
||||
ParallelsToolsFlavor: "foo",
|
||||
}
|
||||
|
||||
driver := state.Get("driver").(*DriverMock)
|
||||
|
||||
// Mock results
|
||||
driver.ToolsIsoPathResult = tf.Name()
|
||||
|
||||
// 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 the driver
|
||||
if !driver.ToolsIsoPathCalled {
|
||||
t.Fatal("tools iso path should be called")
|
||||
}
|
||||
if driver.ToolsIsoPathFlavor != "foo" {
|
||||
t.Fatalf("bad: %#v", driver.ToolsIsoPathFlavor)
|
||||
}
|
||||
|
||||
// Test the resulting state
|
||||
path, ok := state.GetOk("parallels_tools_path")
|
||||
if !ok {
|
||||
t.Fatal("should have parallels_tools_path")
|
||||
}
|
||||
if path != tf.Name() {
|
||||
t.Fatalf("bad: %#v", path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepPrepareParallelsTools_disabled(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := &StepPrepareParallelsTools{
|
||||
ParallelsToolsFlavor: "foo",
|
||||
ParallelsToolsMode: ParallelsToolsModeDisable,
|
||||
}
|
||||
|
||||
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 the driver
|
||||
if driver.ToolsIsoPathCalled {
|
||||
t.Fatal("tools iso path should NOT be called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepPrepareParallelsTools_nonExist(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := &StepPrepareParallelsTools{
|
||||
ParallelsToolsFlavor: "foo",
|
||||
ParallelsToolsMode: "",
|
||||
}
|
||||
|
||||
driver := state.Get("driver").(*DriverMock)
|
||||
|
||||
// Mock results
|
||||
driver.ToolsIsoPathResult = "foo"
|
||||
|
||||
// Test the run
|
||||
if action := step.Run(state); action != multistep.ActionHalt {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
if _, ok := state.GetOk("error"); !ok {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test the driver
|
||||
if !driver.ToolsIsoPathCalled {
|
||||
t.Fatal("tools iso path should be called")
|
||||
}
|
||||
if driver.ToolsIsoPathFlavor != "foo" {
|
||||
t.Fatalf("bad: %#v", driver.ToolsIsoPathFlavor)
|
||||
}
|
||||
|
||||
// Test the resulting state
|
||||
if _, ok := state.GetOk("parallels_tools_path"); ok {
|
||||
t.Fatal("should NOT have parallels_tools_path")
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -1,15 +1,15 @@
|
|||
package iso
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
parallelscommon "github.com/mitchellh/packer/builder/parallels/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
const KeyLeftShift uint32 = 0xFFE1
|
||||
|
@ -20,11 +20,10 @@ 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:
|
||||
// config *config
|
||||
// driver Driver
|
||||
// http_port int
|
||||
// ui packer.Ui
|
||||
|
@ -32,24 +31,32 @@ type bootCommandTemplateData struct {
|
|||
//
|
||||
// Produces:
|
||||
// <nothing>
|
||||
type stepTypeBootCommand struct{}
|
||||
type StepTypeBootCommand struct {
|
||||
BootCommand []string
|
||||
HostInterfaces []string
|
||||
VMName string
|
||||
Tpl *packer.ConfigTemplate
|
||||
}
|
||||
|
||||
func (s *stepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*config)
|
||||
func (s *StepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAction {
|
||||
httpPort := state.Get("http_port").(uint)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
vmName := state.Get("vmName").(string)
|
||||
driver := state.Get("driver").(parallelscommon.Driver)
|
||||
driver := state.Get("driver").(Driver)
|
||||
|
||||
// Determine the host IP
|
||||
ipFinder := &IfconfigIPFinder{Devices: []string{"en0", "en1", "en2", "en3", "en4", "en5", "en6", "en7", "en8", "en9"}}
|
||||
hostIp := "0.0.0.0"
|
||||
|
||||
hostIp, err := ipFinder.HostIP()
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error detecting host IP: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
if len(s.HostInterfaces) > 0 {
|
||||
// Determine the host IP
|
||||
ipFinder := &IfconfigIPFinder{Devices: s.HostInterfaces}
|
||||
|
||||
ip, err := ipFinder.HostIP()
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error detecting host IP: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
hostIp = ip
|
||||
}
|
||||
|
||||
ui.Say(fmt.Sprintf("Host IP for the Parallels machine: %s", hostIp))
|
||||
|
@ -57,12 +64,12 @@ func (s *stepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAction
|
|||
tplData := &bootCommandTemplateData{
|
||||
hostIp,
|
||||
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)
|
||||
|
@ -73,7 +80,7 @@ func (s *stepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAction
|
|||
codes := []string{}
|
||||
for _, code := range scancodes(command) {
|
||||
if code == "wait" {
|
||||
if err := driver.SendKeyScanCodes(vmName, codes...); err != nil {
|
||||
if err := driver.SendKeyScanCodes(s.VMName, codes...); err != nil {
|
||||
err := fmt.Errorf("Error sending boot command: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
|
@ -85,7 +92,7 @@ func (s *stepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAction
|
|||
}
|
||||
|
||||
if code == "wait5" {
|
||||
if err := driver.SendKeyScanCodes(vmName, codes...); err != nil {
|
||||
if err := driver.SendKeyScanCodes(s.VMName, codes...); err != nil {
|
||||
err := fmt.Errorf("Error sending boot command: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
|
@ -97,7 +104,7 @@ func (s *stepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAction
|
|||
}
|
||||
|
||||
if code == "wait10" {
|
||||
if err := driver.SendKeyScanCodes(vmName, codes...); err != nil {
|
||||
if err := driver.SendKeyScanCodes(s.VMName, codes...); err != nil {
|
||||
err := fmt.Errorf("Error sending boot command: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
|
@ -116,7 +123,7 @@ func (s *stepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAction
|
|||
codes = append(codes, code)
|
||||
}
|
||||
log.Printf("Sending scancodes: %#v", codes)
|
||||
if err := driver.SendKeyScanCodes(vmName, codes...); err != nil {
|
||||
if err := driver.SendKeyScanCodes(s.VMName, codes...); err != nil {
|
||||
err := fmt.Errorf("Error sending boot command: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
|
@ -127,7 +134,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
|
||||
|
@ -156,6 +163,17 @@ func scancodes(message string) []string {
|
|||
special["<return>"] = []string{"1c", "9c"}
|
||||
special["<tab>"] = []string{"0f", "8f"}
|
||||
|
||||
special["<up>"] = []string{"48", "c8"}
|
||||
special["<down>"] = []string{"50", "d0"}
|
||||
special["<left>"] = []string{"4b", "cb"}
|
||||
special["<right>"] = []string{"4d", "cd"}
|
||||
special["<spacebar>"] = []string{"39", "b9"}
|
||||
special["<insert>"] = []string{"52", "d2"}
|
||||
special["<home>"] = []string{"47", "c7"}
|
||||
special["<end>"] = []string{"4f", "cf"}
|
||||
special["<pageUp>"] = []string{"49", "c9"}
|
||||
special["<pageDown>"] = []string{"51", "d1"}
|
||||
|
||||
shiftedChars := "!@#$%^&*()_+{}:\"~|<>?"
|
||||
|
||||
scancodeIndex := make(map[string]uint)
|
||||
|
@ -163,7 +181,7 @@ func scancodes(message string) []string {
|
|||
scancodeIndex["!@#$%^&*()_+"] = 0x02
|
||||
scancodeIndex["qwertyuiop[]"] = 0x10
|
||||
scancodeIndex["QWERTYUIOP{}"] = 0x10
|
||||
scancodeIndex["asdfghjkl;´`"] = 0x1e
|
||||
scancodeIndex["asdfghjkl;'`"] = 0x1e
|
||||
scancodeIndex[`ASDFGHJKL:"~`] = 0x1e
|
||||
scancodeIndex["\\zxcvbnm,./"] = 0x2b
|
||||
scancodeIndex["|ZXCVBNM<>?"] = 0x2b
|
|
@ -0,0 +1,77 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStepTypeBootCommand(t *testing.T) {
|
||||
state := testState(t)
|
||||
tpl, _ := packer.NewConfigTemplate()
|
||||
|
||||
var bootcommand = []string{
|
||||
"1234567890-=<enter><wait>",
|
||||
"!@#$%^&*()_+<enter>",
|
||||
"qwertyuiop[]<enter>",
|
||||
"QWERTYUIOP{}<enter>",
|
||||
"asdfghjkl;'`<enter>",
|
||||
`ASDFGHJKL:"~<enter>`,
|
||||
"\\zxcvbnm,./<enter>",
|
||||
"|ZXCVBNM<>?<enter>",
|
||||
" <enter>",
|
||||
}
|
||||
|
||||
step := StepTypeBootCommand{
|
||||
BootCommand: bootcommand,
|
||||
HostInterfaces: []string{},
|
||||
VMName: "myVM",
|
||||
Tpl: tpl,
|
||||
}
|
||||
|
||||
comm := new(packer.MockCommunicator)
|
||||
state.Put("communicator", comm)
|
||||
|
||||
driver := state.Get("driver").(*DriverMock)
|
||||
driver.VersionResult = "foo"
|
||||
state.Put("http_port", uint(0))
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
// Verify
|
||||
var expected = [][]string{
|
||||
[]string{"02", "82", "03", "83", "04", "84", "05", "85", "06", "86", "07", "87", "08", "88", "09", "89", "0a", "8a", "0b", "8b", "0c", "8c", "0d", "8d", "1c", "9c"},
|
||||
[]string{},
|
||||
[]string{"2a", "02", "82", "aa", "2a", "03", "83", "aa", "2a", "04", "84", "aa", "2a", "05", "85", "aa", "2a", "06", "86", "aa", "2a", "07", "87", "aa", "2a", "08", "88", "aa", "2a", "09", "89", "aa", "2a", "0a", "8a", "aa", "2a", "0b", "8b", "aa", "2a", "0c", "8c", "aa", "2a", "0d", "8d", "aa", "1c", "9c"},
|
||||
[]string{"10", "90", "11", "91", "12", "92", "13", "93", "14", "94", "15", "95", "16", "96", "17", "97", "18", "98", "19", "99", "1a", "9a", "1b", "9b", "1c", "9c"},
|
||||
[]string{"2a", "10", "90", "aa", "2a", "11", "91", "aa", "2a", "12", "92", "aa", "2a", "13", "93", "aa", "2a", "14", "94", "aa", "2a", "15", "95", "aa", "2a", "16", "96", "aa", "2a", "17", "97", "aa", "2a", "18", "98", "aa", "2a", "19", "99", "aa", "2a", "1a", "9a", "aa", "2a", "1b", "9b", "aa", "1c", "9c"},
|
||||
[]string{"1e", "9e", "1f", "9f", "20", "a0", "21", "a1", "22", "a2", "23", "a3", "24", "a4", "25", "a5", "26", "a6", "27", "a7", "28", "a8", "29", "a9", "1c", "9c"},
|
||||
[]string{"2a", "1e", "9e", "aa", "2a", "1f", "9f", "aa", "2a", "20", "a0", "aa", "2a", "21", "a1", "aa", "2a", "22", "a2", "aa", "2a", "23", "a3", "aa", "2a", "24", "a4", "aa", "2a", "25", "a5", "aa", "2a", "26", "a6", "aa", "2a", "27", "a7", "aa", "2a", "28", "a8", "aa", "2a", "29", "a9", "aa", "1c", "9c"},
|
||||
[]string{"2b", "ab", "2c", "ac", "2d", "ad", "2e", "ae", "2f", "af", "30", "b0", "31", "b1", "32", "b2", "33", "b3", "34", "b4", "35", "b5", "1c", "9c"},
|
||||
[]string{"2a", "2b", "ab", "aa", "2a", "2c", "ac", "aa", "2a", "2d", "ad", "aa", "2a", "2e", "ae", "aa", "2a", "2f", "af", "aa", "2a", "30", "b0", "aa", "2a", "31", "b1", "aa", "2a", "32", "b2", "aa", "2a", "33", "b3", "aa", "2a", "34", "b4", "aa", "2a", "35", "b5", "aa", "1c", "9c"},
|
||||
[]string{"39", "b9", "1c", "9c"},
|
||||
}
|
||||
fail := false
|
||||
|
||||
for i := range driver.SendKeyScanCodesCalls {
|
||||
t.Logf("prltype %s\n", strings.Join(driver.SendKeyScanCodesCalls[i], " "))
|
||||
}
|
||||
|
||||
for i := range expected {
|
||||
for j := range expected[i] {
|
||||
if driver.SendKeyScanCodesCalls[i][j] != expected[i][j] {
|
||||
fail = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if fail {
|
||||
t.Fatalf("Sent bad scancodes: %#v\n Expected: %#v", driver.SendKeyScanCodesCalls, expected)
|
||||
}
|
||||
}
|
|
@ -8,13 +8,21 @@ import (
|
|||
"os"
|
||||
)
|
||||
|
||||
// This step uploads the Parallels Tools ISO to the virtual machine.
|
||||
//
|
||||
// Uses:
|
||||
// communicator packer.Communicator
|
||||
// parallels_tools_path string
|
||||
// ui packer.Ui
|
||||
//
|
||||
// Produces:
|
||||
type toolsPathTemplate struct {
|
||||
Version string
|
||||
Flavor string
|
||||
}
|
||||
|
||||
// This step uploads the guest additions ISO to the VM.
|
||||
type StepUploadParallelsTools struct {
|
||||
ParallelsToolsHostPath string
|
||||
ParallelsToolsFlavor string
|
||||
ParallelsToolsGuestPath string
|
||||
ParallelsToolsMode string
|
||||
Tpl *packer.ConfigTemplate
|
||||
|
@ -22,7 +30,6 @@ type StepUploadParallelsTools struct {
|
|||
|
||||
func (s *StepUploadParallelsTools) Run(state multistep.StateBag) multistep.StepAction {
|
||||
comm := state.Get("communicator").(packer.Communicator)
|
||||
driver := state.Get("driver").(Driver)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
// If we're attaching then don't do this, since we attached.
|
||||
|
@ -31,20 +38,18 @@ func (s *StepUploadParallelsTools) Run(state multistep.StateBag) multistep.StepA
|
|||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
version, err := driver.Version()
|
||||
if err != nil {
|
||||
state.Put("error", fmt.Errorf("Error reading version for Parallels Tools upload: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
// Get the Paralells Tools path on the host machine
|
||||
parallelsToolsPath := state.Get("parallels_tools_path").(string)
|
||||
|
||||
f, err := os.Open(s.ParallelsToolsHostPath)
|
||||
f, err := os.Open(parallelsToolsPath)
|
||||
if err != nil {
|
||||
state.Put("error", fmt.Errorf("Error opening Parallels Tools ISO: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
tplData := &toolsPathTemplate{
|
||||
Version: version,
|
||||
Flavor: s.ParallelsToolsFlavor,
|
||||
}
|
||||
|
||||
s.ParallelsToolsGuestPath, err = s.Tpl.Process(s.ParallelsToolsGuestPath, tplData)
|
||||
|
@ -55,9 +60,12 @@ func (s *StepUploadParallelsTools) Run(state multistep.StateBag) multistep.StepA
|
|||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
ui.Say("Uploading Parallels Tools ISO...")
|
||||
if err := comm.Upload(s.ParallelsToolsGuestPath, f); err != nil {
|
||||
state.Put("error", fmt.Errorf("Error uploading Parallels Tools: %s", err))
|
||||
ui.Say(fmt.Sprintf("Uploading Parallels Tools for '%s' to path: '%s'",
|
||||
s.ParallelsToolsFlavor, s.ParallelsToolsGuestPath))
|
||||
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())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// These are the different valid mode values for "parallels_tools_mode" which
|
||||
// determine how guest additions are delivered to the guest.
|
||||
const (
|
||||
ParallelsToolsModeDisable string = "disable"
|
||||
ParallelsToolsModeAttach = "attach"
|
||||
ParallelsToolsModeUpload = "upload"
|
||||
)
|
||||
|
||||
type ToolsConfig struct {
|
||||
ParallelsToolsFlavor string `mapstructure:"parallels_tools_flavor"`
|
||||
ParallelsToolsGuestPath string `mapstructure:"parallels_tools_guest_path"`
|
||||
ParallelsToolsMode string `mapstructure:"parallels_tools_mode"`
|
||||
}
|
||||
|
||||
func (c *ToolsConfig) Prepare(t *packer.ConfigTemplate) []error {
|
||||
if c.ParallelsToolsMode == "" {
|
||||
c.ParallelsToolsMode = ParallelsToolsModeUpload
|
||||
}
|
||||
|
||||
if c.ParallelsToolsGuestPath == "" {
|
||||
c.ParallelsToolsGuestPath = "prl-tools-{{.Flavor}}.iso"
|
||||
}
|
||||
|
||||
templates := map[string]*string{
|
||||
"parallels_tools_flavor": &c.ParallelsToolsFlavor,
|
||||
"parallels_tools_mode": &c.ParallelsToolsMode,
|
||||
}
|
||||
|
||||
var err error
|
||||
errs := make([]error, 0)
|
||||
for n, ptr := range templates {
|
||||
*ptr, err = t.Process(*ptr, nil)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("Error processing %s: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := template.New("path").Parse(c.ParallelsToolsGuestPath); err != nil {
|
||||
errs = append(errs, fmt.Errorf("parallels_tools_guest_path invalid: %s", err))
|
||||
}
|
||||
|
||||
validMode := false
|
||||
validModes := []string{
|
||||
ParallelsToolsModeDisable,
|
||||
ParallelsToolsModeAttach,
|
||||
ParallelsToolsModeUpload,
|
||||
}
|
||||
|
||||
for _, mode := range validModes {
|
||||
if c.ParallelsToolsMode == mode {
|
||||
validMode = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !validMode {
|
||||
errs = append(errs,
|
||||
fmt.Errorf("parallels_tools_mode is invalid. Must be one of: %v",
|
||||
validModes))
|
||||
}
|
||||
|
||||
if c.ParallelsToolsFlavor == "" {
|
||||
if c.ParallelsToolsMode != ParallelsToolsModeDisable {
|
||||
errs = append(errs, errors.New("parallels_tools_flavor must be specified."))
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testToolsConfig() *ToolsConfig {
|
||||
return &ToolsConfig{
|
||||
ParallelsToolsFlavor: "foo",
|
||||
ParallelsToolsGuestPath: "foo",
|
||||
ParallelsToolsMode: "attach",
|
||||
}
|
||||
}
|
||||
|
||||
func TestToolsConfigPrepare(t *testing.T) {
|
||||
c := testToolsConfig()
|
||||
errs := c.Prepare(testConfigTemplate(t))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("bad err: %#v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToolsConfigPrepare_ParallelsToolsMode(t *testing.T) {
|
||||
var c *ToolsConfig
|
||||
var errs []error
|
||||
|
||||
// Test default mode
|
||||
c = testToolsConfig()
|
||||
c.ParallelsToolsMode = ""
|
||||
errs = c.Prepare(testConfigTemplate(t))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("should not have error: %#v", errs)
|
||||
}
|
||||
if c.ParallelsToolsMode != ParallelsToolsModeUpload {
|
||||
t.Errorf("bad parallels tools mode: %s", c.ParallelsToolsMode)
|
||||
}
|
||||
|
||||
// Test another mode
|
||||
c = testToolsConfig()
|
||||
c.ParallelsToolsMode = "attach"
|
||||
errs = c.Prepare(testConfigTemplate(t))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("should not have error: %#v", errs)
|
||||
}
|
||||
if c.ParallelsToolsMode != ParallelsToolsModeAttach {
|
||||
t.Fatalf("bad mode: %s", c.ParallelsToolsMode)
|
||||
}
|
||||
|
||||
// Test invalid mode
|
||||
c = testToolsConfig()
|
||||
c.ParallelsToolsMode = "invalid_mode"
|
||||
errs = c.Prepare(testConfigTemplate(t))
|
||||
if len(errs) == 0 {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestToolsConfigPrepare_ParallelsToolsGuestPath(t *testing.T) {
|
||||
var c *ToolsConfig
|
||||
var errs []error
|
||||
|
||||
// Test default path
|
||||
c = testToolsConfig()
|
||||
c.ParallelsToolsGuestPath = ""
|
||||
errs = c.Prepare(testConfigTemplate(t))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("should not have error: %#v", errs)
|
||||
}
|
||||
if c.ParallelsToolsGuestPath == "" {
|
||||
t.Fatal("should not be empty")
|
||||
}
|
||||
|
||||
// Test with a bad value
|
||||
c = testToolsConfig()
|
||||
c.ParallelsToolsGuestPath = "{{{nope}"
|
||||
errs = c.Prepare(testConfigTemplate(t))
|
||||
if len(errs) == 0 {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test with a good one
|
||||
c = testToolsConfig()
|
||||
c.ParallelsToolsGuestPath = "foo"
|
||||
errs = c.Prepare(testConfigTemplate(t))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("should not have error: %s", errs)
|
||||
}
|
||||
|
||||
if c.ParallelsToolsGuestPath != "foo" {
|
||||
t.Fatalf("bad guest path: %s", c.ParallelsToolsGuestPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToolsConfigPrepare_ParallelsToolsFlavor(t *testing.T) {
|
||||
var c *ToolsConfig
|
||||
var errs []error
|
||||
|
||||
// Test with a default value
|
||||
c = testToolsConfig()
|
||||
c.ParallelsToolsFlavor = ""
|
||||
errs = c.Prepare(testConfigTemplate(t))
|
||||
if len(errs) == 0 {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test with an bad value
|
||||
c = testToolsConfig()
|
||||
c.ParallelsToolsMode = "attach"
|
||||
c.ParallelsToolsFlavor = ""
|
||||
errs = c.Prepare(testConfigTemplate(t))
|
||||
if len(errs) == 0 {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test with a good one
|
||||
c = testToolsConfig()
|
||||
c.ParallelsToolsMode = "disable"
|
||||
c.ParallelsToolsFlavor = ""
|
||||
errs = c.Prepare(testConfigTemplate(t))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("should not have error: %s", errs)
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package common
|
||||
|
||||
// These are the different valid mode values for "parallels_tools_mode" which
|
||||
// determine how guest additions are delivered to the guest.
|
||||
const (
|
||||
ParallelsToolsModeDisable string = "disable"
|
||||
ParallelsToolsModeAttach = "attach"
|
||||
ParallelsToolsModeUpload = "upload"
|
||||
)
|
|
@ -22,30 +22,32 @@ type config struct {
|
|||
common.PackerConfig `mapstructure:",squash"`
|
||||
parallelscommon.FloppyConfig `mapstructure:",squash"`
|
||||
parallelscommon.OutputConfig `mapstructure:",squash"`
|
||||
parallelscommon.PrlctlConfig `mapstructure:",squash"`
|
||||
parallelscommon.PrlctlVersionConfig `mapstructure:",squash"`
|
||||
parallelscommon.RunConfig `mapstructure:",squash"`
|
||||
parallelscommon.ShutdownConfig `mapstructure:",squash"`
|
||||
parallelscommon.SSHConfig `mapstructure:",squash"`
|
||||
parallelscommon.PrlctlConfig `mapstructure:",squash"`
|
||||
parallelscommon.PrlctlVersionConfig `mapstructure:",squash"`
|
||||
parallelscommon.ToolsConfig `mapstructure:",squash"`
|
||||
|
||||
BootCommand []string `mapstructure:"boot_command"`
|
||||
DiskSize uint `mapstructure:"disk_size"`
|
||||
ParallelsToolsMode string `mapstructure:"parallels_tools_mode"`
|
||||
ParallelsToolsGuestPath string `mapstructure:"parallels_tools_guest_path"`
|
||||
ParallelsToolsHostPath string `mapstructure:"parallels_tools_host_path"`
|
||||
GuestOSType string `mapstructure:"guest_os_type"`
|
||||
GuestOSDistribution string `mapstructure:"guest_os_distribution"`
|
||||
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"`
|
||||
ISOUrls []string `mapstructure:"iso_urls"`
|
||||
VMName string `mapstructure:"vm_name"`
|
||||
BootCommand []string `mapstructure:"boot_command"`
|
||||
DiskSize uint `mapstructure:"disk_size"`
|
||||
GuestOSType string `mapstructure:"guest_os_type"`
|
||||
HardDriveInterface string `mapstructure:"hard_drive_interface"`
|
||||
HostInterfaces []string `mapstructure:"host_interfaces"`
|
||||
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"`
|
||||
ISOUrls []string `mapstructure:"iso_urls"`
|
||||
VMName string `mapstructure:"vm_name"`
|
||||
|
||||
RawSingleISOUrl string `mapstructure:"iso_url"`
|
||||
|
||||
// Deprecated parameters
|
||||
GuestOSDistribution string `mapstructure:"guest_os_distribution"`
|
||||
ParallelsToolsHostPath string `mapstructure:"parallels_tools_host_path"`
|
||||
|
||||
tpl *packer.ConfigTemplate
|
||||
}
|
||||
|
||||
|
@ -68,28 +70,17 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
errs = packer.MultiErrorAppend(
|
||||
errs, b.config.OutputConfig.Prepare(b.config.tpl, &b.config.PackerConfig)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare(b.config.tpl)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.ShutdownConfig.Prepare(b.config.tpl)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.SSHConfig.Prepare(b.config.tpl)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.PrlctlConfig.Prepare(b.config.tpl)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.PrlctlVersionConfig.Prepare(b.config.tpl)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.ShutdownConfig.Prepare(b.config.tpl)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.SSHConfig.Prepare(b.config.tpl)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.ToolsConfig.Prepare(b.config.tpl)...)
|
||||
warnings := make([]string, 0)
|
||||
|
||||
if b.config.DiskSize == 0 {
|
||||
b.config.DiskSize = 40000
|
||||
}
|
||||
|
||||
if b.config.ParallelsToolsMode == "" {
|
||||
b.config.ParallelsToolsMode = "upload"
|
||||
}
|
||||
|
||||
if b.config.ParallelsToolsGuestPath == "" {
|
||||
b.config.ParallelsToolsGuestPath = "prl-tools.iso"
|
||||
}
|
||||
|
||||
if b.config.ParallelsToolsHostPath == "" {
|
||||
b.config.ParallelsToolsHostPath = "/Applications/Parallels Desktop.app/Contents/Resources/Tools/prl-tools-other.iso"
|
||||
}
|
||||
|
||||
if b.config.HardDriveInterface == "" {
|
||||
b.config.HardDriveInterface = "sata"
|
||||
}
|
||||
|
@ -98,8 +89,14 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
b.config.GuestOSType = "other"
|
||||
}
|
||||
|
||||
if b.config.GuestOSDistribution == "" {
|
||||
b.config.GuestOSDistribution = "other"
|
||||
if b.config.GuestOSDistribution != "" {
|
||||
// Compatibility with older templates:
|
||||
// Use value of 'guest_os_distribution' if it is defined.
|
||||
b.config.GuestOSType = b.config.GuestOSDistribution
|
||||
warnings = append(warnings,
|
||||
"A 'guest_os_distribution' has been completely replaced with 'guest_os_type'\n"+
|
||||
"It is recommended to remove it and assign the previous value to 'guest_os_type'.\n"+
|
||||
"Run it to see all available values: `prlctl create x -d list` ")
|
||||
}
|
||||
|
||||
if b.config.HTTPPortMin == 0 {
|
||||
|
@ -110,23 +107,24 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
b.config.HTTPPortMax = 9000
|
||||
}
|
||||
|
||||
if len(b.config.HostInterfaces) == 0 {
|
||||
b.config.HostInterfaces = []string{"en0", "en1", "en2", "en3", "en4", "en5", "en6", "en7",
|
||||
"en8", "en9", "ppp0", "ppp1", "ppp2"}
|
||||
}
|
||||
|
||||
if b.config.VMName == "" {
|
||||
b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName)
|
||||
}
|
||||
|
||||
// Errors
|
||||
templates := map[string]*string{
|
||||
"parallels_tools_mode": &b.config.ParallelsToolsMode,
|
||||
"parallels_tools_host_path": &b.config.ParallelsToolsHostPath,
|
||||
"parallels_tools_guest_path": &b.config.ParallelsToolsGuestPath,
|
||||
"guest_os_type": &b.config.GuestOSType,
|
||||
"guest_os_distribution": &b.config.GuestOSDistribution,
|
||||
"hard_drive_interface": &b.config.HardDriveInterface,
|
||||
"http_directory": &b.config.HTTPDir,
|
||||
"iso_checksum": &b.config.ISOChecksum,
|
||||
"iso_checksum_type": &b.config.ISOChecksumType,
|
||||
"iso_url": &b.config.RawSingleISOUrl,
|
||||
"vm_name": &b.config.VMName,
|
||||
"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_url": &b.config.RawSingleISOUrl,
|
||||
"vm_name": &b.config.VMName,
|
||||
}
|
||||
|
||||
for n, ptr := range templates {
|
||||
|
@ -147,17 +145,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
validates := map[string]*string{
|
||||
"parallels_tools_guest_path": &b.config.ParallelsToolsGuestPath,
|
||||
}
|
||||
|
||||
for n, ptr := range validates {
|
||||
if err := b.config.tpl.Validate(*ptr); err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Error parsing %s: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
for i, command := range b.config.BootCommand {
|
||||
if err := b.config.tpl.Validate(command); err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
|
@ -214,25 +201,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
validMode := false
|
||||
validModes := []string{
|
||||
parallelscommon.ParallelsToolsModeDisable,
|
||||
parallelscommon.ParallelsToolsModeAttach,
|
||||
parallelscommon.ParallelsToolsModeUpload,
|
||||
}
|
||||
|
||||
for _, mode := range validModes {
|
||||
if b.config.ParallelsToolsMode == mode {
|
||||
validMode = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !validMode {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("parallels_tools_mode is invalid. Must be one of: %v", validModes))
|
||||
}
|
||||
|
||||
// Warnings
|
||||
if b.config.ISOChecksumType == "none" {
|
||||
warnings = append(warnings,
|
||||
|
@ -246,6 +214,12 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
|||
"will forcibly halt the virtual machine, which may result in data loss.")
|
||||
}
|
||||
|
||||
if b.config.ParallelsToolsHostPath != "" {
|
||||
warnings = append(warnings,
|
||||
"A 'parallels_tools_host_path' has been deprecated and not in use anymore\n"+
|
||||
"You can remove it from your Packer template.")
|
||||
}
|
||||
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
return warnings, errs
|
||||
}
|
||||
|
@ -261,6 +235,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
}
|
||||
|
||||
steps := []multistep.Step{
|
||||
¶llelscommon.StepPrepareParallelsTools{
|
||||
ParallelsToolsFlavor: b.config.ParallelsToolsFlavor,
|
||||
ParallelsToolsMode: b.config.ParallelsToolsMode,
|
||||
},
|
||||
&common.StepDownload{
|
||||
Checksum: b.config.ISOChecksum,
|
||||
ChecksumType: b.config.ISOChecksumType,
|
||||
|
@ -280,8 +258,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
new(stepCreateDisk),
|
||||
new(stepAttachISO),
|
||||
¶llelscommon.StepAttachParallelsTools{
|
||||
ParallelsToolsHostPath: b.config.ParallelsToolsHostPath,
|
||||
ParallelsToolsMode: b.config.ParallelsToolsMode,
|
||||
ParallelsToolsMode: b.config.ParallelsToolsMode,
|
||||
},
|
||||
new(parallelscommon.StepAttachFloppy),
|
||||
¶llelscommon.StepPrlctl{
|
||||
|
@ -292,7 +269,12 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
BootWait: b.config.BootWait,
|
||||
Headless: b.config.Headless, // TODO: migth work on Enterprise Ed.
|
||||
},
|
||||
new(stepTypeBootCommand),
|
||||
¶llelscommon.StepTypeBootCommand{
|
||||
BootCommand: b.config.BootCommand,
|
||||
HostInterfaces: b.config.HostInterfaces,
|
||||
VMName: b.config.VMName,
|
||||
Tpl: b.config.tpl,
|
||||
},
|
||||
&common.StepConnectSSH{
|
||||
SSHAddress: parallelscommon.SSHAddress,
|
||||
SSHConfig: parallelscommon.SSHConfigFunc(b.config.SSHConfig),
|
||||
|
@ -302,8 +284,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
Path: b.config.PrlctlVersionFile,
|
||||
},
|
||||
¶llelscommon.StepUploadParallelsTools{
|
||||
ParallelsToolsFlavor: b.config.ParallelsToolsFlavor,
|
||||
ParallelsToolsGuestPath: b.config.ParallelsToolsGuestPath,
|
||||
ParallelsToolsHostPath: b.config.ParallelsToolsHostPath,
|
||||
ParallelsToolsMode: b.config.ParallelsToolsMode,
|
||||
Tpl: b.config.tpl,
|
||||
},
|
||||
|
@ -312,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
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package iso
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/packer/builder/parallels/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
@ -9,11 +8,12 @@ import (
|
|||
|
||||
func testConfig() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"iso_checksum": "foo",
|
||||
"iso_checksum_type": "md5",
|
||||
"iso_url": "http://www.google.com/",
|
||||
"shutdown_command": "yes",
|
||||
"ssh_username": "foo",
|
||||
"iso_checksum": "foo",
|
||||
"iso_checksum_type": "md5",
|
||||
"iso_url": "http://www.google.com/",
|
||||
"shutdown_command": "yes",
|
||||
"ssh_username": "foo",
|
||||
"parallels_tools_flavor": "lin",
|
||||
|
||||
packer.BuildNameConfigKey: "foo",
|
||||
}
|
||||
|
@ -38,10 +38,6 @@ func TestBuilderPrepare_Defaults(t *testing.T) {
|
|||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.ParallelsToolsMode != common.ParallelsToolsModeUpload {
|
||||
t.Errorf("bad parallels tools mode: %s", b.config.ParallelsToolsMode)
|
||||
}
|
||||
|
||||
if b.config.GuestOSType != "other" {
|
||||
t.Errorf("bad guest OS type: %s", b.config.GuestOSType)
|
||||
}
|
||||
|
@ -83,104 +79,22 @@ func TestBuilderPrepare_DiskSize(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_ParallelsToolsMode(t *testing.T) {
|
||||
func TestBuilderPrepare_GuestOSType(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
delete(config, "guest_os_distribution")
|
||||
|
||||
// test default mode
|
||||
delete(config, "parallels_tools_mode")
|
||||
// Test deprecated parameter
|
||||
config["guest_os_distribution"] = "bolgenos"
|
||||
warns, err := b.Prepare(config)
|
||||
if len(warns) > 0 {
|
||||
t.Fatalf("bad: %#v", warns)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("bad err: %s", err)
|
||||
}
|
||||
|
||||
// Test another mode
|
||||
config["parallels_tools_mode"] = "attach"
|
||||
b = Builder{}
|
||||
warns, err = b.Prepare(config)
|
||||
if len(warns) > 0 {
|
||||
t.Fatalf("bad: %#v", warns)
|
||||
if len(warns) == 0 {
|
||||
t.Fatalf("should have warning")
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.ParallelsToolsMode != common.ParallelsToolsModeAttach {
|
||||
t.Fatalf("bad: %s", b.config.ParallelsToolsMode)
|
||||
}
|
||||
|
||||
// Test bad mode
|
||||
config["parllels_tools_mode"] = "teleport"
|
||||
b = Builder{}
|
||||
warns, err = b.Prepare(config)
|
||||
if len(warns) > 0 {
|
||||
t.Fatalf("bad: %#v", warns)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("should error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_ParallelsToolsGuestPath(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
delete(config, "parallesl_tools_guest_path")
|
||||
warns, err := b.Prepare(config)
|
||||
if len(warns) > 0 {
|
||||
t.Fatalf("bad: %#v", warns)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("bad err: %s", err)
|
||||
}
|
||||
|
||||
if b.config.ParallelsToolsGuestPath != "prl-tools.iso" {
|
||||
t.Fatalf("bad: %s", b.config.ParallelsToolsGuestPath)
|
||||
}
|
||||
|
||||
config["parallels_tools_guest_path"] = "foo"
|
||||
b = Builder{}
|
||||
warns, err = b.Prepare(config)
|
||||
if len(warns) > 0 {
|
||||
t.Fatalf("bad: %#v", warns)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.ParallelsToolsGuestPath != "foo" {
|
||||
t.Fatalf("bad size: %s", b.config.ParallelsToolsGuestPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_ParallelsToolsHostPath(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
config["parallels_tools_host_path"] = ""
|
||||
warns, err := b.Prepare(config)
|
||||
if len(warns) > 0 {
|
||||
t.Fatalf("bad: %#v", warns)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if b.config.ParallelsToolsHostPath != "/Applications/Parallels Desktop.app/Contents/Resources/Tools/prl-tools-other.iso" {
|
||||
t.Fatalf("bad: %s", b.config.ParallelsToolsHostPath)
|
||||
}
|
||||
|
||||
config["parallels_tools_host_path"] = "./prl-tools-lin.iso"
|
||||
b = Builder{}
|
||||
warns, err = b.Prepare(config)
|
||||
if len(warns) > 0 {
|
||||
t.Fatalf("bad: %#v", warns)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("should not have error: %s", err)
|
||||
if b.config.GuestOSType != "bolgenos" {
|
||||
t.Fatalf("bad: %s", b.config.GuestOSType)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -434,3 +348,19 @@ func TestBuilderPrepare_ISOUrl(t *testing.T) {
|
|||
t.Fatalf("bad: %#v", b.config.ISOUrls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_ParallelsToolsHostPath(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
delete(config, "parallels_tools_host_path")
|
||||
|
||||
// Test that it is deprecated
|
||||
config["parallels_tools_host_path"] = "/path/to/iso"
|
||||
warns, err := b.Prepare(config)
|
||||
if len(warns) == 0 {
|
||||
t.Fatalf("should have warning")
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue