Commit 331003c8 authored by Nathan Hartwell's avatar Nathan Hartwell

Merge branch 'master' of https://github.com/mitchellh/packer

parents 0c34c7a2 2aa5a198
......@@ -6,5 +6,4 @@
/website/build
.DS_Store
.vagrant
Vagrantfile
test/.env
......@@ -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
## 0.6.1 (unreleased)
## 0.7.2 (unreleased)
FEATURES:
* 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/openstack: skip certificate verification [GH-1121]
* 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.
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"
updatedeps:
@$(ECHO) "$(OK_COLOR)==> Updating all dependencies$(NO_COLOR)"
@go get -d -v -u ./...
@echo $(DEPS) | xargs -n1 go get -d -u
dev:
@TF_DEV=1 sh -c "$(CURDIR)/scripts/build.sh"
clean:
@rm -rf bin/ local/ pkg/ src/ website/.sass-cache website/build
test:
go test $(TEST) $(TESTARGS) -timeout=10s
format:
go fmt ./...
testrace:
go test -race $(TEST) $(TESTARGS)
test: deps
@$(ECHO) "$(OK_COLOR)==> Testing Packer...$(NO_COLOR)"
go test ./...
updatedeps:
go get -u -v -p 2 ./...
.PHONY: all clean deps format test updatedeps
.PHONY: bin default test updatedeps
......@@ -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!
$ make updatedeps
...
$ make
...
If you need to cross-compile Packer for other platforms, take a look at
`scripts/dist.sh`.
To compile a development version of Packer and the built-in plugins,
run `make dev`. This will put Packer binaries in the `bin` folder:
You can run tests by typing `make test`.
$ make dev
...
$ bin/packer
...
This will run tests for Packer core along with all the core builders and commands and such that come with Packer.
If you make any changes to the code, run `make format` in order to automatically
format the code according to Go standards.
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.
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.
$ make test TEST=./packer
...
# -*- 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
}
}
......
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
}
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"))
}
......
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,28 +74,136 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi
return multistep.ActionHalt
}
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
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)
}
s.instance = &runResp.Instances[0]
ui.Message(fmt.Sprintf("Instance ID: %s", s.instance.InstanceId))
var instanceId string
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})
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
}
_, err = ec2conn.CreateTags([]string{s.instance.InstanceId}, ec2Tags)
instanceResp, err := ec2conn.Instances([]string{instanceId}, nil)
if err != nil {
ui.Message(
fmt.Sprintf("Failed to tag a Name on the builder 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 = &instanceResp.Reservations[0].Instances[0]
ui.Message(fmt.Sprintf("Instance ID: %s", s.instance.InstanceId))
ui.Say(fmt.Sprintf("Waiting for instance (%s) to become ready...", s.instance.InstanceId))
stateChange := StateChangeConf{
......@@ -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",
}
WaitForState(&stateChange)
// Terminate the source instance if it exists
if s.instance != nil {
ui.Say("Terminating the source AWS instance...")
if _, err := ec2conn.TerminateInstances([]string{s.instance.InstanceId}); err != nil {
ui.Error(fmt.Sprintf("Error terminating instance, may still be around: %s", err))
return
}
stateChange := StateChangeConf{
Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"},
Refresh: InstanceStateRefreshFunc(ec2conn, s.instance),
Target: "terminated",
}
WaitForState(&stateChange)
}
}
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,
......
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
}
}
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))
......
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
......
package docker
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
// StepCommit commits the container to a image.
type StepCommit struct {
imageId string
}
func (s *StepCommit) Run(state multistep.StateBag) multistep.StepAction {
driver := state.Get("driver").(Driver)
containerId := state.Get("container_id").(string)
ui := state.Get("ui").(packer.Ui)
ui.Say("Committing the container")
imageId, err := driver.Commit(containerId)
if err != nil {
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Save the container ID
s.imageId = imageId
state.Put("image_id", s.imageId)
ui.Message(fmt.Sprintf("Image ID: %s", s.imageId))
return multistep.ActionContinue
}
func (s *StepCommit) Cleanup(state multistep.StateBag) {}
package docker
import (
"errors"
"github.com/mitchellh/multistep"
"testing"
)
func testStepCommitState(t *testing.T) multistep.StateBag {
state := testState(t)
state.Put("container_id", "foo")
return state
}
func TestStepCommit_impl(t *testing.T) {
var _ multistep.Step = new(StepCommit)
}
func TestStepCommit(t *testing.T) {
state := testStepCommitState(t)
step := new(StepCommit)
defer step.Cleanup(state)
driver := state.Get("driver").(*MockDriver)
driver.CommitImageId = "bar"
// run the step
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
// verify we did the right thing
if !driver.CommitCalled {
t.Fatal("should've called")
}
// verify the ID is saved
idRaw, ok := state.GetOk("image_id")
if !ok {
t.Fatal("should've saved ID")
}
id := idRaw.(string)
if id != driver.CommitImageId {
t.Fatalf("bad: %#v", id)
}
}
func TestStepCommit_error(t *testing.T) {
state := testStepCommitState(t)
step := new(StepCommit)
defer step.Cleanup(state)
driver := state.Get("driver").(*MockDriver)
driver.CommitErr = errors.New("foo")
// run the step
if action := step.Run(state); action != multistep.ActionHalt {
t.Fatalf("bad action: %#v", action)
}
// verify the ID is not saved
if _, ok := state.GetOk("image_id"); ok {
t.Fatal("shouldn't save image ID")
}
}
......@@ -12,6 +12,7 @@ type StepExport struct{}
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 {
......
......@@ -2,11 +2,19 @@ package googlecompute
import (
"encoding/json"
"io/ioutil"
"os"
)
// clientSecrets represents the client secrets of a GCE service account.
type clientSecrets struct {
// 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"`
......@@ -15,18 +23,13 @@ type clientSecrets struct {
}
}
// 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)
func loadJSON(result interface{}, path string) error {
f, err := os.Open(path)
if err != nil {
return nil, err
return err
}
defer f.Close()
return cs, nil
dec := json.NewDecoder(f)
return dec.Decode(result)
}
......@@ -35,7 +35,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
// 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),
......
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"`
clientSecrets *clientSecrets
AccountFile string `mapstructure:"account_file"`
ClientSecretsFile string `mapstructure:"client_secrets_file"`
ProjectId string `mapstructure:"project_id"`
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,14 +138,14 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
errs, errors.New("a bucket_name must be specified"))
}
if c.ClientSecretsFile == "" {
if c.AccountFile == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("a client_secrets_file must be specified"))
errs, errors.New("an account_file must be specified"))
}
if c.PrivateKeyFile == "" {
if c.ClientSecretsFile == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("a private_key_file must be specified"))
errs, errors.New("a client_secrets_file must be specified"))
}
if c.ProjectId == "" {
......@@ -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))
}
}
......
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)
......
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,
},
}
for v, d := range drivers {
version, _ := d.Version()
if strings.HasPrefix(version, v) {
return d, nil
}
supportedVersions = append(supportedVersions, v)
}
return driver, nil
return nil, fmt.Errorf(
"Unable to initialize any driver. Supported Parallels Desktop versions: "+
"%s\n", strings.Join(supportedVersions, ", "))
}
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
}
package iso
package common
// Interface to help find the host IP that is available from within
// the Parallels virtual machines.
......
package common
const Prltype string = `
import sys
import prlsdkapi
##
def main():
if len(sys.argv) < 3:
print "Usage: prltype VM_NAME SCANCODE..."
sys.exit(1)
vm_name = sys.argv[1]
scancodes = sys.argv[2:]
server = login()
vm, vm_io = connect(server, vm_name)
send(scancodes, vm, vm_io)
disconnect(server, vm, vm_io)
##
def login():
prlsdkapi.prlsdk.InitializeSDK(prlsdkapi.prlsdk.consts.PAM_DESKTOP_MAC)
server = prlsdkapi.Server()
login_job=server.login_local()
login_job.wait()
return server
##
def connect(server, vm_name):
vm_list_job = server.get_vm_list()
result = vm_list_job.wait()
vm_list = [result.get_param_by_index(i) for i in range(result.get_params_count())]
vm = [vm for vm in vm_list if vm.get_name() == vm_name]
if not vm:
vm_names = [vm.get_name() for vm in vm_list]
raise Exception("%s: No such VM. Available VM's are:\n%s" % (vm_name, "\n".join(vm_names)))
vm = vm[0]
vm_io = prlsdkapi.VmIO()
vm_io.connect_to_vm(vm).wait()
return (vm, vm_io)
##
def disconnect(server, vm, vm_io):
if vm and vm_io:
vm_io.disconnect_from_vm(vm)
if server:
server.logoff()
prlsdkapi.deinit_sdk
##
def send(scancodes, vm, vm_io):
timeout = 10
consts = prlsdkapi.prlsdk.consts
for scancode in scancodes:
c = int(scancode, 16)
if (c < 128):
vm_io.send_key_event(vm, (c,), consts.PKE_PRESS, timeout)
else:
vm_io.send_key_event(vm, (c - 128,) , consts.PKE_RELEASE, timeout)
##
if __name__ == "__main__":
main()
`
package common
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))
}
}
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) {}
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")
}
}
package common
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
// This step removes any devices (floppy disks, ISOs, etc.) from the
// machine that we may have added.
//
// Uses:
// driver Driver
// ui packer.Ui
// vmName string
//
// Produces:
type StepRemoveDevices struct{}
func (s *StepRemoveDevices) Run(state multistep.StateBag) multistep.StepAction {
driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui)
vmName := state.Get("vmName").(string)
// Remove the attached floppy disk, if it exists
if _, ok := state.GetOk("floppy_path"); ok {
ui.Message("Removing floppy drive...")
command := []string{"set", vmName, "--device-del", "fdd0"}
if err := driver.Prlctl(command...); err != nil {
err := fmt.Errorf("Error removing floppy: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
if _, ok := state.GetOk("attachedIso"); ok {
command := []string{
"set", vmName,
"--device-set", "cdrom0",
"--device", "Default CD/DVD-ROM",
}
if err := driver.Prlctl(command...); err != nil {
err := fmt.Errorf("Error detaching ISO: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
if _, ok := state.GetOk("attachedToolsIso"); ok {
command := []string{"set", vmName, "--device-del", "cdrom1"}
if err := driver.Prlctl(command...); err != nil {
err := fmt.Errorf("Error detaching ISO: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
return multistep.ActionContinue
}
func (s *StepRemoveDevices) Cleanup(state multistep.StateBag) {
}
package 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)
// Determine the host IP
ipFinder := &IfconfigIPFinder{Devices: []string{"en0", "en1", "en2", "en3", "en4", "en5", "en6", "en7", "en8", "en9"}}
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
driver := state.Get("driver").(Driver)
hostIp := "0.0.0.0"
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
......
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
}
......
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
}
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)
}
}
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"
)
This diff is collapsed.
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) {
var b Builder
config := testConfig()
// test default mode
delete(config, "parallels_tools_mode")
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 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) {
func TestBuilderPrepare_GuestOSType(t *testing.T) {
var b Builder
config := testConfig()
delete(config, "guest_os_distribution")
delete(config, "parallesl_tools_guest_path")
// 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)
}
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 len(warns) == 0 {
t.Fatalf("should have warning")
}
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)
}
}
This diff is collapsed.
......@@ -20,7 +20,7 @@ func (s *stepCreateDisk) Run(state multistep.StateBag) multistep.StepAction {
command := []string{
"set", vmName,
"--device-set", "hdd0",
"--device-add", "hdd",
"--size", strconv.FormatUint(uint64(config.DiskSize), 10),
"--iface", config.HardDriveInterface,
}
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment