From 94c0fac5adcc55d71487743a7953ec46d3b3b81e Mon Sep 17 00:00:00 2001 From: Geoffrey Bachelet Date: Wed, 3 Jul 2013 19:11:37 -0400 Subject: [PATCH 01/55] Typo --- builder/virtualbox/step_run.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builder/virtualbox/step_run.go b/builder/virtualbox/step_run.go index 1fa01d0ff..df978bdb6 100644 --- a/builder/virtualbox/step_run.go +++ b/builder/virtualbox/step_run.go @@ -27,7 +27,7 @@ func (s *stepRun) Run(state map[string]interface{}) multistep.StepAction { if config.Headless == true { ui.Message("WARNING: The VM will be started in headless mode, as configured.\n" + "In headless mode, errors during the boot sequence or OS setup\n" + - "won't be easily visible. Use at your own discresion.") + "won't be easily visible. Use at your own discretion.") guiArgument = "headless" } command := []string{"startvm", vmName, "--type", guiArgument} From e8d31bc21a6fb0c0415fa62e131217a29cb9703d Mon Sep 17 00:00:00 2001 From: Geoffrey Bachelet Date: Wed, 3 Jul 2013 20:07:25 -0400 Subject: [PATCH 02/55] provisioner/shell: execute script directly instead of using /bin/sh --- provisioner/shell/provisioner.go | 2 +- website/source/docs/provisioners/shell.html.markdown | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/provisioner/shell/provisioner.go b/provisioner/shell/provisioner.go index 1f89bfa79..ee3b2b178 100644 --- a/provisioner/shell/provisioner.go +++ b/provisioner/shell/provisioner.go @@ -62,7 +62,7 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { } if p.config.ExecuteCommand == "" { - p.config.ExecuteCommand = "{{.Vars}} sh {{.Path}}" + p.config.ExecuteCommand = "chmod +x {{.Path}}; {{.Vars}} {{.Path}}" } if p.config.Inline != nil && len(p.config.Inline) == 0 { diff --git a/website/source/docs/provisioners/shell.html.markdown b/website/source/docs/provisioners/shell.html.markdown index 9dc0d64c0..d99299689 100644 --- a/website/source/docs/provisioners/shell.html.markdown +++ b/website/source/docs/provisioners/shell.html.markdown @@ -51,7 +51,7 @@ Optional parameters: `key=value`. * `execute_command` (string) - The command to use to execute the script. - By default this is `{{ .Vars }} sh {{ .Path }}`. The value of this is + By default this is `chmod +x {{ .Path }}; {{ .Vars }} {{ .Path }}`. The value of this is treated as [configuration template](/docs/templates/configuration- templates.html). There are two available variables: `Path`, which is the path to the script to run, and `Vars`, which is the list of From c259f30e58ccf18da67ea57618294c75626d851f Mon Sep 17 00:00:00 2001 From: Eric Lathrop Date: Tue, 2 Jul 2013 22:11:30 -0400 Subject: [PATCH 03/55] Implement file upload provisioner per #118. --- config.go | 1 + plugin/provisioner-file/main.go | 10 +++ provisioner/file/provisioner.go | 53 +++++++++++ provisioner/file/provisioner_test.go | 128 +++++++++++++++++++++++++++ 4 files changed, 192 insertions(+) create mode 100644 plugin/provisioner-file/main.go create mode 100644 provisioner/file/provisioner.go create mode 100644 provisioner/file/provisioner_test.go diff --git a/config.go b/config.go index f2ed48956..7bfaabc76 100644 --- a/config.go +++ b/config.go @@ -35,6 +35,7 @@ const defaultConfig = ` }, "provisioners": { + "file": "packer-provisioner-file", "shell": "packer-provisioner-shell" } } diff --git a/plugin/provisioner-file/main.go b/plugin/provisioner-file/main.go new file mode 100644 index 000000000..1f5a63413 --- /dev/null +++ b/plugin/provisioner-file/main.go @@ -0,0 +1,10 @@ +package main + +import ( + "github.com/mitchellh/packer/packer/plugin" + "github.com/mitchellh/packer/provisioner/file" +) + +func main() { + plugin.ServeProvisioner(new(file.Provisioner)) +} diff --git a/provisioner/file/provisioner.go b/provisioner/file/provisioner.go new file mode 100644 index 000000000..711beffa6 --- /dev/null +++ b/provisioner/file/provisioner.go @@ -0,0 +1,53 @@ +package file + +import ( + "errors" + "fmt" + "github.com/mitchellh/mapstructure" + "github.com/mitchellh/packer/packer" + "os" +) + +type config struct { + // The local path of the file to upload. + Source string + + // The remote path where the local file will be uploaded to. + Destination string +} + +type Provisioner struct { + config config +} + +func (p *Provisioner) Prepare(raws ...interface{}) error { + for _, raw := range raws { + if err := mapstructure.Decode(raw, &p.config); err != nil { + return err + } + } + + errs := []error{} + + if _, err := os.Stat(p.config.Source); err != nil { + errs = append(errs, fmt.Errorf("Bad source file '%s': %s", p.config.Source, err)) + } + + if len(p.config.Destination) == 0 { + errs = append(errs, errors.New("Destination must be specified.")) + } + + if len(errs) > 0 { + return &packer.MultiError{errs} + } + return nil +} + +func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { + ui.Say(fmt.Sprintf("Uploading %s => %s", p.config.Source, p.config.Destination)) + f, err := os.Open(p.config.Source) + if err != nil { + return err + } + return comm.Upload(p.config.Destination, f) +} diff --git a/provisioner/file/provisioner_test.go b/provisioner/file/provisioner_test.go new file mode 100644 index 000000000..017bd3365 --- /dev/null +++ b/provisioner/file/provisioner_test.go @@ -0,0 +1,128 @@ +package file + +import ( + "github.com/mitchellh/packer/packer" + "io" + "io/ioutil" + "os" + "strings" + "testing" +) + +func TestProvisioner_Impl(t *testing.T) { + var raw interface{} + raw = &Provisioner{} + if _, ok := raw.(packer.Provisioner); !ok { + t.Fatalf("must be a provisioner") + } +} + +func TestProvisionerPrepare_InvalidSource(t *testing.T) { + var p Provisioner + config := map[string]interface{}{"source": "/this/should/not/exist", "destination": "something"} + + err := p.Prepare(config) + + if err == nil { + t.Fatalf("should require existing file") + } +} + +func TestProvisionerPrepare_ValidSource(t *testing.T) { + var p Provisioner + + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("error tempfile: %s", err) + } + defer os.Remove(tf.Name()) + config := map[string]interface{}{"source": tf.Name(), "destination": "something"} + + err = p.Prepare(config) + + if err != nil { + t.Fatalf("should allow valid file: %s", err) + } +} + +func TestProvisionerPrepare_EmptyDestination(t *testing.T) { + var p Provisioner + config := map[string]interface{}{"source": "/this/exists"} + + err := p.Prepare(config) + + if err == nil { + t.Fatalf("should require destination path") + } +} + +type stubUploadCommunicator struct { + dest string + data io.Reader +} + +func (suc *stubUploadCommunicator) Download(src string, data io.Writer) error { + return nil +} + +func (suc *stubUploadCommunicator) Upload(dest string, data io.Reader) error { + suc.dest = dest + suc.data = data + return nil +} + +func (suc *stubUploadCommunicator) Start(cmd *packer.RemoteCmd) error { + return nil +} + +type stubUi struct { + sayMessages string +} + +func (su *stubUi) Ask(string) (string, error) { + return "", nil +} + +func (su *stubUi) Error(string) { +} + +func (su *stubUi) Message(string) { +} + +func (su *stubUi) Say(msg string) { + su.sayMessages += msg +} + +func TestProvisionerProvision_SendsFile(t *testing.T) { + var p Provisioner + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("error tempfile: %s", err) + } + defer os.Remove(tf.Name()) + if _, err = tf.Write([]byte("hello")); err != nil { + t.Fatalf("error writing tempfile: %s", err) + } + config := map[string]interface{}{"source": tf.Name(), "destination": "something"} + p.Prepare(config) + + ui := &stubUi{} + comm := &stubUploadCommunicator{} + err = p.Provision(ui, comm) + if err != nil { + t.Fatalf("should successfully provision: %s", err) + } + if !strings.Contains(ui.sayMessages, tf.Name()) { + t.Fatalf("should print source filename") + } + if !strings.Contains(ui.sayMessages, "something") { + t.Fatalf("should print destination filename") + } + if comm.dest != "something" { + t.Fatalf("should upload to configured destination") + } + read, err := ioutil.ReadAll(comm.data) + if err != nil || string(read) != "hello" { + t.Fatalf("should upload with source file's data") + } +} From 05c7d08bb219e3a583a0dd479c708fba95e2a139 Mon Sep 17 00:00:00 2001 From: Eric Lathrop Date: Thu, 4 Jul 2013 15:07:53 -0400 Subject: [PATCH 04/55] Add file provisioner documentation. --- .../docs/provisioners/file.html.markdown | 31 +++++++++++++++++++ .../docs/provisioners/shell.html.markdown | 2 +- website/source/layouts/docs.erb | 1 + 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 website/source/docs/provisioners/file.html.markdown diff --git a/website/source/docs/provisioners/file.html.markdown b/website/source/docs/provisioners/file.html.markdown new file mode 100644 index 000000000..30d082f0c --- /dev/null +++ b/website/source/docs/provisioners/file.html.markdown @@ -0,0 +1,31 @@ +--- +layout: "docs" +--- + +# File Provisioner + +Type: `file` + +The file provisioner uploads files to machines build by Packer. + +## Basic Example + +
+{
+  "type": "file",
+  "source": "app.tar.gz",
+  "destination": "/tmp/app.tar.gz"
+}
+
+ +## Configuration Reference + +The available configuration options are listed below. All elements are required. + +* `source` (string) - The path to a local file to upload to the machine. The + path can be absolute or relative. If it is relative, it is relative to the + working directory when ?Packer is executed. + +* `destination` (string) - The path where the file will be uploaded to in the + machine. This value must be a writable location and any parent directories + must already exist. diff --git a/website/source/docs/provisioners/shell.html.markdown b/website/source/docs/provisioners/shell.html.markdown index 9dc0d64c0..51ddce502 100644 --- a/website/source/docs/provisioners/shell.html.markdown +++ b/website/source/docs/provisioners/shell.html.markdown @@ -23,7 +23,7 @@ The example below is fully functional. ## Configuration Reference -The reference of available configuratin options is listed below. The only +The reference of available configuration options is listed below. The only required element is either "inline" or "script". Every other option is optional. Exactly _one_ of the following is required: diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 2582862e9..a6ed7f2cd 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -36,6 +36,7 @@ From 7f9c755a425837f6ee90397db747c805d43c1521 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 4 Jul 2013 12:44:36 -0700 Subject: [PATCH 05/55] fmt --- communicator/ssh/password_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/communicator/ssh/password_test.go b/communicator/ssh/password_test.go index bed2d1cb1..e55779e82 100644 --- a/communicator/ssh/password_test.go +++ b/communicator/ssh/password_test.go @@ -45,4 +45,3 @@ func TestPasswordKeybardInteractive_Challenge(t *testing.T) { t.Fatalf("invalid password: %#v", result) } } - From 4f2344f48bdaf8ffb2e5401cb3721b495effe52b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 4 Jul 2013 12:50:00 -0700 Subject: [PATCH 06/55] provisioner/file: style nitpicks /cc @ericlathrop --- provisioner/file/provisioner.go | 8 +++-- provisioner/file/provisioner_test.go | 45 ++++++++++++++++++++-------- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/provisioner/file/provisioner.go b/provisioner/file/provisioner.go index 711beffa6..2e7c17190 100644 --- a/provisioner/file/provisioner.go +++ b/provisioner/file/provisioner.go @@ -30,16 +30,18 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { errs := []error{} if _, err := os.Stat(p.config.Source); err != nil { - errs = append(errs, fmt.Errorf("Bad source file '%s': %s", p.config.Source, err)) + errs = append(errs, + fmt.Errorf("Bad source '%s': %s", p.config.Source, err)) } - if len(p.config.Destination) == 0 { + if p.config.Destination == "" { errs = append(errs, errors.New("Destination must be specified.")) } if len(errs) > 0 { return &packer.MultiError{errs} } + return nil } @@ -49,5 +51,7 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { if err != nil { return err } + defer f.Close() + return comm.Upload(p.config.Destination, f) } diff --git a/provisioner/file/provisioner_test.go b/provisioner/file/provisioner_test.go index 017bd3365..6fa1e031a 100644 --- a/provisioner/file/provisioner_test.go +++ b/provisioner/file/provisioner_test.go @@ -9,6 +9,12 @@ import ( "testing" ) +func testConfig() map[string]interface{} { + return map[string]interface{}{ + "destination": "something", + } +} + func TestProvisioner_Impl(t *testing.T) { var raw interface{} raw = &Provisioner{} @@ -19,10 +25,10 @@ func TestProvisioner_Impl(t *testing.T) { func TestProvisionerPrepare_InvalidSource(t *testing.T) { var p Provisioner - config := map[string]interface{}{"source": "/this/should/not/exist", "destination": "something"} + config := testConfig() + config["source"] = "/this/should/not/exist" err := p.Prepare(config) - if err == nil { t.Fatalf("should require existing file") } @@ -36,10 +42,11 @@ func TestProvisionerPrepare_ValidSource(t *testing.T) { t.Fatalf("error tempfile: %s", err) } defer os.Remove(tf.Name()) - config := map[string]interface{}{"source": tf.Name(), "destination": "something"} + + config := testConfig() + config["source"] = tf.Name() err = p.Prepare(config) - if err != nil { t.Fatalf("should allow valid file: %s", err) } @@ -47,10 +54,10 @@ func TestProvisionerPrepare_ValidSource(t *testing.T) { func TestProvisionerPrepare_EmptyDestination(t *testing.T) { var p Provisioner - config := map[string]interface{}{"source": "/this/exists"} + config := testConfig() + delete(config, "destination") err := p.Prepare(config) - if err == nil { t.Fatalf("should require destination path") } @@ -58,7 +65,7 @@ func TestProvisionerPrepare_EmptyDestination(t *testing.T) { type stubUploadCommunicator struct { dest string - data io.Reader + data []byte } func (suc *stubUploadCommunicator) Download(src string, data io.Writer) error { @@ -66,9 +73,10 @@ func (suc *stubUploadCommunicator) Download(src string, data io.Writer) error { } func (suc *stubUploadCommunicator) Upload(dest string, data io.Reader) error { + var err error suc.dest = dest - suc.data = data - return nil + suc.data, err = ioutil.ReadAll(data) + return err } func (suc *stubUploadCommunicator) Start(cmd *packer.RemoteCmd) error { @@ -100,11 +108,19 @@ func TestProvisionerProvision_SendsFile(t *testing.T) { t.Fatalf("error tempfile: %s", err) } defer os.Remove(tf.Name()) + if _, err = tf.Write([]byte("hello")); err != nil { t.Fatalf("error writing tempfile: %s", err) } - config := map[string]interface{}{"source": tf.Name(), "destination": "something"} - p.Prepare(config) + + config := map[string]interface{}{ + "source": tf.Name(), + "destination": "something", + } + + if err := p.Prepare(config); err != nil { + t.Fatalf("err: %s", err) + } ui := &stubUi{} comm := &stubUploadCommunicator{} @@ -112,17 +128,20 @@ func TestProvisionerProvision_SendsFile(t *testing.T) { if err != nil { t.Fatalf("should successfully provision: %s", err) } + if !strings.Contains(ui.sayMessages, tf.Name()) { t.Fatalf("should print source filename") } + if !strings.Contains(ui.sayMessages, "something") { t.Fatalf("should print destination filename") } + if comm.dest != "something" { t.Fatalf("should upload to configured destination") } - read, err := ioutil.ReadAll(comm.data) - if err != nil || string(read) != "hello" { + + if string(comm.data) != "hello" { t.Fatalf("should upload with source file's data") } } From 49dc2c4cba50ff2ec0137696b459474a901115f3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 4 Jul 2013 12:50:49 -0700 Subject: [PATCH 07/55] Update CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1679beec4..932f346e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ ## 0.1.5 (unreleased) +FEATURES: +* "file" uploader will upload files and directories from the machine + running Packer to the remote machine. ## 0.1.4 (July 2, 2013) From f1c8f0a8cd4736478adcc22b6534052b99f6f55a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 4 Jul 2013 12:58:11 -0700 Subject: [PATCH 08/55] website: add more info to the file provisioner page --- website/source/docs/provisioners/file.html.markdown | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/website/source/docs/provisioners/file.html.markdown b/website/source/docs/provisioners/file.html.markdown index 30d082f0c..191b89ee8 100644 --- a/website/source/docs/provisioners/file.html.markdown +++ b/website/source/docs/provisioners/file.html.markdown @@ -6,7 +6,10 @@ layout: "docs" Type: `file` -The file provisioner uploads files to machines build by Packer. +The file provisioner uploads files to machines built by Packer. The +recommended usage of the file provisioner is to use it to upload files, +and then use [shell provisioner](/docs/provisioners/shell.html) to move +them to the proper place, set permissions, etc. ## Basic Example @@ -23,9 +26,9 @@ The file provisioner uploads files to machines build by Packer. The available configuration options are listed below. All elements are required. * `source` (string) - The path to a local file to upload to the machine. The - path can be absolute or relative. If it is relative, it is relative to the - working directory when ?Packer is executed. + path can be absolute or relative. If it is relative, it is relative to the + working directory when Packer is executed. -* `destination` (string) - The path where the file will be uploaded to in the - machine. This value must be a writable location and any parent directories +* `destination` (string) - The path where the file will be uploaded to in the + machine. This value must be a writable location and any parent directories must already exist. From cf191350cd3b17ec60e694f9c1e092dc0e8bc446 Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Thu, 4 Jul 2013 14:44:48 -0700 Subject: [PATCH 09/55] Virtualbox: clean up port forwarding before exporting OVF. --- builder/virtualbox/step_export.go | 16 +++++++++++++--- builder/virtualbox/step_forward_ssh.go | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/builder/virtualbox/step_export.go b/builder/virtualbox/step_export.go index e29aabf0b..52472246a 100644 --- a/builder/virtualbox/step_export.go +++ b/builder/virtualbox/step_export.go @@ -7,8 +7,7 @@ import ( "path/filepath" ) -// This step creates the virtual disk that will be used as the -// hard drive for the virtual machine. +// This step cleans up forwarded ports and exports the VM to an OVF. // // Uses: // @@ -22,9 +21,20 @@ func (s *stepExport) Run(state map[string]interface{}) multistep.StepAction { ui := state["ui"].(packer.Ui) vmName := state["vmName"].(string) + // Clear out the Packer-created forwarding rule + ui.Say(fmt.Sprintf("Deleting forwarded port mapping for SSH (host port %d)", state["sshHostPort"])) + command := []string{"modifyvm", vmName, "--natpf1", "delete", "packerssh"} + if err := driver.VBoxManage(command...); err != nil { + err := fmt.Errorf("Error deleting port forwarding rule: %s", err) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // Export the VM to an OVF outputPath := filepath.Join(config.OutputDir, "packer.ovf") - command := []string{ + command = []string{ "export", vmName, "--output", diff --git a/builder/virtualbox/step_forward_ssh.go b/builder/virtualbox/step_forward_ssh.go index aee47bf1c..74a2354ec 100644 --- a/builder/virtualbox/step_forward_ssh.go +++ b/builder/virtualbox/step_forward_ssh.go @@ -36,7 +36,7 @@ func (s *stepForwardSSH) Run(state map[string]interface{}) multistep.StepAction } } - // Attach the disk to the controller + // Create a forwarded port mapping to the VM ui.Say(fmt.Sprintf("Creating forwarded port mapping for SSH (host port %d)", sshHostPort)) command := []string{ "modifyvm", vmName, From 3068d0e3fd9e082cce3c4c7c4eafd711f5a9f0e7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 4 Jul 2013 15:47:06 -0700 Subject: [PATCH 10/55] Update CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 932f346e7..02dbc56ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ FEATURES: * "file" uploader will upload files and directories from the machine running Packer to the remote machine. +IMPROVEMENTS: + +* virtualbox: Delete the packer-made SSH port forwarding prior to + exporting the VM. + ## 0.1.4 (July 2, 2013) FEATURES: From 8735ac99dbf92ae03c29d960dd97cef6800fffc1 Mon Sep 17 00:00:00 2001 From: Emil Sit Date: Thu, 4 Jul 2013 23:59:52 -0400 Subject: [PATCH 11/55] README: Clarify repository placement relative to GOPATH If you don't put the repository in the right place in GOPATH, changes to your sources don't get built. Signed-off-by: Emil Sit --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 43a7d52db..9b157cab1 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,8 @@ For some additional dependencies, Go needs [Mercurial](http://mercurial.selenic. to be installed. Packer itself doesn't require this but a dependency of a dependency does. -Next, clone this repository then just type `make`. In a few moments, -you'll have a working `packer` executable: +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: ``` $ make From c7ce4bf74c28bc99496871e7a02b366fa5530169 Mon Sep 17 00:00:00 2001 From: Philip Cristiano Date: Fri, 5 Jul 2013 08:42:22 -0400 Subject: [PATCH 12/55] docs: Fix invalid example --- website/source/docs/templates/introduction.html.markdown | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/website/source/docs/templates/introduction.html.markdown b/website/source/docs/templates/introduction.html.markdown index 21180f0f7..799fd9e20 100644 --- a/website/source/docs/templates/introduction.html.markdown +++ b/website/source/docs/templates/introduction.html.markdown @@ -54,6 +54,7 @@ missing valid AWS access keys. Otherwise, it would work properly with "secret_key": "...", "region": "us-east-1", "source_ami": "ami-de0d9eb7", + "instance_type": "m1.small", "ssh_username": "ubuntu", "ami_name": "packer {{.CreateTime}}" } @@ -62,7 +63,7 @@ missing valid AWS access keys. Otherwise, it would work properly with "provisioners": [ { "type": "shell", - "path": "setup_things.sh" + "script": "setup_things.sh" } ] } From fb0af12c22a5af392d32dbad97ae695872b2b53c Mon Sep 17 00:00:00 2001 From: Philip Cristiano Date: Fri, 5 Jul 2013 13:22:38 -0400 Subject: [PATCH 13/55] docs: Use free-tier instance size --- website/source/docs/templates/introduction.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/docs/templates/introduction.html.markdown b/website/source/docs/templates/introduction.html.markdown index 799fd9e20..8a2213675 100644 --- a/website/source/docs/templates/introduction.html.markdown +++ b/website/source/docs/templates/introduction.html.markdown @@ -54,7 +54,7 @@ missing valid AWS access keys. Otherwise, it would work properly with "secret_key": "...", "region": "us-east-1", "source_ami": "ami-de0d9eb7", - "instance_type": "m1.small", + "instance_type": "t1.micro", "ssh_username": "ubuntu", "ami_name": "packer {{.CreateTime}}" } From 04c4d7f7a9d1a95d44779591526997b57e3c17c0 Mon Sep 17 00:00:00 2001 From: Philip Cristiano Date: Fri, 5 Jul 2013 13:35:31 -0400 Subject: [PATCH 14/55] docs: Use free-tier instance size --- website/source/docs/builders/amazon-ebs.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/docs/builders/amazon-ebs.html.markdown b/website/source/docs/builders/amazon-ebs.html.markdown index 0e619eb09..2d5e24724 100644 --- a/website/source/docs/builders/amazon-ebs.html.markdown +++ b/website/source/docs/builders/amazon-ebs.html.markdown @@ -81,7 +81,7 @@ Here is a basic example. It is completely valid except for the access keys: "secret_key": "YOUR SECRET KEY HERE", "region": "us-east-1", "source_ami": "ami-de0d9eb7", - "instance_type": "m1.small", + "instance_type": "t1.micro", "ssh_username": "ubuntu", "ami_name": "packer-quick-start {{.CreateTime}}" } From 534c923a44e1d2a22baedcc9052bb3736da194b1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 5 Jul 2013 10:41:54 -0700 Subject: [PATCH 15/55] post-processor/vagrant: Extra logging --- post-processor/vagrant/aws.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/post-processor/vagrant/aws.go b/post-processor/vagrant/aws.go index 9ca633f16..110954a7e 100644 --- a/post-processor/vagrant/aws.go +++ b/post-processor/vagrant/aws.go @@ -5,6 +5,7 @@ import ( "github.com/mitchellh/mapstructure" "github.com/mitchellh/packer/packer" "io/ioutil" + "log" "os" "path/filepath" "strings" @@ -75,6 +76,7 @@ func (p *AWSBoxPostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact vagrantfileContents := defaultAWSVagrantfile if p.config.VagrantfileTemplate != "" { + log.Printf("Using vagrantfile template: %s", p.config.VagrantfileTemplate) f, err := os.Open(p.config.VagrantfileTemplate) if err != nil { return nil, false, err From f5003c4e195560c4e6dbad8cf439e223c77bcc54 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 5 Jul 2013 10:50:57 -0700 Subject: [PATCH 16/55] post-processor/vagrant: properly forward config into provider [GH-129] --- CHANGELOG.md | 5 +++++ post-processor/vagrant/post-processor.go | 11 +++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02dbc56ed..71ad528e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ IMPROVEMENTS: * virtualbox: Delete the packer-made SSH port forwarding prior to exporting the VM. +BUG FIXES: + +* vagrant: Properly configure the provider-specific post-processors so + things like `vagrantfile_template` work. [GH-129] + ## 0.1.4 (July 2, 2013) FEATURES: diff --git a/post-processor/vagrant/post-processor.go b/post-processor/vagrant/post-processor.go index 3b6c8cd47..8446b9a5c 100644 --- a/post-processor/vagrant/post-processor.go +++ b/post-processor/vagrant/post-processor.go @@ -24,11 +24,15 @@ type Config struct { } type PostProcessor struct { - config Config - premade map[string]packer.PostProcessor + config Config + premade map[string]packer.PostProcessor + rawConfigs []interface{} } func (p *PostProcessor) Configure(raws ...interface{}) error { + // Store the raw configs for usage later + p.rawConfigs = raws + for _, raw := range raws { err := mapstructure.Decode(raw, &p.config) if err != nil { @@ -93,8 +97,7 @@ func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (pac return nil, false, fmt.Errorf("Vagrant box post-processor not found: %s", ppName) } - config := map[string]string{"output": p.config.OutputPath} - if err := pp.Configure(config); err != nil { + if err := pp.Configure(p.rawConfigs...); err != nil { return nil, false, err } } From 8b25acfab65f2053d64f2ab8384bfa6006df296b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 5 Jul 2013 11:00:18 -0700 Subject: [PATCH 17/55] fmt --- builder/virtualbox/step_export.go | 8 ++++---- post-processor/vagrant/aws.go | 2 ++ post-processor/vagrant/util.go | 2 +- provisioner/file/provisioner_test.go | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/builder/virtualbox/step_export.go b/builder/virtualbox/step_export.go index 52472246a..a37f718f4 100644 --- a/builder/virtualbox/step_export.go +++ b/builder/virtualbox/step_export.go @@ -21,17 +21,17 @@ func (s *stepExport) Run(state map[string]interface{}) multistep.StepAction { ui := state["ui"].(packer.Ui) vmName := state["vmName"].(string) - // Clear out the Packer-created forwarding rule + // Clear out the Packer-created forwarding rule ui.Say(fmt.Sprintf("Deleting forwarded port mapping for SSH (host port %d)", state["sshHostPort"])) command := []string{"modifyvm", vmName, "--natpf1", "delete", "packerssh"} if err := driver.VBoxManage(command...); err != nil { err := fmt.Errorf("Error deleting port forwarding rule: %s", err) - state["error"] = err + state["error"] = err ui.Error(err.Error()) - return multistep.ActionHalt + return multistep.ActionHalt } - // Export the VM to an OVF + // Export the VM to an OVF outputPath := filepath.Join(config.OutputDir, "packer.ovf") command = []string{ diff --git a/post-processor/vagrant/aws.go b/post-processor/vagrant/aws.go index 110954a7e..ae5de6ac9 100644 --- a/post-processor/vagrant/aws.go +++ b/post-processor/vagrant/aws.go @@ -79,12 +79,14 @@ func (p *AWSBoxPostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact log.Printf("Using vagrantfile template: %s", p.config.VagrantfileTemplate) f, err := os.Open(p.config.VagrantfileTemplate) if err != nil { + err = fmt.Errorf("error opening vagrantfile template: %s", err) return nil, false, err } defer f.Close() contents, err := ioutil.ReadAll(f) if err != nil { + err = fmt.Errorf("error reading vagrantfile template: %s", err) return nil, false, err } diff --git a/post-processor/vagrant/util.go b/post-processor/vagrant/util.go index cd1c8c351..fa6ee8f25 100644 --- a/post-processor/vagrant/util.go +++ b/post-processor/vagrant/util.go @@ -47,7 +47,7 @@ func DirToBox(dst, dir string) error { // Skip directories if info.IsDir() { - log.Printf("Skiping directory '%s' for box '%s'", path, dst) + log.Printf("Skipping directory '%s' for box '%s'", path, dst) return nil } diff --git a/provisioner/file/provisioner_test.go b/provisioner/file/provisioner_test.go index 6fa1e031a..1cae2395a 100644 --- a/provisioner/file/provisioner_test.go +++ b/provisioner/file/provisioner_test.go @@ -114,7 +114,7 @@ func TestProvisionerProvision_SendsFile(t *testing.T) { } config := map[string]interface{}{ - "source": tf.Name(), + "source": tf.Name(), "destination": "something", } From 324a2229a6e9edfde30cc6aa16f55b84c0d1c1cb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 5 Jul 2013 11:11:54 -0700 Subject: [PATCH 18/55] post-processor/vagrant: provider PPs get properly configured --- CHANGELOG.md | 2 ++ post-processor/vagrant/aws.go | 1 + post-processor/vagrant/post-processor.go | 16 +++++++++++----- post-processor/vagrant/util.go | 2 +- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71ad528e7..c04bc7642 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ IMPROVEMENTS: BUG FIXES: +* vagrant: The `BuildName` template propery works properly in + the output path. * vagrant: Properly configure the provider-specific post-processors so things like `vagrantfile_template` work. [GH-129] diff --git a/post-processor/vagrant/aws.go b/post-processor/vagrant/aws.go index ae5de6ac9..747207832 100644 --- a/post-processor/vagrant/aws.go +++ b/post-processor/vagrant/aws.go @@ -105,6 +105,7 @@ func (p *AWSBoxPostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact // Compress the directory to the given output path if err := DirToBox(outputPath, dir); err != nil { + err = fmt.Errorf("error creating box: %s", err) return nil, false, err } diff --git a/post-processor/vagrant/post-processor.go b/post-processor/vagrant/post-processor.go index 8446b9a5c..0ad450dc8 100644 --- a/post-processor/vagrant/post-processor.go +++ b/post-processor/vagrant/post-processor.go @@ -40,8 +40,10 @@ func (p *PostProcessor) Configure(raws ...interface{}) error { } } + ppExtraConfig := make(map[string]interface{}) if p.config.OutputPath == "" { p.config.OutputPath = "packer_{{ .BuildName }}_{{.Provider}}.box" + ppExtraConfig["output"] = p.config.OutputPath } _, err := template.New("output").Parse(p.config.OutputPath) @@ -49,16 +51,15 @@ func (p *PostProcessor) Configure(raws ...interface{}) error { return fmt.Errorf("output invalid template: %s", err) } + // Store the extra configuration for post-processors + p.rawConfigs = append(p.rawConfigs, ppExtraConfig) + // TODO(mitchellh): Properly handle multiple raw configs var mapConfig map[string]interface{} if err := mapstructure.Decode(raws[0], &mapConfig); err != nil { return err } - packerConfig := map[string]interface{}{ - packer.BuildNameConfigKey: p.config.PackerBuildName, - } - p.premade = make(map[string]packer.PostProcessor) errors := make([]error, 0) for k, raw := range mapConfig { @@ -67,7 +68,12 @@ func (p *PostProcessor) Configure(raws ...interface{}) error { continue } - if err := pp.Configure(raw, packerConfig); err != nil { + // Create the proper list of configurations + ppConfigs := make([]interface{}, 0, len(p.rawConfigs)+1) + copy(ppConfigs, p.rawConfigs) + ppConfigs = append(ppConfigs, raw) + + if err := pp.Configure(ppConfigs...); err != nil { errors = append(errors, err) } diff --git a/post-processor/vagrant/util.go b/post-processor/vagrant/util.go index fa6ee8f25..5966008c5 100644 --- a/post-processor/vagrant/util.go +++ b/post-processor/vagrant/util.go @@ -25,7 +25,7 @@ type OutputPathTemplate struct { // box. This function does not perform checks to verify that dir is // actually a proper box. This is an expected precondition. func DirToBox(dst, dir string) error { - log.Printf("Turning dir into box: %s", dir) + log.Printf("Turning dir into box: %s => %s", dir, dst) dstF, err := os.Create(dst) if err != nil { return err From 62199c8c7339ba145f521b6b68a26f25446b4342 Mon Sep 17 00:00:00 2001 From: Pierre Riteau Date: Fri, 5 Jul 2013 19:23:24 +0100 Subject: [PATCH 19/55] Fix some typos and an error in the documentation --- website/source/docs/builders/digitalocean.html.markdown | 2 +- website/source/docs/command-line/build.html.markdown | 2 +- website/source/docs/extend/plugins.html.markdown | 2 +- website/source/docs/templates/post-processors.html.markdown | 4 ++-- website/source/docs/templates/provisioners.html.markdown | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/website/source/docs/builders/digitalocean.html.markdown b/website/source/docs/builders/digitalocean.html.markdown index 8f2a6eb70..990410dd1 100644 --- a/website/source/docs/builders/digitalocean.html.markdown +++ b/website/source/docs/builders/digitalocean.html.markdown @@ -89,7 +89,7 @@ the prior linked page for information on syntax if you're unfamiliar with it. The available variables are shown below: -* `CreateTime`- This will be replaced with the Unix timestamp of when the +* `CreateTime` - This will be replaced with the Unix timestamp of when the image is created. ## Finding Image, Region, and Size IDs diff --git a/website/source/docs/command-line/build.html.markdown b/website/source/docs/command-line/build.html.markdown index e6ae28c24..8e8f5832d 100644 --- a/website/source/docs/command-line/build.html.markdown +++ b/website/source/docs/command-line/build.html.markdown @@ -14,7 +14,7 @@ artifacts that are created will be outputted at the end of the build. * `-debug` - Disables parallelization and enables debug mode. Debug mode flags the builders that they should output debugging information. The exact behavior of debug mode is left to the builder. In general, builders usually will stop - between each step, waiting keyboard input before continuing. This will allow + between each step, waiting for keyboard input before continuing. This will allow the user to inspect state and so on. * `-except=foo,bar,baz` - Builds all the builds except those with the given diff --git a/website/source/docs/extend/plugins.html.markdown b/website/source/docs/extend/plugins.html.markdown index 79cc71107..cdd7d1088 100644 --- a/website/source/docs/extend/plugins.html.markdown +++ b/website/source/docs/extend/plugins.html.markdown @@ -8,7 +8,7 @@ Plugins allow new functionality to be added to Packer without modifying the core source code. Packer plugins are able to add new commands, builders, provisioners, hooks, and more. In fact, much of Packer itself is implemented by writing plugins that are simply distributed with -the Packer. For example, all the commands, builders, provisioners, and more +Packer. For example, all the commands, builders, provisioners, and more that ship with Packer are implemented as Plugins that are simply hardcoded to load with Packer. diff --git a/website/source/docs/templates/post-processors.html.markdown b/website/source/docs/templates/post-processors.html.markdown index 608b6733e..e4c1bfa8c 100644 --- a/website/source/docs/templates/post-processors.html.markdown +++ b/website/source/docs/templates/post-processors.html.markdown @@ -72,7 +72,7 @@ A **sequence definition** is a JSON array comprised of other **simple** or **detailed** definitions. The post-processors defined in the array are run in order, with the artifact of each feeding into the next, and any intermediary artifacts being discarded. A sequence definition may not contain another -sequence definition. Sequnce definitions are used to chain together multiple +sequence definition. Sequence definitions are used to chain together multiple post-processors. An example is shown below, where the artifact of a build is compressed then uploaded, but the compressed result is not kept. @@ -93,7 +93,7 @@ are simply shortcuts for a **sequence** definition of only one element. ## Input Artifacts When using post-processors, the input artifact (coming from a builder or -another post-proccessor) is discarded by default after the post-processor runs. +another post-processor) is discarded by default after the post-processor runs. This is because generally, you don't want the intermediary artifacts on the way to the final artifact created. diff --git a/website/source/docs/templates/provisioners.html.markdown b/website/source/docs/templates/provisioners.html.markdown index a2c95e628..f4263fb9b 100644 --- a/website/source/docs/templates/provisioners.html.markdown +++ b/website/source/docs/templates/provisioners.html.markdown @@ -39,7 +39,7 @@ the `type` key. This key specifies the name of the provisioner to use. Additional keys within the object are used to configure the provisioner, with the exception of a handful of special keys, covered later. -As an example, the "shell" provisioner requires at least the `script` key, +As an example, the "shell" provisioner requires a key such as `script` which specifies a path to a shell script to execute within the machines being created. From d5731a13d4feb9ef07d784940379ed55b25d00c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Salvador=20Girone=CC=80s?= Date: Sat, 6 Jul 2013 11:28:56 +0200 Subject: [PATCH 20/55] Local mode for VBoxGuestAdditions. Provide local path and SHA256 --- builder/virtualbox/builder.go | 89 +++++++--- .../step_download_guest_additions.go | 157 ++++++++++-------- 2 files changed, 158 insertions(+), 88 deletions(-) diff --git a/builder/virtualbox/builder.go b/builder/virtualbox/builder.go index 47df9e54b..f7ec0e455 100644 --- a/builder/virtualbox/builder.go +++ b/builder/virtualbox/builder.go @@ -25,29 +25,31 @@ type Builder struct { } type config struct { - BootCommand []string `mapstructure:"boot_command"` - BootWait time.Duration `` - DiskSize uint `mapstructure:"disk_size"` - GuestAdditionsPath string `mapstructure:"guest_additions_path"` - GuestOSType string `mapstructure:"guest_os_type"` - Headless bool `mapstructure:"headless"` - HTTPDir string `mapstructure:"http_directory"` - HTTPPortMin uint `mapstructure:"http_port_min"` - HTTPPortMax uint `mapstructure:"http_port_max"` - ISOMD5 string `mapstructure:"iso_md5"` - ISOUrl string `mapstructure:"iso_url"` - OutputDir string `mapstructure:"output_directory"` - ShutdownCommand string `mapstructure:"shutdown_command"` - ShutdownTimeout time.Duration `` - SSHHostPortMin uint `mapstructure:"ssh_host_port_min"` - SSHHostPortMax uint `mapstructure:"ssh_host_port_max"` - SSHPassword string `mapstructure:"ssh_password"` - SSHPort uint `mapstructure:"ssh_port"` - SSHUser string `mapstructure:"ssh_username"` - SSHWaitTimeout time.Duration `` - VBoxVersionFile string `mapstructure:"virtualbox_version_file"` - VBoxManage [][]string `mapstructure:"vboxmanage"` - VMName string `mapstructure:"vm_name"` + BootCommand []string `mapstructure:"boot_command"` + BootWait time.Duration `` + DiskSize uint `mapstructure:"disk_size"` + GuestAdditionsPath string `mapstructure:"guest_additions_path"` + GuestAdditionsURL string `mapstructure:"guest_additions_url"` + GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"` + GuestOSType string `mapstructure:"guest_os_type"` + Headless bool `mapstructure:"headless"` + HTTPDir string `mapstructure:"http_directory"` + HTTPPortMin uint `mapstructure:"http_port_min"` + HTTPPortMax uint `mapstructure:"http_port_max"` + ISOMD5 string `mapstructure:"iso_md5"` + ISOUrl string `mapstructure:"iso_url"` + OutputDir string `mapstructure:"output_directory"` + ShutdownCommand string `mapstructure:"shutdown_command"` + ShutdownTimeout time.Duration `` + SSHHostPortMin uint `mapstructure:"ssh_host_port_min"` + SSHHostPortMax uint `mapstructure:"ssh_host_port_max"` + SSHPassword string `mapstructure:"ssh_password"` + SSHPort uint `mapstructure:"ssh_port"` + SSHUser string `mapstructure:"ssh_username"` + SSHWaitTimeout time.Duration `` + VBoxVersionFile string `mapstructure:"virtualbox_version_file"` + VBoxManage [][]string `mapstructure:"vboxmanage"` + VMName string `mapstructure:"vm_name"` PackerBuildName string `mapstructure:"packer_build_name"` PackerDebug bool `mapstructure:"packer_debug"` @@ -170,6 +172,47 @@ func (b *Builder) Prepare(raws ...interface{}) error { } } + if b.config.GuestAdditionsSHA256 != "" { + b.config.GuestAdditionsSHA256 = strings.ToLower(b.config.GuestAdditionsSHA256) + } + + if b.config.GuestAdditionsURL != "" { + url, err := url.Parse(b.config.GuestAdditionsURL) + if err != nil { + errs = append(errs, fmt.Errorf("guest_additions_url is not a valid URL: %s", err)) + } else { + if url.Scheme == "" { + url.Scheme = "file" + } + + if url.Scheme == "file" { + if _, err := os.Stat(url.Path); err != nil { + errs = append(errs, fmt.Errorf("guest_additions_url points to bad file: %s", err)) + } + } else { + supportedSchemes := []string{"file", "http", "https"} + scheme := strings.ToLower(url.Scheme) + + found := false + for _, supported := range supportedSchemes { + if scheme == supported { + found = true + break + } + } + + if !found { + errs = append(errs, fmt.Errorf("Unsupported URL scheme in guest_additions_url: %s", scheme)) + } + } + } + + if len(errs) == 0 { + // Put the URL back together since we may have modified it + b.config.GuestAdditionsURL = url.String() + } + } + if _, err := os.Stat(b.config.OutputDir); err == nil { errs = append(errs, errors.New("Output directory already exists. It must not exist.")) } diff --git a/builder/virtualbox/step_download_guest_additions.go b/builder/virtualbox/step_download_guest_additions.go index 127f9de8e..3253a1b9d 100644 --- a/builder/virtualbox/step_download_guest_additions.go +++ b/builder/virtualbox/step_download_guest_additions.go @@ -33,7 +33,9 @@ func (s *stepDownloadGuestAdditions) Run(state map[string]interface{}) multistep cache := state["cache"].(packer.Cache) driver := state["driver"].(Driver) ui := state["ui"].(packer.Ui) + config := state["config"].(*config) + // Get VBox version version, err := driver.Version() if err != nil { state["error"] = fmt.Errorf("Error reading version for guest additions download: %s", err) @@ -45,69 +47,19 @@ func (s *stepDownloadGuestAdditions) Run(state map[string]interface{}) multistep version = newVersion } - // First things first, we get the list of checksums for the files available - // for this version. - checksumsUrl := fmt.Sprintf("http://download.virtualbox.org/virtualbox/%s/SHA256SUMS", version) - checksumsFile, err := ioutil.TempFile("", "packer") - if err != nil { - state["error"] = fmt.Errorf( - "Failed creating temporary file to store guest addition checksums: %s", - err) - return multistep.ActionHalt - } - checksumsFile.Close() - defer os.Remove(checksumsFile.Name()) - - downloadConfig := &common.DownloadConfig{ - Url: checksumsUrl, - TargetPath: checksumsFile.Name(), - Hash: nil, - } - - log.Printf("Downloading guest addition checksums: %s", checksumsUrl) - download := common.NewDownloadClient(downloadConfig) - checksumsPath, action := s.progressDownload(download, state) - if action != multistep.ActionContinue { - return action - } - additionsName := fmt.Sprintf("VBoxGuestAdditions_%s.iso", version) - // Next, we find the checksum for the file we're looking to download. - // It is an error if the checksum cannot be found. - checksumsF, err := os.Open(checksumsPath) - if err != nil { - state["error"] = fmt.Errorf("Error opening guest addition checksums: %s", err) - return multistep.ActionHalt - } - defer checksumsF.Close() + // Use provided version or get it from virtualbox.org + var checksum string - // We copy the contents of the file into memory. In general this file - // is quite small so that is okay. In the future, we probably want to - // use bufio and iterate line by line. - var contents bytes.Buffer - io.Copy(&contents, checksumsF) - - checksum := "" - for _, line := range strings.Split(contents.String(), "\n") { - parts := strings.Fields(line) - log.Printf("Checksum file parts: %#v", parts) - if len(parts) != 2 { - // Bogus line - continue - } - - if strings.HasSuffix(parts[1], additionsName) { - checksum = parts[0] - log.Printf("Guest additions checksum: %s", checksum) - break - } - } - - if checksum == "" { - state["error"] = fmt.Errorf("The checksum for the file '%s' could not be found.", additionsName) - return multistep.ActionHalt - } + if config.GuestAdditionsSHA256 != "" { + checksum = config.GuestAdditionsSHA256 + } else { + checksum, action = s.downloadAdditionsSHA256(state, version, additionsName) + if action != multistep.ActionContinue { + return action + } + } checksumBytes, err := hex.DecodeString(checksum) if err != nil { @@ -115,23 +67,29 @@ func (s *stepDownloadGuestAdditions) Run(state map[string]interface{}) multistep return multistep.ActionHalt } - url := fmt.Sprintf( - "http://download.virtualbox.org/virtualbox/%s/%s", - version, additionsName) + // Use the provided source (URL or file path) or generate it + url := config.GuestAdditionsURL + if url == "" { + url = fmt.Sprintf( + "http://download.virtualbox.org/virtualbox/%s/%s", + version, + additionsName) + } + log.Printf("Guest additions URL: %s", url) log.Printf("Acquiring lock to download the guest additions ISO.") cachePath := cache.Lock(url) defer cache.Unlock(url) - downloadConfig = &common.DownloadConfig{ + downloadConfig := &common.DownloadConfig{ Url: url, TargetPath: cachePath, Hash: sha256.New(), Checksum: checksumBytes, } - download = common.NewDownloadClient(downloadConfig) + download := common.NewDownloadClient(downloadConfig) ui.Say("Downloading VirtualBox guest additions. Progress will be shown periodically.") state["guest_additions_path"], action = s.progressDownload(download, state) return action @@ -179,3 +137,72 @@ DownloadWaitLoop: return result, multistep.ActionContinue } + +func (s *stepDownloadGuestAdditions) downloadAdditionsSHA256 (state map[string]interface{}, additionsVersion string, additionsName string) (string, multistep.StepAction) { + // First things first, we get the list of checksums for the files available + // for this version. + checksumsUrl := fmt.Sprintf("http://download.virtualbox.org/virtualbox/%s/SHA256SUMS", additionsVersion) + checksumsFile, err := ioutil.TempFile("", "packer") + + if err != nil { + state["error"] = fmt.Errorf( + "Failed creating temporary file to store guest addition checksums: %s", + err) + return "", multistep.ActionHalt + } + + checksumsFile.Close() + defer os.Remove(checksumsFile.Name()) + + downloadConfig := &common.DownloadConfig{ + Url: checksumsUrl, + TargetPath: checksumsFile.Name(), + Hash: nil, + } + + log.Printf("Downloading guest addition checksums: %s", checksumsUrl) + download := common.NewDownloadClient(downloadConfig) + checksumsPath, action := s.progressDownload(download, state) + if action != multistep.ActionContinue { + return "", action + } + + // Next, we find the checksum for the file we're looking to download. + // It is an error if the checksum cannot be found. + checksumsF, err := os.Open(checksumsPath) + if err != nil { + state["error"] = fmt.Errorf("Error opening guest addition checksums: %s", err) + return "", multistep.ActionHalt + } + defer checksumsF.Close() + + // We copy the contents of the file into memory. In general this file + // is quite small so that is okay. In the future, we probably want to + // use bufio and iterate line by line. + var contents bytes.Buffer + io.Copy(&contents, checksumsF) + + checksum := "" + for _, line := range strings.Split(contents.String(), "\n") { + parts := strings.Fields(line) + log.Printf("Checksum file parts: %#v", parts) + if len(parts) != 2 { + // Bogus line + continue + } + + if strings.HasSuffix(parts[1], additionsName) { + checksum = parts[0] + log.Printf("Guest additions checksum: %s", checksum) + break + } + } + + if checksum == "" { + state["error"] = fmt.Errorf("The checksum for the file '%s' could not be found.", additionsName) + return "", multistep.ActionHalt + } + + return checksum, multistep.ActionContinue + +} From c46aae5b09fcbcb68b44fa933f7f280f8a45a093 Mon Sep 17 00:00:00 2001 From: Jack Pearkes Date: Sun, 7 Jul 2013 15:31:37 +0200 Subject: [PATCH 21/55] website: specify ruby version in the gemfile --- website/Gemfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/Gemfile b/website/Gemfile index 29ff7e53c..d270a5ae4 100644 --- a/website/Gemfile +++ b/website/Gemfile @@ -1,5 +1,7 @@ source 'https://rubygems.org' +ruby '1.9.3' + gem "middleman", "~> 3.0.6" gem "middleman-minify-html", "~> 3.0.0" gem "rack-contrib", "~> 1.1.0" From 70155fc6951ba0a5f021673c2d934007c1809649 Mon Sep 17 00:00:00 2001 From: Jack Pearkes Date: Sun, 7 Jul 2013 15:37:22 +0200 Subject: [PATCH 22/55] website: add a shutdown command to the vm builder config examples --- website/source/docs/builders/virtualbox.html.markdown | 3 ++- website/source/docs/builders/vmware.html.markdown | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/website/source/docs/builders/virtualbox.html.markdown b/website/source/docs/builders/virtualbox.html.markdown index 506609ee3..d869c8508 100644 --- a/website/source/docs/builders/virtualbox.html.markdown +++ b/website/source/docs/builders/virtualbox.html.markdown @@ -27,7 +27,8 @@ Ubuntu to self-install. Still, the example serves to show the basic configuratio "iso_url": "http://releases.ubuntu.com/12.04/ubuntu-12.04.2-server-amd64.iso", "iso_md5": "af5f788aee1b32c4b2634734309cc9e9", "ssh_username": "packer", - "ssh_wait_timeout": "30s" + "ssh_wait_timeout": "30s", + "shutdown_command": "shutdown -P now" } diff --git a/website/source/docs/builders/vmware.html.markdown b/website/source/docs/builders/vmware.html.markdown index e16e6918d..e9bf5955b 100644 --- a/website/source/docs/builders/vmware.html.markdown +++ b/website/source/docs/builders/vmware.html.markdown @@ -28,7 +28,8 @@ Ubuntu to self-install. Still, the example serves to show the basic configuratio "iso_url": "http://releases.ubuntu.com/12.04/ubuntu-12.04.2-server-amd64.iso", "iso_md5": "af5f788aee1b32c4b2634734309cc9e9", "ssh_username": "packer", - "ssh_wait_timeout": "30s" + "ssh_wait_timeout": "30s", + "shutdown_command": "shutdown -P now" } From ee9aa9b071da435c26c592377c4588206fd49a04 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 7 Jul 2013 09:09:22 -0700 Subject: [PATCH 23/55] builder/virtualbox: typo in test --- builder/virtualbox/builder_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builder/virtualbox/builder_test.go b/builder/virtualbox/builder_test.go index eb080af47..a2f5b5bb1 100644 --- a/builder/virtualbox/builder_test.go +++ b/builder/virtualbox/builder_test.go @@ -119,7 +119,7 @@ func TestBuilderPrepare_GuestAdditionsPath(t *testing.T) { var b Builder config := testConfig() - delete(config, "disk_size") + delete(config, "guest_additions_path") err := b.Prepare(config) if err != nil { t.Fatalf("bad err: %s", err) From c1ad88aaa6468e8400834686883b009f81e660dc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 7 Jul 2013 09:14:16 -0700 Subject: [PATCH 24/55] builder/virtualbox: test the configuration /cc @sgirones --- builder/virtualbox/builder_test.go | 76 ++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/builder/virtualbox/builder_test.go b/builder/virtualbox/builder_test.go index a2f5b5bb1..6b94eeefe 100644 --- a/builder/virtualbox/builder_test.go +++ b/builder/virtualbox/builder_test.go @@ -141,6 +141,82 @@ func TestBuilderPrepare_GuestAdditionsPath(t *testing.T) { } } +func TestBuilderPrepare_GuestAdditionsSHA256(t *testing.T) { + var b Builder + config := testConfig() + + delete(config, "guest_additions_sha256") + err := b.Prepare(config) + if err != nil { + t.Fatalf("bad err: %s", err) + } + + if b.config.GuestAdditionsSHA256 != "" { + t.Fatalf("bad: %s", b.config.GuestAdditionsSHA256) + } + + config["guest_additions_sha256"] = "FOO" + b = Builder{} + err = b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if b.config.GuestAdditionsSHA256 != "foo" { + t.Fatalf("bad size: %s", b.config.GuestAdditionsSHA256) + } +} + +func TestBuilderPrepare_GuestAdditionsURL(t *testing.T) { + var b Builder + config := testConfig() + + config["guest_additions_url"] = "" + err := b.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + if b.config.GuestAdditionsURL != "" { + t.Fatalf("should be empty: %s", b.config.GuestAdditionsURL) + } + + config["guest_additions_url"] = "i/am/a/file/that/doesnt/exist" + err = b.Prepare(config) + if err == nil { + t.Error("should have error") + } + + config["guest_additions_url"] = "file:i/am/a/file/that/doesnt/exist" + err = b.Prepare(config) + if err == nil { + t.Error("should have error") + } + + config["guest_additions_url"] = "http://www.packer.io" + err = b.Prepare(config) + if err != nil { + t.Errorf("should not have error: %s", err) + } + + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("error tempfile: %s", err) + } + defer os.Remove(tf.Name()) + + config["guest_additions_url"] = tf.Name() + err = b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if b.config.GuestAdditionsURL != "file://"+tf.Name() { + t.Fatalf("guest_additions_url should be modified: %s", b.config.GuestAdditionsURL) + } +} + + func TestBuilderPrepare_HTTPPort(t *testing.T) { var b Builder config := testConfig() From 4ecb92358bfaa750c5e13418565fec7c6a2deccf Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 7 Jul 2013 09:14:41 -0700 Subject: [PATCH 25/55] fmt --- builder/virtualbox/builder.go | 50 +++++++++---------- builder/virtualbox/builder_test.go | 1 - .../step_download_guest_additions.go | 46 ++++++++--------- 3 files changed, 48 insertions(+), 49 deletions(-) diff --git a/builder/virtualbox/builder.go b/builder/virtualbox/builder.go index f7ec0e455..d0860331e 100644 --- a/builder/virtualbox/builder.go +++ b/builder/virtualbox/builder.go @@ -25,31 +25,31 @@ type Builder struct { } type config struct { - BootCommand []string `mapstructure:"boot_command"` - BootWait time.Duration `` - DiskSize uint `mapstructure:"disk_size"` - GuestAdditionsPath string `mapstructure:"guest_additions_path"` - GuestAdditionsURL string `mapstructure:"guest_additions_url"` - GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"` - GuestOSType string `mapstructure:"guest_os_type"` - Headless bool `mapstructure:"headless"` - HTTPDir string `mapstructure:"http_directory"` - HTTPPortMin uint `mapstructure:"http_port_min"` - HTTPPortMax uint `mapstructure:"http_port_max"` - ISOMD5 string `mapstructure:"iso_md5"` - ISOUrl string `mapstructure:"iso_url"` - OutputDir string `mapstructure:"output_directory"` - ShutdownCommand string `mapstructure:"shutdown_command"` - ShutdownTimeout time.Duration `` - SSHHostPortMin uint `mapstructure:"ssh_host_port_min"` - SSHHostPortMax uint `mapstructure:"ssh_host_port_max"` - SSHPassword string `mapstructure:"ssh_password"` - SSHPort uint `mapstructure:"ssh_port"` - SSHUser string `mapstructure:"ssh_username"` - SSHWaitTimeout time.Duration `` - VBoxVersionFile string `mapstructure:"virtualbox_version_file"` - VBoxManage [][]string `mapstructure:"vboxmanage"` - VMName string `mapstructure:"vm_name"` + BootCommand []string `mapstructure:"boot_command"` + BootWait time.Duration `` + DiskSize uint `mapstructure:"disk_size"` + GuestAdditionsPath string `mapstructure:"guest_additions_path"` + GuestAdditionsURL string `mapstructure:"guest_additions_url"` + GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"` + GuestOSType string `mapstructure:"guest_os_type"` + Headless bool `mapstructure:"headless"` + HTTPDir string `mapstructure:"http_directory"` + HTTPPortMin uint `mapstructure:"http_port_min"` + HTTPPortMax uint `mapstructure:"http_port_max"` + ISOMD5 string `mapstructure:"iso_md5"` + ISOUrl string `mapstructure:"iso_url"` + OutputDir string `mapstructure:"output_directory"` + ShutdownCommand string `mapstructure:"shutdown_command"` + ShutdownTimeout time.Duration `` + SSHHostPortMin uint `mapstructure:"ssh_host_port_min"` + SSHHostPortMax uint `mapstructure:"ssh_host_port_max"` + SSHPassword string `mapstructure:"ssh_password"` + SSHPort uint `mapstructure:"ssh_port"` + SSHUser string `mapstructure:"ssh_username"` + SSHWaitTimeout time.Duration `` + VBoxVersionFile string `mapstructure:"virtualbox_version_file"` + VBoxManage [][]string `mapstructure:"vboxmanage"` + VMName string `mapstructure:"vm_name"` PackerBuildName string `mapstructure:"packer_build_name"` PackerDebug bool `mapstructure:"packer_debug"` diff --git a/builder/virtualbox/builder_test.go b/builder/virtualbox/builder_test.go index 6b94eeefe..655ce57ad 100644 --- a/builder/virtualbox/builder_test.go +++ b/builder/virtualbox/builder_test.go @@ -216,7 +216,6 @@ func TestBuilderPrepare_GuestAdditionsURL(t *testing.T) { } } - func TestBuilderPrepare_HTTPPort(t *testing.T) { var b Builder config := testConfig() diff --git a/builder/virtualbox/step_download_guest_additions.go b/builder/virtualbox/step_download_guest_additions.go index 3253a1b9d..3965df779 100644 --- a/builder/virtualbox/step_download_guest_additions.go +++ b/builder/virtualbox/step_download_guest_additions.go @@ -35,7 +35,7 @@ func (s *stepDownloadGuestAdditions) Run(state map[string]interface{}) multistep ui := state["ui"].(packer.Ui) config := state["config"].(*config) - // Get VBox version + // Get VBox version version, err := driver.Version() if err != nil { state["error"] = fmt.Errorf("Error reading version for guest additions download: %s", err) @@ -49,17 +49,17 @@ func (s *stepDownloadGuestAdditions) Run(state map[string]interface{}) multistep additionsName := fmt.Sprintf("VBoxGuestAdditions_%s.iso", version) - // Use provided version or get it from virtualbox.org - var checksum string + // Use provided version or get it from virtualbox.org + var checksum string - if config.GuestAdditionsSHA256 != "" { - checksum = config.GuestAdditionsSHA256 - } else { - checksum, action = s.downloadAdditionsSHA256(state, version, additionsName) - if action != multistep.ActionContinue { - return action - } - } + if config.GuestAdditionsSHA256 != "" { + checksum = config.GuestAdditionsSHA256 + } else { + checksum, action = s.downloadAdditionsSHA256(state, version, additionsName) + if action != multistep.ActionContinue { + return action + } + } checksumBytes, err := hex.DecodeString(checksum) if err != nil { @@ -67,14 +67,14 @@ func (s *stepDownloadGuestAdditions) Run(state map[string]interface{}) multistep return multistep.ActionHalt } - // Use the provided source (URL or file path) or generate it - url := config.GuestAdditionsURL - if url == "" { - url = fmt.Sprintf( - "http://download.virtualbox.org/virtualbox/%s/%s", - version, - additionsName) - } + // Use the provided source (URL or file path) or generate it + url := config.GuestAdditionsURL + if url == "" { + url = fmt.Sprintf( + "http://download.virtualbox.org/virtualbox/%s/%s", + version, + additionsName) + } log.Printf("Guest additions URL: %s", url) @@ -138,12 +138,12 @@ DownloadWaitLoop: return result, multistep.ActionContinue } -func (s *stepDownloadGuestAdditions) downloadAdditionsSHA256 (state map[string]interface{}, additionsVersion string, additionsName string) (string, multistep.StepAction) { - // First things first, we get the list of checksums for the files available +func (s *stepDownloadGuestAdditions) downloadAdditionsSHA256(state map[string]interface{}, additionsVersion string, additionsName string) (string, multistep.StepAction) { + // First things first, we get the list of checksums for the files available // for this version. checksumsUrl := fmt.Sprintf("http://download.virtualbox.org/virtualbox/%s/SHA256SUMS", additionsVersion) checksumsFile, err := ioutil.TempFile("", "packer") - + if err != nil { state["error"] = fmt.Errorf( "Failed creating temporary file to store guest addition checksums: %s", @@ -203,6 +203,6 @@ func (s *stepDownloadGuestAdditions) downloadAdditionsSHA256 (state map[string]i return "", multistep.ActionHalt } - return checksum, multistep.ActionContinue + return checksum, multistep.ActionContinue } From cd6e154bd1afefa8a7c506d39e918258d8f874ae Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 7 Jul 2013 09:17:27 -0700 Subject: [PATCH 26/55] builder/virtualbox: style nitpicks and CHANGELOG --- CHANGELOG.md | 2 ++ builder/virtualbox/step_download_guest_additions.go | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c04bc7642..532395e4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ FEATURES: * "file" uploader will upload files and directories from the machine running Packer to the remote machine. +* VirtualBox guest additions URL and checksum can now be specified, allowing + the VirtualBox builder to have the ability to be used completely offline. IMPROVEMENTS: diff --git a/builder/virtualbox/step_download_guest_additions.go b/builder/virtualbox/step_download_guest_additions.go index 3965df779..9e304e968 100644 --- a/builder/virtualbox/step_download_guest_additions.go +++ b/builder/virtualbox/step_download_guest_additions.go @@ -142,17 +142,17 @@ func (s *stepDownloadGuestAdditions) downloadAdditionsSHA256(state map[string]in // First things first, we get the list of checksums for the files available // for this version. checksumsUrl := fmt.Sprintf("http://download.virtualbox.org/virtualbox/%s/SHA256SUMS", additionsVersion) - checksumsFile, err := ioutil.TempFile("", "packer") + checksumsFile, err := ioutil.TempFile("", "packer") if err != nil { state["error"] = fmt.Errorf( "Failed creating temporary file to store guest addition checksums: %s", err) return "", multistep.ActionHalt } + defer os.Remove(checksumsFile.Name()) checksumsFile.Close() - defer os.Remove(checksumsFile.Name()) downloadConfig := &common.DownloadConfig{ Url: checksumsUrl, From 308205cc6f9f8ee03e7b56725b18b4a179672fd5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 7 Jul 2013 12:06:13 -0700 Subject: [PATCH 27/55] website: document the guest additinos URL and SHA256 /cc @sgirones --- website/source/docs/builders/virtualbox.html.markdown | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/website/source/docs/builders/virtualbox.html.markdown b/website/source/docs/builders/virtualbox.html.markdown index d869c8508..1f6d42f6b 100644 --- a/website/source/docs/builders/virtualbox.html.markdown +++ b/website/source/docs/builders/virtualbox.html.markdown @@ -76,6 +76,16 @@ Optional: of the user. This is a [configuration template](/docs/templates/configuration-templates.html) where the `Version` variable is replaced with the VirtualBox version. +* `guest_additions_sha256` (string) - The SHA256 checksum of the guest + additions ISO that will be uploaded to the guest VM. By default the + checksums will be downloaded from the VirtualBox website, so this only + needs to be set if you want to be explicit about the checksum. + +* `guest_additions_url` (string) - The URL to the guest additions ISO + to upload. This can also be a file URL if the ISO is at a local path. + By default the VirtualBox builder will go and download the proper + guest additions ISO from the internet. + * `guest_os_type` (string) - The guest OS type being installed. By default this is "other", but you can get _dramatic_ performance improvements by setting this to the proper value. To view all available values for this From abc2bc60ab0e4cb01910ec2f68c385ffe7b01718 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 7 Jul 2013 12:16:31 -0700 Subject: [PATCH 28/55] builder/common: Error on non-200 download responses [GH-141] --- CHANGELOG.md | 2 ++ builder/common/download.go | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 532395e4c..c30467f49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ IMPROVEMENTS: BUG FIXES: +* core: Non-200 response codes on downloads now show proper errors. + [GH-141] * vagrant: The `BuildName` template propery works properly in the output path. * vagrant: Properly configure the provider-specific post-processors so diff --git a/builder/common/download.go b/builder/common/download.go index ff59faea4..d5d97d1c0 100644 --- a/builder/common/download.go +++ b/builder/common/download.go @@ -107,6 +107,9 @@ func (d *DownloadClient) Get() (string, error) { log.Printf("Downloading: %s", url.String()) err = d.downloader.Download(f, url) + if err != nil { + return "", err + } } if d.config.Hash != nil { @@ -160,11 +163,22 @@ func (*HTTPDownloader) Cancel() { } func (d *HTTPDownloader) Download(dst io.Writer, src *url.URL) error { + log.Printf("Starting download: %s", src.String()) resp, err := http.Get(src.String()) if err != nil { return err } + if resp.StatusCode != 200 { + log.Printf( + "Non-200 status code: %d. Getting error body.", resp.StatusCode) + + errorBody := new(bytes.Buffer) + io.Copy(errorBody, resp.Body) + return fmt.Errorf("HTTP error '%d'! Remote side responded:\n%s", + resp.StatusCode, errorBody.String()) + } + d.progress = 0 d.total = uint(resp.ContentLength) From eb009ca4e331f0a653f593e88ca92e51b183ec18 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 7 Jul 2013 12:23:32 -0700 Subject: [PATCH 29/55] communicator/ssh: show more descriptive error if SCP not avail [GH-127] --- CHANGELOG.md | 2 ++ communicator/ssh/communicator.go | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c30467f49..46c1c8a49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ FEATURES: IMPROVEMENTS: +* core: If SCP is not available, a more descriptive error message + is shown telling the user. [GH-127] * virtualbox: Delete the packer-made SSH port forwarding prior to exporting the VM. diff --git a/communicator/ssh/communicator.go b/communicator/ssh/communicator.go index cc5097777..50bf9c9e3 100644 --- a/communicator/ssh/communicator.go +++ b/communicator/ssh/communicator.go @@ -3,6 +3,7 @@ package ssh import ( "bytes" "code.google.com/p/go.crypto/ssh" + "errors" "fmt" "github.com/mitchellh/packer/packer" "io" @@ -145,6 +146,14 @@ func (c *comm) Upload(path string, input io.Reader) error { // Otherwise, we have an ExitErorr, meaning we can just read // the exit status log.Printf("non-zero exit status: %d", exitErr.ExitStatus()) + + // If we exited with status 127, it means SCP isn't available. + // Return a more descriptive error for that. + if exitErr.ExitStatus() == 127 { + return errors.New( + "SCP failed to start. This usually means that SCP is not\n" + + "properly installed on the remote system.") + } } return err From f59dfa90a9174213fd92168f9dcf9cca70bff752 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 7 Jul 2013 17:37:44 -0700 Subject: [PATCH 30/55] Started on a contributing doc --- CONTRIBUTING.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..7a9d7449a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,43 @@ +# Contributing to Packer + +**First:** if you're unsure or afraid of _anything_, just ask +or submit the issue or pull request anyways. You won't be yelled at for +giving your best effort. The worst that can happen is that you'll be +politely asked to change something. We appreciate any sort of contributions, +and don't want a wall of rules to get in the way of that. + +However, for those individuals who want a bit more guidance on the +best way to contribute to the project, read on. This document will cover +what we're looking for. By addressing all the points we're looking for, +it raises the chances we can quickly merge or address your contributions. + +## Issues + +### Reporting an Issue + +* Make sure you test against the latest released version. It is possible + we already fixed the bug you're experiencing. + +* Provide a reproducible test case. If a contributor can't reproduce an + issue, then it dramatically lowers the chances it'll get fixed. And in + some cases, the issue will eventually be closed. + +* Respond promptly to any questions made by the Packer team to your issue. + Stale issues will be closed. + +### Issue Lifecycle + +1. The issue is reported. + +2. The issue is verified and categorized by a Packer collaborator. + Categorization is done via tags. For example, bugs are marked as "bugs" + and easy fixes are marked as "easy". + +3. Unless it is critical, the issue is left for a period of time (sometimes + many weeks), giving outside contributors a chance to address the issue. + +4. The issue is addressed in a pull request or commit. The issue will be + referenced in the commit message so that the code that fixes it is clearly + linked. + +5. The issue is closed. From 8c01d2c226f88b361b80aae3044a8be926aaf7b4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 7 Jul 2013 17:44:13 -0700 Subject: [PATCH 31/55] post-processor/vagrant: properly close file handles [GH-100] --- CHANGELOG.md | 2 ++ post-processor/vagrant/util.go | 21 +++++++++++++++++++++ post-processor/vagrant/virtualbox.go | 15 ++------------- post-processor/vagrant/vmware.go | 15 ++------------- 4 files changed, 27 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46c1c8a49..7218653b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ BUG FIXES: the output path. * vagrant: Properly configure the provider-specific post-processors so things like `vagrantfile_template` work. [GH-129] +* vagrant: Close filehandles when copying files so Windows can + rename files. [GH-100] ## 0.1.4 (July 2, 2013) diff --git a/post-processor/vagrant/util.go b/post-processor/vagrant/util.go index 5966008c5..b194c129b 100644 --- a/post-processor/vagrant/util.go +++ b/post-processor/vagrant/util.go @@ -21,6 +21,27 @@ type OutputPathTemplate struct { Provider string } +// Copies a file by copying the contents of the file to another place. +func CopyContents(dst, src string) error { + srcF, err := os.Open(src) + if err != nil { + return err + } + defer srcF.Close() + + dstF, err := os.Create(dst) + if err != nil { + return err + } + defer dstF.Close() + + if _, err := io.Copy(dstF, srcF); err != nil { + return err + } + + return nil +} + // DirToBox takes the directory and compresses it into a Vagrant-compatible // box. This function does not perform checks to verify that dir is // actually a proper box. This is an expected precondition. diff --git a/post-processor/vagrant/virtualbox.go b/post-processor/vagrant/virtualbox.go index fd8b7dfce..0539d0167 100644 --- a/post-processor/vagrant/virtualbox.go +++ b/post-processor/vagrant/virtualbox.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/mitchellh/mapstructure" "github.com/mitchellh/packer/packer" - "io" "io/ioutil" "log" "os" @@ -66,19 +65,9 @@ func (p *VBoxBoxPostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifac // Copy all of the original contents into the temporary directory for _, path := range artifact.Files() { ui.Message(fmt.Sprintf("Copying: %s", path)) - src, err := os.Open(path) - if err != nil { - return nil, false, err - } - defer src.Close() - dst, err := os.Create(filepath.Join(dir, filepath.Base(path))) - if err != nil { - return nil, false, err - } - defer dst.Close() - - if _, err := io.Copy(dst, src); err != nil { + dstPath := filepath.Join(dir, filepath.Base(path)) + if err := CopyContents(dstPath, path); err != nil { return nil, false, err } } diff --git a/post-processor/vagrant/vmware.go b/post-processor/vagrant/vmware.go index 211e6322c..471582026 100644 --- a/post-processor/vagrant/vmware.go +++ b/post-processor/vagrant/vmware.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/mitchellh/mapstructure" "github.com/mitchellh/packer/packer" - "io" "io/ioutil" "os" "path/filepath" @@ -51,19 +50,9 @@ func (p *VMwareBoxPostProcessor) PostProcess(ui packer.Ui, artifact packer.Artif // Copy all of the original contents into the temporary directory for _, path := range artifact.Files() { ui.Message(fmt.Sprintf("Copying: %s", path)) - src, err := os.Open(path) - if err != nil { - return nil, false, err - } - defer src.Close() - dst, err := os.Create(filepath.Join(dir, filepath.Base(path))) - if err != nil { - return nil, false, err - } - defer dst.Close() - - if _, err := io.Copy(dst, src); err != nil { + dstPath := filepath.Join(dir, filepath.Base(path)) + if err := CopyContents(dstPath, path); err != nil { return nil, false, err } } From 62430fdcea26a8b8a71500b8ad401ee97a1d1071 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 7 Jul 2013 17:52:20 -0700 Subject: [PATCH 32/55] provisioner/shell: inline_shebang for inline scripts --- CHANGELOG.md | 4 ++++ provisioner/shell/provisioner.go | 8 ++++++++ provisioner/shell/provisioner_test.go | 27 +++++++++++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7218653b1..9e8a6f269 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ IMPROVEMENTS: * core: If SCP is not available, a more descriptive error message is shown telling the user. [GH-127] +* shell: Scripts are now executed by default according to their shebang, + not with `/bin/sh`. [GH-105] +* shell: You can specify what interpreter you want inline scripts to + run with `inline_shebang`. * virtualbox: Delete the packer-made SSH port forwarding prior to exporting the VM. diff --git a/provisioner/shell/provisioner.go b/provisioner/shell/provisioner.go index ee3b2b178..f027c5944 100644 --- a/provisioner/shell/provisioner.go +++ b/provisioner/shell/provisioner.go @@ -25,6 +25,9 @@ type config struct { // in the context of a single shell. Inline []string + // The shebang value used when running inline scripts. + InlineShebang string `mapstructure:"inline_shebang"` + // The local path of the shell script to upload and execute. Script string @@ -69,6 +72,10 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { p.config.Inline = nil } + if p.config.InlineShebang == "" { + p.config.InlineShebang = "/bin/sh" + } + if p.config.RemotePath == "" { p.config.RemotePath = DefaultRemotePath } @@ -136,6 +143,7 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { // Write our contents to it writer := bufio.NewWriter(tf) + writer.WriteString(fmt.Sprintf("#!%s\n", p.config.InlineShebang)) for _, command := range p.config.Inline { if _, err := writer.WriteString(command + "\n"); err != nil { return fmt.Errorf("Error preparing shell script: %s", err) diff --git a/provisioner/shell/provisioner_test.go b/provisioner/shell/provisioner_test.go index 2256d313d..32d3084a5 100644 --- a/provisioner/shell/provisioner_test.go +++ b/provisioner/shell/provisioner_test.go @@ -35,6 +35,33 @@ func TestProvisionerPrepare_Defaults(t *testing.T) { } } +func TestProvisionerPrepare_InlineShebang(t *testing.T) { + config := testConfig() + + delete(config, "inline_shebang") + p := new(Provisioner) + err := p.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if p.config.InlineShebang != "/bin/sh" { + t.Fatalf("bad value: %s", p.config.InlineShebang) + } + + // Test with a good one + config["inline_shebang"] = "foo" + p = new(Provisioner) + err = p.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if p.config.InlineShebang != "foo" { + t.Fatalf("bad value: %s", p.config.InlineShebang) + } +} + func TestProvisionerPrepare_Script(t *testing.T) { config := testConfig() delete(config, "inline") From f7587be61731c10ac74ccad7980eac891e6a43f1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 7 Jul 2013 17:55:02 -0700 Subject: [PATCH 33/55] website: document the inline_shebang stuff --- website/source/docs/provisioners/shell.html.markdown | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/website/source/docs/provisioners/shell.html.markdown b/website/source/docs/provisioners/shell.html.markdown index 0765421a4..6268e5c22 100644 --- a/website/source/docs/provisioners/shell.html.markdown +++ b/website/source/docs/provisioners/shell.html.markdown @@ -57,6 +57,11 @@ Optional parameters: the path to the script to run, and `Vars`, which is the list of `environment_vars`, if configured. +* `inline_shebang` (string) - The + [shebang](http://en.wikipedia.org/wiki/Shebang_(Unix)) value to use when + running commands specified by `inline`. By default, this is `/bin/sh`. + If you're not using `inline`, then this configuration has no effect. + * `remote_path` (string) - The path where the script will be uploaded to in the machine. This defaults to "/tmp/script.sh". This value must be a writable location and any parent directories must already exist. From 27bada1ba9a15d7af65ade7a69e505fea57c58f9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 7 Jul 2013 18:04:30 -0700 Subject: [PATCH 34/55] builder/virtualbox: whitespace --- builder/virtualbox/step_create_vm.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/builder/virtualbox/step_create_vm.go b/builder/virtualbox/step_create_vm.go index 3e21c5bd7..6a8d579ef 100644 --- a/builder/virtualbox/step_create_vm.go +++ b/builder/virtualbox/step_create_vm.go @@ -22,7 +22,10 @@ func (s *stepCreateVM) Run(state map[string]interface{}) multistep.StepAction { name := config.VMName commands := make([][]string, 4) - commands[0] = []string{"createvm", "--name", name, "--ostype", config.GuestOSType, "--register"} + commands[0] = []string{ + "createvm", "--name", name, + "--ostype", config.GuestOSType, "--register", + } commands[1] = []string{ "modifyvm", name, "--boot1", "disk", "--boot2", "dvd", "--boot3", "none", "--boot4", "none", From 65d74f90c193f345edc4bc03c918130611e9f178 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 7 Jul 2013 20:37:43 -0700 Subject: [PATCH 35/55] builder/amazonebs: retry SSH handshakes [GH-130] --- CHANGELOG.md | 1 + builder/amazonebs/step_connect_ssh.go | 206 +++++++++++++++----------- 2 files changed, 117 insertions(+), 90 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e8a6f269..8f73e247e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ BUG FIXES: * core: Non-200 response codes on downloads now show proper errors. [GH-141] +* amazon-ebs: SSH handshake is retried. [GH-130] * vagrant: The `BuildName` template propery works properly in the output path. * vagrant: Properly configure the provider-specific post-processors so diff --git a/builder/amazonebs/step_connect_ssh.go b/builder/amazonebs/step_connect_ssh.go index 770ec27c2..903439341 100644 --- a/builder/amazonebs/step_connect_ssh.go +++ b/builder/amazonebs/step_connect_ssh.go @@ -14,10 +14,64 @@ import ( ) type stepConnectSSH struct { - conn net.Conn + cancel bool + conn net.Conn } func (s *stepConnectSSH) Run(state map[string]interface{}) multistep.StepAction { + config := state["config"].(config) + ui := state["ui"].(packer.Ui) + + var comm packer.Communicator + var err error + + waitDone := make(chan bool, 1) + go func() { + comm, err = s.waitForSSH(state) + waitDone <- true + }() + + log.Printf("Waiting for SSH, up to timeout: %s", config.SSHTimeout.String()) + + timeout := time.After(config.SSHTimeout) +WaitLoop: + for { + // Wait for either SSH to become available, a timeout to occur, + // or an interrupt to come through. + select { + case <-waitDone: + if err != nil { + ui.Error(fmt.Sprintf("Error waiting for SSH: %s", err)) + return multistep.ActionHalt + } + + state["communicator"] = comm + break WaitLoop + case <-timeout: + ui.Error("Timeout waiting for SSH.") + s.cancel = true + return multistep.ActionHalt + case <-time.After(1 * time.Second): + if _, ok := state[multistep.StateCancelled]; ok { + log.Println("Interrupt detected, quitting waiting for SSH.") + return multistep.ActionHalt + } + } + } + + return multistep.ActionContinue +} + +func (s *stepConnectSSH) Cleanup(map[string]interface{}) { + if s.conn != nil { + s.conn.Close() + s.conn = nil + } +} + +// This blocks until SSH becomes available, and sends the communicator +// on the given channel. +func (s *stepConnectSSH) waitForSSH(state map[string]interface{}) (packer.Communicator, error) { config := state["config"].(config) instance := state["instance"].(*ec2.Instance) privateKey := state["privateKey"].(string) @@ -28,98 +82,70 @@ func (s *stepConnectSSH) Run(state map[string]interface{}) multistep.StepAction keyring := &ssh.SimpleKeychain{} err := keyring.AddPEMKey(privateKey) if err != nil { - err := fmt.Errorf("Error setting up SSH config: %s", err) - state["error"] = err - ui.Error(err.Error()) - return multistep.ActionHalt - } - - // Build the actual SSH client configuration - sshConfig := &gossh.ClientConfig{ - User: config.SSHUsername, - Auth: []gossh.ClientAuth{ - gossh.ClientAuthKeyring(keyring), - }, - } - - // Start trying to connect to SSH - connected := make(chan bool, 1) - connectQuit := make(chan bool, 1) - defer func() { - connectQuit <- true - }() - - go func() { - var err error - - ui.Say("Connecting to the instance via SSH...") - attempts := 0 - for { - select { - case <-connectQuit: - return - default: - } - - attempts += 1 - log.Printf( - "Opening TCP conn for SSH to %s:%d (attempt %d)", - instance.DNSName, config.SSHPort, attempts) - s.conn, err = net.Dial("tcp", fmt.Sprintf("%s:%d", instance.DNSName, config.SSHPort)) - if err == nil { - break - } - - // A brief sleep so we're not being overly zealous attempting - // to connect to the instance. - time.Sleep(500 * time.Millisecond) - } - - connected <- true - }() - - log.Printf("Waiting up to %s for SSH connection", config.SSHTimeout) - timeout := time.After(config.SSHTimeout) - -ConnectWaitLoop: - for { - select { - case <-connected: - // We connected. Just break the loop. - break ConnectWaitLoop - case <-timeout: - err := errors.New("Timeout waiting for SSH to become available.") - state["error"] = err - ui.Error(err.Error()) - return multistep.ActionHalt - case <-time.After(1 * time.Second): - if _, ok := state[multistep.StateCancelled]; ok { - log.Println("Interrupt detected, quitting waiting for SSH.") - return multistep.ActionHalt - } - } + return nil, fmt.Errorf("Error setting up SSH config: %s", err) } + ui.Say("Waiting for SSH to become available...") var comm packer.Communicator - if err == nil { - comm, err = ssh.New(s.conn, sshConfig) + var nc net.Conn + for { + if nc != nil { + nc.Close() + } + + time.Sleep(5 * time.Second) + + if s.cancel { + log.Println("SSH wait cancelled. Exiting loop.") + return nil, errors.New("SSH wait cancelled") + } + + // Attempt to connect to SSH port + log.Printf( + "Opening TCP conn for SSH to %s:%d", + instance.DNSName, config.SSHPort) + nc, err := net.Dial("tcp", + fmt.Sprintf("%s:%d", instance.DNSName, config.SSHPort)) + if err != nil { + log.Printf("TCP connection to SSH ip/port failed: %s", err) + continue + } + + // Build the actual SSH client configuration + sshConfig := &gossh.ClientConfig{ + User: config.SSHUsername, + Auth: []gossh.ClientAuth{ + gossh.ClientAuthKeyring(keyring), + }, + } + + sshConnectSuccess := make(chan bool, 1) + go func() { + comm, err = ssh.New(nc, sshConfig) + if err != nil { + log.Printf("SSH connection fail: %s", err) + sshConnectSuccess <- false + return + } + + sshConnectSuccess <- true + }() + + select { + case success := <-sshConnectSuccess: + if !success { + continue + } + case <-time.After(5 * time.Second): + log.Printf("SSH handshake timeout. Trying again.") + continue + } + + ui.Say("Connected via SSH!") + break } - if err != nil { - err := fmt.Errorf("Error connecting to SSH: %s", err) - state["error"] = err - ui.Error(err.Error()) - return multistep.ActionHalt - } - - // Set the communicator on the state bag so it can be used later - state["communicator"] = comm - - return multistep.ActionContinue -} - -func (s *stepConnectSSH) Cleanup(map[string]interface{}) { - if s.conn != nil { - s.conn.Close() - } + // Store the connection so we can close it later + s.conn = nc + return comm, nil } From ee1ad5fa568a7a6c85d233e494f798c4d1ea7471 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 7 Jul 2013 20:50:53 -0700 Subject: [PATCH 36/55] provisioner/shell: close source script file handle --- provisioner/shell/provisioner.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/provisioner/shell/provisioner.go b/provisioner/shell/provisioner.go index f027c5944..3d7571c0d 100644 --- a/provisioner/shell/provisioner.go +++ b/provisioner/shell/provisioner.go @@ -165,6 +165,7 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { if err != nil { return fmt.Errorf("Error opening shell script: %s", err) } + defer f.Close() log.Printf("Uploading %s => %s", path, p.config.RemotePath) err = comm.Upload(p.config.RemotePath, f) @@ -172,6 +173,9 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { return fmt.Errorf("Error uploading shell script: %s", err) } + // Close the original file since we copied it + f.Close() + // Flatten the environment variables flattendVars := strings.Join(p.config.Vars, " ") From ef1ec3708b7a3f8edb993a9864cd5ada404b87f1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 7 Jul 2013 20:59:43 -0700 Subject: [PATCH 37/55] packer/rpc: Properly close net.conn when remote process ends --- packer/rpc/communicator.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packer/rpc/communicator.go b/packer/rpc/communicator.go index 46a098af9..cd25b7e5d 100644 --- a/packer/rpc/communicator.go +++ b/packer/rpc/communicator.go @@ -153,12 +153,14 @@ func (c *CommunicatorServer) Start(args *CommunicatorStartArgs, reply *interface var cmd packer.RemoteCmd cmd.Command = args.Command + toClose := make([]net.Conn, 0) if args.StdinAddress != "" { stdinC, err := net.Dial("tcp", args.StdinAddress) if err != nil { return err } + toClose = append(toClose, stdinC) cmd.Stdin = stdinC } @@ -168,6 +170,7 @@ func (c *CommunicatorServer) Start(args *CommunicatorStartArgs, reply *interface return err } + toClose = append(toClose, stdoutC) cmd.Stdout = stdoutC } @@ -177,6 +180,7 @@ func (c *CommunicatorServer) Start(args *CommunicatorStartArgs, reply *interface return err } + toClose = append(toClose, stderrC) cmd.Stderr = stderrC } @@ -196,6 +200,9 @@ func (c *CommunicatorServer) Start(args *CommunicatorStartArgs, reply *interface // exit. When it does, report it back to caller... go func() { defer responseC.Close() + for _, conn := range toClose { + defer conn.Close() + } for !cmd.Exited { time.Sleep(50 * time.Millisecond) From 3adc5e03d7788a9600e35ae4e44ebe56958e0210 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 7 Jul 2013 21:09:47 -0700 Subject: [PATCH 38/55] v0.1.5 --- CHANGELOG.md | 6 +++--- packer/version.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f73e247e..86285ddbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,9 @@ -## 0.1.5 (unreleased) +## 0.1.5 (July 7, 2013) FEATURES: -* "file" uploader will upload files and directories from the machine - running Packer to the remote machine. +* "file" uploader will upload files from the machine running Packer to the + remote machine. * VirtualBox guest additions URL and checksum can now be specified, allowing the VirtualBox builder to have the ability to be used completely offline. diff --git a/packer/version.go b/packer/version.go index c54d66d1f..766052a9a 100644 --- a/packer/version.go +++ b/packer/version.go @@ -11,7 +11,7 @@ const Version = "0.1.5" // Any pre-release marker for the version. If this is "" (empty string), // then it means that it is a final release. Otherwise, this is the // pre-release marker. -const VersionPrerelease = "dev" +const VersionPrerelease = "" type versionCommand byte From 845349b5a99cc511e39559fd3298af49f4220002 Mon Sep 17 00:00:00 2001 From: Bernard McKeever Date: Mon, 8 Jul 2013 11:09:16 +0100 Subject: [PATCH 39/55] Making sense of the sentence The sentence was missing a few words. I've made it read similar to the previous 3 sentences. --- website/source/docs/templates/introduction.html.markdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/source/docs/templates/introduction.html.markdown b/website/source/docs/templates/introduction.html.markdown index 8a2213675..a1674dcff 100644 --- a/website/source/docs/templates/introduction.html.markdown +++ b/website/source/docs/templates/introduction.html.markdown @@ -33,8 +33,8 @@ Along with each key, it is noted whether it is required or not. information on how to define and configure a provisioner, read the sub-section on [configuring provisioners in templates](/docs/templates/provisioners.html). -* `post-processors` (optional) is an array of that defines the various - post-processing steps to take with the built images. This is an optional +* `post-processors` (optional) is an array of one or more objects that defines the + various post-processing steps to take with the built images. This is an optional field. If not specified, then no post-processing will be done. For more information on what post-processors do and how they're defined, read the sub-section on [configuring post-processors in templates](/docs/templates/post-processors.html). From d4e8321a80c59df9366694d39e1e06def96d2be3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 8 Jul 2013 15:17:09 -0700 Subject: [PATCH 40/55] Up version for dev --- CHANGELOG.md | 4 ++++ packer/version.go | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86285ddbc..5fa0cba94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.6 (unreleased) + + + ## 0.1.5 (July 7, 2013) FEATURES: diff --git a/packer/version.go b/packer/version.go index 766052a9a..54b98da0b 100644 --- a/packer/version.go +++ b/packer/version.go @@ -6,12 +6,12 @@ import ( ) // The version of packer. -const Version = "0.1.5" +const Version = "0.1.6" // Any pre-release marker for the version. If this is "" (empty string), // then it means that it is a final release. Otherwise, this is the // pre-release marker. -const VersionPrerelease = "" +const VersionPrerelease = "dev" type versionCommand byte From 3b32ba9f11a736fb0fcf9e58035f26f5663fb9d7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 8 Jul 2013 15:37:01 -0700 Subject: [PATCH 41/55] scripts: Add git commit to builds --- packer.go | 4 +++- packer/version.go | 8 ++++++++ scripts/build.sh | 16 ++++++++++++++-- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/packer.go b/packer.go index 4fc3cf138..d5fbe7439 100644 --- a/packer.go +++ b/packer.go @@ -27,7 +27,9 @@ func main() { runtime.GOMAXPROCS(runtime.NumCPU()) } - log.Printf("Packer Version: %s %s", packer.Version, packer.VersionPrerelease) + log.Printf( + "Packer Version: %s %s %s", + packer.Version, packer.VersionPrerelease, packer.GitCommit) log.Printf("Packer Target OS/Arch: %s %s", runtime.GOOS, runtime.GOARCH) config, err := loadConfig() diff --git a/packer/version.go b/packer/version.go index 54b98da0b..e351a9fa1 100644 --- a/packer/version.go +++ b/packer/version.go @@ -5,6 +5,10 @@ import ( "fmt" ) +// The git commit that is being compiled. This will be filled in by the +// compiler for source builds. +var GitCommit string + // The version of packer. const Version = "0.1.6" @@ -29,6 +33,10 @@ func (versionCommand) Run(env Environment, args []string) int { fmt.Fprintf(&versionString, ".%s", VersionPrerelease) } + if GitCommit != "" { + fmt.Fprintf(&versionString, " (%s)", GitCommit) + } + env.Ui().Say(versionString.String()) return 0 } diff --git a/scripts/build.sh b/scripts/build.sh index 148082a9c..439231c1f 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,4 +1,6 @@ #!/bin/bash +# +# This script only builds the application from source. set -e NO_COLOR="\x1b[0m" @@ -14,13 +16,23 @@ DIR="$( cd -P "$( dirname "$SOURCE" )/.." && pwd )" # Change into that directory cd $DIR +# Get the git commit +GIT_COMMIT=$(git rev-parse --short HEAD) +GIT_DIRTY=$(test -n "`git status --porcelain`" && echo "+CHANGES") + # Compile the main Packer app echo -e "${OK_COLOR}--> Compiling Packer${NO_COLOR}" -go build -v -o bin/packer . +go build \ + -ldflags "-X github.com/mitchellh/packer/packer.GitCommit ${GIT_COMMIT}${GIT_DIRTY}" \ + -v \ + -o bin/packer . # Go over each plugin and build it for PLUGIN in $(find ./plugin -mindepth 1 -maxdepth 1 -type d); do PLUGIN_NAME=$(basename ${PLUGIN}) echo -e "${OK_COLOR}--> Compiling Plugin: ${PLUGIN_NAME}${NO_COLOR}" - go build -v -o bin/packer-${PLUGIN_NAME} ${PLUGIN} + go build \ + -ldflags "-X github.com/mitchellh/packer/packer.GitCommit ${GIT_COMMIT}${GIT_DIRTY}" \ + -v \ + -o bin/packer-${PLUGIN_NAME} ${PLUGIN} done From 207c15b91bb20bad33987812616e3d21bf948bb3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 8 Jul 2013 15:38:14 -0700 Subject: [PATCH 42/55] packer: only show git commit in version output for prerelease --- packer/version.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packer/version.go b/packer/version.go index e351a9fa1..36d927a30 100644 --- a/packer/version.go +++ b/packer/version.go @@ -31,10 +31,10 @@ func (versionCommand) Run(env Environment, args []string) int { fmt.Fprintf(&versionString, "Packer v%s", Version) if VersionPrerelease != "" { fmt.Fprintf(&versionString, ".%s", VersionPrerelease) - } - if GitCommit != "" { - fmt.Fprintf(&versionString, " (%s)", GitCommit) + if GitCommit != "" { + fmt.Fprintf(&versionString, " (%s)", GitCommit) + } } env.Ui().Say(versionString.String()) From c80b1bb75d88da97a9c8cb486f0cb4610080851d Mon Sep 17 00:00:00 2001 From: Steven Merrill Date: Tue, 9 Jul 2013 01:24:19 -0400 Subject: [PATCH 43/55] Don't allow a dirty Git tree to fail the make command. --- scripts/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build.sh b/scripts/build.sh index 439231c1f..9877ffa38 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -18,7 +18,7 @@ cd $DIR # Get the git commit GIT_COMMIT=$(git rev-parse --short HEAD) -GIT_DIRTY=$(test -n "`git status --porcelain`" && echo "+CHANGES") +GIT_DIRTY=$(test -n "`git status --porcelain`" && echo "+CHANGES" || true) # Compile the main Packer app echo -e "${OK_COLOR}--> Compiling Packer${NO_COLOR}" From bd937ec8f22fb31ac1bd68364b35d8ac3df85b4a Mon Sep 17 00:00:00 2001 From: Radu Voicilas Date: Tue, 9 Jul 2013 09:28:07 +0300 Subject: [PATCH 44/55] Fix typo --- packer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packer.go b/packer.go index d5fbe7439..04acf0ff9 100644 --- a/packer.go +++ b/packer.go @@ -102,7 +102,7 @@ func loadConfig() (*config, error) { mustExist = false if err != nil { - log.Printf("Error detecing default config file path: %s", err) + log.Printf("Error detecting default config file path: %s", err) } } From 70f63492f9424d211f9fe7b0a5f2c7a7fe977482 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 8 Jul 2013 20:29:38 -0700 Subject: [PATCH 45/55] builder/common: step to create floppy disks --- builder/common/step_create_floppy.go | 126 +++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 builder/common/step_create_floppy.go diff --git a/builder/common/step_create_floppy.go b/builder/common/step_create_floppy.go new file mode 100644 index 000000000..ee99e9f5d --- /dev/null +++ b/builder/common/step_create_floppy.go @@ -0,0 +1,126 @@ +package common + +import ( + "fmt" + "github.com/mitchellh/go-fs" + "github.com/mitchellh/go-fs/fat" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "io" + "io/ioutil" + "log" + "os" + "path/filepath" +) + +// StepCreateFloppy will create a floppy disk with the given files. +// The floppy disk doesn't support sub-directories. Only files at the +// root level are supported. +type StepCreateFloppy struct { + Files []string +} + +func (s *StepCreateFloppy) Run(state map[string]interface{}) multistep.StepAction { + if len(s.Files) == 0 { + log.Println("No floppy files specified. Floppy disk will not be made.") + return multistep.ActionContinue + } + + ui := state["ui"].(packer.Ui) + ui.Say("Creating floppy disk...") + + // Create a temporary file to be our floppy drive + floppyF, err := ioutil.TempFile("", "packer") + if err != nil { + state["error"] = fmt.Errorf("Error creating temporary file for floppy: %s", err) + return multistep.ActionHalt + } + defer floppyF.Close() + + log.Printf("Floppy path: %s", floppyF.Name()) + + // Set the size of the file to be a floppy sized + if err := floppyF.Truncate(1440 * 1024); err != nil { + state["error"] = fmt.Errorf("Error creating floppy: %s", err) + return multistep.ActionHalt + } + + // BlockDevice backed by the file for our filesystem + log.Println("Initializing block device backed by temporary file") + device, err := fs.NewFileDisk(floppyF) + if err != nil { + state["error"] = fmt.Errorf("Error creating floppy: %s", err) + return multistep.ActionHalt + } + + // Format the block device so it contains a valid FAT filesystem + log.Println("Formatting the block device with a FAT filesystem...") + formatConfig := &fat.SuperFloppyConfig{ + FATType: fat.FAT12, + Label: "packer", + OEMName: "packer", + } + if fat.FormatSuperFloppy(device, formatConfig); err != nil { + state["error"] = fmt.Errorf("Error creating floppy: %s", err) + return multistep.ActionHalt + } + + // The actual FAT filesystem + log.Println("Initializing FAT filesystem on block device") + fatFs, err := fat.New(device) + if err != nil { + state["error"] = fmt.Errorf("Error creating floppy: %s", err) + return multistep.ActionHalt + } + + // Get the root directory to the filesystem + log.Println("Reading the root directory from the filesystem") + rootDir, err := fatFs.RootDir() + if err != nil { + state["error"] = fmt.Errorf("Error creating floppy: %s", err) + return multistep.ActionHalt + } + + // Go over each file and copy it. + for _, filename := range s.Files { + ui.Message(fmt.Sprintf("Copying: %s", filepath.Base(filename))) + if s.addSingleFile(rootDir, filename); err != nil { + state["error"] = fmt.Errorf("Error adding file to floppy: %s", err) + return multistep.ActionHalt + } + } + + // Set the path to the floppy so it can be used later + state["floppy_path"] = floppyF.Name() + + return multistep.ActionHalt +} + +func (s *StepCreateFloppy) Cleanup(map[string]interface{}) { +} + +func (s *StepCreateFloppy) addSingleFile(dir fs.Directory, src string) error { + log.Printf("Adding file to floppy: %s", src) + + inputF, err := os.Open(src) + if err != nil { + return err + } + defer inputF.Close() + + entry, err := dir.AddFile(filepath.Base(src)) + if err != nil { + return err + } + + fatFile, err := entry.File() + if err != nil { + return err + } + + if _, err := io.Copy(fatFile, inputF); err != nil { + return err + } + + return nil +} From cae89a49a16e080dfac00484a94cce76d67ebc33 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 8 Jul 2013 20:56:23 -0700 Subject: [PATCH 46/55] builder/vmware: support floppy_files for mounting a floppy disk --- builder/common/step_create_floppy.go | 11 ++++- builder/vmware/builder.go | 9 ++++ builder/vmware/builder_test.go | 28 +++++++++++ builder/vmware/step_clean_vmx.go | 72 ++++++++++++++++++++++++++++ builder/vmware/step_create_vmx.go | 15 ++++-- 5 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 builder/vmware/step_clean_vmx.go diff --git a/builder/common/step_create_floppy.go b/builder/common/step_create_floppy.go index ee99e9f5d..9e0bf4e33 100644 --- a/builder/common/step_create_floppy.go +++ b/builder/common/step_create_floppy.go @@ -18,6 +18,8 @@ import ( // root level are supported. type StepCreateFloppy struct { Files []string + + floppyPath string } func (s *StepCreateFloppy) Run(state map[string]interface{}) multistep.StepAction { @@ -37,6 +39,9 @@ func (s *StepCreateFloppy) Run(state map[string]interface{}) multistep.StepActio } defer floppyF.Close() + // Set the path so we can remove it later + s.floppyPath = floppyF.Name() + log.Printf("Floppy path: %s", floppyF.Name()) // Set the size of the file to be a floppy sized @@ -91,12 +96,16 @@ func (s *StepCreateFloppy) Run(state map[string]interface{}) multistep.StepActio } // Set the path to the floppy so it can be used later - state["floppy_path"] = floppyF.Name() + state["floppy_path"] = s.floppyPath return multistep.ActionHalt } func (s *StepCreateFloppy) Cleanup(map[string]interface{}) { + if s.floppyPath != "" { + log.Printf("Deleting floppy disk: %s", s.floppyPath) + os.Remove(s.floppyPath) + } } func (s *StepCreateFloppy) addSingleFile(dir fs.Directory, src string) error { diff --git a/builder/vmware/builder.go b/builder/vmware/builder.go index e48103c73..708f3c988 100644 --- a/builder/vmware/builder.go +++ b/builder/vmware/builder.go @@ -28,6 +28,7 @@ type Builder struct { type config struct { DiskName string `mapstructure:"vmdk_name"` DiskSize uint `mapstructure:"disk_size"` + FloppyFiles []string `mapstructure:"floppy_files"` GuestOSType string `mapstructure:"guest_os_type"` ISOMD5 string `mapstructure:"iso_md5"` ISOUrl string `mapstructure:"iso_url"` @@ -76,6 +77,10 @@ func (b *Builder) Prepare(raws ...interface{}) error { b.config.DiskSize = 40000 } + if b.config.FloppyFiles == nil { + b.config.FloppyFiles = make([]string, 0) + } + if b.config.GuestOSType == "" { b.config.GuestOSType = "other" } @@ -230,6 +235,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe &stepPrepareTools{}, &stepDownloadISO{}, &stepPrepareOutputDir{}, + &common.StepCreateFloppy{ + Files: b.config.FloppyFiles, + }, &stepCreateDisk{}, &stepCreateVMX{}, &stepHTTPServer{}, @@ -241,6 +249,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe &stepProvision{}, &stepShutdown{}, &stepCleanFiles{}, + &stepCleanVMX{}, &stepCompactDisk{}, } diff --git a/builder/vmware/builder_test.go b/builder/vmware/builder_test.go index 8d32636d8..0cf8a42b1 100644 --- a/builder/vmware/builder_test.go +++ b/builder/vmware/builder_test.go @@ -4,6 +4,7 @@ import ( "github.com/mitchellh/packer/packer" "io/ioutil" "os" + "reflect" "testing" "time" ) @@ -107,6 +108,33 @@ func TestBuilderPrepare_DiskSize(t *testing.T) { } } +func TestBuilderPrepare_FloppyFiles(t *testing.T) { + var b Builder + config := testConfig() + + delete(config, "floppy_files") + err := b.Prepare(config) + if err != nil { + t.Fatalf("bad err: %s", err) + } + + if len(b.config.FloppyFiles) != 0 { + t.Fatalf("bad: %#v", b.config.FloppyFiles) + } + + config["floppy_files"] = []string{"foo", "bar"} + b = Builder{} + err = b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + expected := []string{"foo", "bar"} + if !reflect.DeepEqual(b.config.FloppyFiles, expected) { + t.Fatalf("bad: %#v", b.config.FloppyFiles) + } +} + func TestBuilderPrepare_HTTPPort(t *testing.T) { var b Builder config := testConfig() diff --git a/builder/vmware/step_clean_vmx.go b/builder/vmware/step_clean_vmx.go new file mode 100644 index 000000000..595b90e01 --- /dev/null +++ b/builder/vmware/step_clean_vmx.go @@ -0,0 +1,72 @@ +package vmware + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "io/ioutil" + "log" + "os" + "strings" +) + +// This step cleans up the VMX by removing or changing this prior to +// being ready for use. +// +// Uses: +// ui packer.Ui +// vmx_path string +// +// Produces: +// +type stepCleanVMX struct{} + +func (s stepCleanVMX) Run(state map[string]interface{}) multistep.StepAction { + if _, ok := state["floppy_path"]; !ok { + return multistep.ActionContinue + } + + ui := state["ui"].(packer.Ui) + vmxPath := state["vmx_path"].(string) + + vmxData, err := s.readVMX(vmxPath) + if err != nil { + state["error"] = fmt.Errorf("Error reading VMX: %s", err) + return multistep.ActionHalt + } + + // Delete the floppy0 entries so the floppy is no longer mounted + ui.Say("Unmounting floppy from VMX...") + for k, _ := range vmxData { + if strings.HasPrefix(k, "floppy0.") { + log.Printf("Deleting key: %s", k) + delete(vmxData, k) + } + } + vmxData["floppy0.present"] = "FALSE" + + // Rewrite the VMX + if err := WriteVMX(vmxPath, vmxData); err != nil { + state["error"] = fmt.Errorf("Error writing VMX: %s", err) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (stepCleanVMX) Cleanup(map[string]interface{}) {} + +func (stepCleanVMX) readVMX(vmxPath string) (map[string]string, error) { + vmxF, err := os.Open(vmxPath) + if err != nil { + return nil, err + } + defer vmxF.Close() + + vmxBytes, err := ioutil.ReadAll(vmxF) + if err != nil { + return nil, err + } + + return ParseVMX(string(vmxBytes)), nil +} diff --git a/builder/vmware/step_create_vmx.go b/builder/vmware/step_create_vmx.go index 56e1962ce..8165fb36b 100644 --- a/builder/vmware/step_create_vmx.go +++ b/builder/vmware/step_create_vmx.go @@ -36,10 +36,10 @@ func (stepCreateVMX) Run(state map[string]interface{}) multistep.StepAction { ui.Say("Building and writing VMX file") tplData := &vmxTemplateData{ - config.VMName, - config.GuestOSType, - config.DiskName, - isoPath, + Name: config.VMName, + GuestOS: config.GuestOSType, + DiskName: config.DiskName, + ISOPath: isoPath, } var buf bytes.Buffer @@ -55,6 +55,13 @@ func (stepCreateVMX) Run(state map[string]interface{}) multistep.StepAction { } } + if floppyPathRaw, ok := state["floppy_path"]; ok { + log.Println("Floppy path present, setting in VMX") + vmxData["floppy0.present"] = "TRUE" + vmxData["floppy0.fileType"] = "file" + vmxData["floppy0.fileName"] = floppyPathRaw.(string) + } + vmxPath := filepath.Join(config.OutputDir, config.VMName+".vmx") if err := WriteVMX(vmxPath, vmxData); err != nil { err := fmt.Errorf("Error creating VMX file: %s", err) From 24fad7d443f70642b926da48bd6cf0e6f3726cda Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 8 Jul 2013 20:58:09 -0700 Subject: [PATCH 47/55] script: build completes --- scripts/build.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/build.sh b/scripts/build.sh index 9877ffa38..15d917b85 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,8 +1,6 @@ #!/bin/bash # # This script only builds the application from source. -set -e - NO_COLOR="\x1b[0m" OK_COLOR="\x1b[32;01m" ERROR_COLOR="\x1b[31;01m" From 65b00775e8e8affc52a07b21f51df93fcd707abb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 8 Jul 2013 21:24:09 -0700 Subject: [PATCH 48/55] builder/common: continue after creating floppy --- builder/common/step_create_floppy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builder/common/step_create_floppy.go b/builder/common/step_create_floppy.go index 9e0bf4e33..9909811fe 100644 --- a/builder/common/step_create_floppy.go +++ b/builder/common/step_create_floppy.go @@ -98,7 +98,7 @@ func (s *StepCreateFloppy) Run(state map[string]interface{}) multistep.StepActio // Set the path to the floppy so it can be used later state["floppy_path"] = s.floppyPath - return multistep.ActionHalt + return multistep.ActionContinue } func (s *StepCreateFloppy) Cleanup(map[string]interface{}) { From eaa6c9fec140abb0251558842b4a687767c231ff Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 9 Jul 2013 11:21:42 -0700 Subject: [PATCH 49/55] packer: Only trim whitespace on the right of prefixed UI --- packer/ui.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packer/ui.go b/packer/ui.go index 271f3a219..9d06fed93 100644 --- a/packer/ui.go +++ b/packer/ui.go @@ -10,6 +10,7 @@ import ( "os/signal" "strings" "sync" + "unicode" ) type UiColor uint @@ -110,7 +111,7 @@ func (u *PrefixedUi) prefixLines(prefix, message string) string { result.WriteString(fmt.Sprintf("%s: %s\n", prefix, line)) } - return strings.TrimSpace(result.String()) + return strings.TrimRightFunc(result.String(), unicode.IsSpace) } func (rw *ReaderWriterUi) Ask(query string) (string, error) { From efc9e9db6e07dd30c57e2d73b9a95568b086a722 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 9 Jul 2013 12:29:40 -0700 Subject: [PATCH 50/55] builder/virtualbox: support floppy_files --- builder/virtualbox/builder.go | 9 ++ builder/virtualbox/builder_test.go | 27 +++++ builder/virtualbox/step_attach_floppy.go | 129 +++++++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 builder/virtualbox/step_attach_floppy.go diff --git a/builder/virtualbox/builder.go b/builder/virtualbox/builder.go index d0860331e..e45286a77 100644 --- a/builder/virtualbox/builder.go +++ b/builder/virtualbox/builder.go @@ -28,6 +28,7 @@ type config struct { BootCommand []string `mapstructure:"boot_command"` BootWait time.Duration `` DiskSize uint `mapstructure:"disk_size"` + FloppyFiles []string `mapstructure:"floppy_files"` GuestAdditionsPath string `mapstructure:"guest_additions_path"` GuestAdditionsURL string `mapstructure:"guest_additions_url"` GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"` @@ -73,6 +74,10 @@ func (b *Builder) Prepare(raws ...interface{}) error { b.config.DiskSize = 40000 } + if b.config.FloppyFiles == nil { + b.config.FloppyFiles = make([]string, 0) + } + if b.config.GuestAdditionsPath == "" { b.config.GuestAdditionsPath = "VBoxGuestAdditions.iso" } @@ -265,11 +270,15 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe new(stepDownloadGuestAdditions), new(stepDownloadISO), new(stepPrepareOutputDir), + &common.StepCreateFloppy{ + Files: b.config.FloppyFiles, + }, new(stepHTTPServer), new(stepSuppressMessages), new(stepCreateVM), new(stepCreateDisk), new(stepAttachISO), + new(stepAttachFloppy), new(stepForwardSSH), new(stepVBoxManage), new(stepRun), diff --git a/builder/virtualbox/builder_test.go b/builder/virtualbox/builder_test.go index 655ce57ad..46f9c8997 100644 --- a/builder/virtualbox/builder_test.go +++ b/builder/virtualbox/builder_test.go @@ -115,6 +115,33 @@ func TestBuilderPrepare_DiskSize(t *testing.T) { } } +func TestBuilderPrepare_FloppyFiles(t *testing.T) { + var b Builder + config := testConfig() + + delete(config, "floppy_files") + err := b.Prepare(config) + if err != nil { + t.Fatalf("bad err: %s", err) + } + + if len(b.config.FloppyFiles) != 0 { + t.Fatalf("bad: %#v", b.config.FloppyFiles) + } + + config["floppy_files"] = []string{"foo", "bar"} + b = Builder{} + err = b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + expected := []string{"foo", "bar"} + if !reflect.DeepEqual(b.config.FloppyFiles, expected) { + t.Fatalf("bad: %#v", b.config.FloppyFiles) + } +} + func TestBuilderPrepare_GuestAdditionsPath(t *testing.T) { var b Builder config := testConfig() diff --git a/builder/virtualbox/step_attach_floppy.go b/builder/virtualbox/step_attach_floppy.go new file mode 100644 index 000000000..be7394160 --- /dev/null +++ b/builder/virtualbox/step_attach_floppy.go @@ -0,0 +1,129 @@ +package virtualbox + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "io" + "io/ioutil" + "log" + "os" + "path/filepath" +) + +// This step attaches the ISO to the virtual machine. +// +// Uses: +// +// Produces: +type stepAttachFloppy struct { + floppyPath string +} + +func (s *stepAttachFloppy) Run(state map[string]interface{}) multistep.StepAction { + // Determine if we even have a floppy disk to attach + var floppyPath string + if floppyPathRaw, ok := state["floppy_path"]; ok { + floppyPath = floppyPathRaw.(string) + } else { + log.Println("No floppy disk, not attaching.") + return multistep.ActionContinue + } + + // VirtualBox is really dumb and can't figure out the format of the file + // without an extension, so we need to add the "vfd" extension to the + // floppy. + floppyPath, err := s.copyFloppy(floppyPath) + if err != nil { + state["error"] = fmt.Errorf("Error preparing floppy: %s", err) + return multistep.ActionHalt + } + + driver := state["driver"].(Driver) + ui := state["ui"].(packer.Ui) + vmName := state["vmName"].(string) + + ui.Say("Attaching floppy disk...") + + // Create the floppy disk controller + command := []string{ + "storagectl", vmName, + "--name", "Floppy Controller", + "--add", "floppy", + } + if err := driver.VBoxManage(command...); err != nil { + state["error"] = fmt.Errorf("Error creating floppy controller: %s", err) + return multistep.ActionHalt + } + + // Attach the floppy to the controller + command = []string{ + "storageattach", vmName, + "--storagectl", "Floppy Controller", + "--port", "0", + "--device", "0", + "--type", "fdd", + "--medium", floppyPath, + } + if err := driver.VBoxManage(command...); err != nil { + state["error"] = fmt.Errorf("Error attaching floppy: %s", err) + return multistep.ActionHalt + } + + // Track the path so that we can unregister it from VirtualBox later + s.floppyPath = floppyPath + + return multistep.ActionContinue +} + +func (s *stepAttachFloppy) Cleanup(state map[string]interface{}) { + if s.floppyPath == "" { + return + } + + // Delete the floppy disk + defer os.Remove(s.floppyPath) + + driver := state["driver"].(Driver) + ui := state["ui"].(packer.Ui) + vmName := state["vmName"].(string) + + command := []string{ + "storageattach", vmName, + "--storagectl", "Floppy Controller", + "--port", "0", + "--device", "0", + "--medium", "none", + } + + if err := driver.VBoxManage(command...); err != nil { + ui.Error(fmt.Sprintf("Error unregistering floppy: %s", err)) + } +} + +func (s *stepAttachFloppy) copyFloppy(path string) (string, error) { + tempdir, err := ioutil.TempDir("", "packer") + if err != nil { + return "", err + } + + floppyPath := filepath.Join(tempdir, "floppy.vfd") + f, err := os.Create(floppyPath) + if err != nil { + return "", err + } + defer f.Close() + + sourceF, err := os.Open(path) + if err != nil { + return "", err + } + defer sourceF.Close() + + log.Printf("Copying floppy to temp location: %s", floppyPath) + if _, err := io.Copy(f, sourceF); err != nil { + return "", err + } + + return floppyPath, nil +} From c9dc998b0e9fdf627a4f015ccaa9838545e1baa4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 9 Jul 2013 12:34:10 -0700 Subject: [PATCH 51/55] website: document the floppy_files feature --- website/source/docs/builders/virtualbox.html.markdown | 7 +++++++ website/source/docs/builders/vmware.html.markdown | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/website/source/docs/builders/virtualbox.html.markdown b/website/source/docs/builders/virtualbox.html.markdown index 1f6d42f6b..03cd23ced 100644 --- a/website/source/docs/builders/virtualbox.html.markdown +++ b/website/source/docs/builders/virtualbox.html.markdown @@ -70,6 +70,13 @@ Optional: * `disk_size` (int) - The size, in megabytes, of the hard disk to create for the VM. By default, this is 40000 (40 GB). +* `floppy_files` (array of strings) - A list of files to put onto a floppy + disk that is attached when the VM is booted for the first time. This is + most useful for unattended Windows installs, which look for an + `Autounattend.xml` file on removable media. By default no floppy will + be attached. The files listed in this configuration will all be put + into the root directory of the floppy disk; sub-directories are not supported. + * `guest_additions_path` (string) - The path on the guest virtual machine where the VirtualBox guest additions ISO will be uploaded. By default this is "VBoxGuestAdditions.iso" which should upload into the login directory diff --git a/website/source/docs/builders/vmware.html.markdown b/website/source/docs/builders/vmware.html.markdown index e9bf5955b..7b97a2c9e 100644 --- a/website/source/docs/builders/vmware.html.markdown +++ b/website/source/docs/builders/vmware.html.markdown @@ -73,6 +73,13 @@ Optional: actual file representing the disk will not use the full size unless it is full. By default this is set to 40,000 (40 GB). +* `floppy_files` (array of strings) - A list of files to put onto a floppy + disk that is attached when the VM is booted for the first time. This is + most useful for unattended Windows installs, which look for an + `Autounattend.xml` file on removable media. By default no floppy will + be attached. The files listed in this configuration will all be put + into the root directory of the floppy disk; sub-directories are not supported. + * `guest_os_type` (string) - The guest OS type being installed. This will be set in the VMware VMX. By default this is "other". By specifying a more specific OS type, VMware may perform some optimizations or virtual hardware changes From e60945ed7bf0683f951bcbf654728e35a84f3e12 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 9 Jul 2013 12:38:34 -0700 Subject: [PATCH 52/55] builder/virtualbox: remove floppy drive before exporting --- builder/virtualbox/step_attach_floppy.go | 3 +-- builder/virtualbox/step_export.go | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/builder/virtualbox/step_attach_floppy.go b/builder/virtualbox/step_attach_floppy.go index be7394160..bb5c8c5d8 100644 --- a/builder/virtualbox/step_attach_floppy.go +++ b/builder/virtualbox/step_attach_floppy.go @@ -85,7 +85,6 @@ func (s *stepAttachFloppy) Cleanup(state map[string]interface{}) { defer os.Remove(s.floppyPath) driver := state["driver"].(Driver) - ui := state["ui"].(packer.Ui) vmName := state["vmName"].(string) command := []string{ @@ -97,7 +96,7 @@ func (s *stepAttachFloppy) Cleanup(state map[string]interface{}) { } if err := driver.VBoxManage(command...); err != nil { - ui.Error(fmt.Sprintf("Error unregistering floppy: %s", err)) + log.Printf("Error unregistering floppy: %s", err) } } diff --git a/builder/virtualbox/step_export.go b/builder/virtualbox/step_export.go index a37f718f4..c15fc1a0a 100644 --- a/builder/virtualbox/step_export.go +++ b/builder/virtualbox/step_export.go @@ -22,7 +22,8 @@ func (s *stepExport) Run(state map[string]interface{}) multistep.StepAction { vmName := state["vmName"].(string) // Clear out the Packer-created forwarding rule - ui.Say(fmt.Sprintf("Deleting forwarded port mapping for SSH (host port %d)", state["sshHostPort"])) + ui.Say("Preparing to export machine...") + ui.Message(fmt.Sprintf("Deleting forwarded port mapping for SSH (host port %d)", state["sshHostPort"])) command := []string{"modifyvm", vmName, "--natpf1", "delete", "packerssh"} if err := driver.VBoxManage(command...); err != nil { err := fmt.Errorf("Error deleting port forwarding rule: %s", err) @@ -31,6 +32,23 @@ func (s *stepExport) Run(state map[string]interface{}) multistep.StepAction { return multistep.ActionHalt } + // Remove the attached floppy disk, if it exists + if _, ok := state["floppy_path"]; ok { + ui.Message("Removing floppy drive...") + command := []string{ + "storageattach", vmName, + "--storagectl", "Floppy Controller", + "--port", "0", + "--device", "0", + "--medium", "none", + } + if err := driver.VBoxManage(command...); err != nil { + state["error"] = fmt.Errorf("Error removing floppy: %s", err) + return multistep.ActionHalt + } + + } + // Export the VM to an OVF outputPath := filepath.Join(config.OutputDir, "packer.ovf") From 86ec808fe04977f937433b3bfc5de6fcb59b60f2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 9 Jul 2013 12:39:28 -0700 Subject: [PATCH 53/55] builder/virtualbox: paused is still running --- builder/virtualbox/driver.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/builder/virtualbox/driver.go b/builder/virtualbox/driver.go index b36b5c636..3a7e4bfb1 100644 --- a/builder/virtualbox/driver.go +++ b/builder/virtualbox/driver.go @@ -59,6 +59,12 @@ func (d *VBox42Driver) IsRunning(name string) (bool, error) { if line == `VMState="stopping"` { return true, nil } + + // We consider "paused" to still be running. We wait for it to + // be completely stopped or some other state. + if line == `VMState="paused"` { + return true, nil + } } return false, nil From a5e5e689e070c875ad0befdc86a4230721cf57dd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 9 Jul 2013 12:41:17 -0700 Subject: [PATCH 54/55] Update CHANGELOG --- CHANGELOG.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fa0cba94..9850c56e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ -## 0.1.6 (unreleased) +## 0.2.0 (unreleased) +FEATURES: +* VirtualBox and VMware can now have `floppy_files` specified to attach + floppy disks when booting. This allows for unattended Windows installs. + +BUG FIXES: + +* core: UI messages are now properly prefixed with spaces again. +* virtualbox: "paused" doesn't mean the VM is stopped, improving + shutdown detection. ## 0.1.5 (July 7, 2013) From 8537d08caa63f227bb23623b0fc54c1f16fa1d0e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 9 Jul 2013 12:42:54 -0700 Subject: [PATCH 55/55] scripts: add -e to build.sh again --- scripts/build.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/build.sh b/scripts/build.sh index 15d917b85..9877ffa38 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,6 +1,8 @@ #!/bin/bash # # This script only builds the application from source. +set -e + NO_COLOR="\x1b[0m" OK_COLOR="\x1b[32;01m" ERROR_COLOR="\x1b[31;01m"