From 80fc1f032b29d227d1fbd836cc4c0f1ac4ba8292 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 19 Jun 2015 15:06:06 -0700 Subject: [PATCH 1/4] provisioner/shell-local: a first stab --- provisioner/shell-local/communicator.go | 81 +++++++++++++++ provisioner/shell-local/provisioner.go | 109 ++++++++++++++++++++ provisioner/shell-local/provisioner_test.go | 67 ++++++++++++ 3 files changed, 257 insertions(+) create mode 100644 provisioner/shell-local/communicator.go create mode 100644 provisioner/shell-local/provisioner.go create mode 100644 provisioner/shell-local/provisioner_test.go diff --git a/provisioner/shell-local/communicator.go b/provisioner/shell-local/communicator.go new file mode 100644 index 000000000..5cf3cd980 --- /dev/null +++ b/provisioner/shell-local/communicator.go @@ -0,0 +1,81 @@ +package shell + +import ( + "fmt" + "io" + "os" + "os/exec" + "syscall" + + "github.com/mitchellh/packer/packer" + "github.com/mitchellh/packer/template/interpolate" +) + +type Communicator struct { + ExecuteCommand []string + Ctx interpolate.Context +} + +func (c *Communicator) Start(cmd *packer.RemoteCmd) error { + // Render the template so that we know how to execute the command + c.Ctx.Data = &ExecuteCommandTemplate{ + Command: cmd.Command, + } + for i, field := range c.ExecuteCommand { + command, err := interpolate.Render(field, &c.Ctx) + if err != nil { + return fmt.Errorf("Error processing command: %s", err) + } + + c.ExecuteCommand[i] = command + } + + // Build the local command to execute + localCmd := exec.Command(c.ExecuteCommand[0], c.ExecuteCommand[1:]...) + localCmd.Stdin = cmd.Stdin + localCmd.Stdout = cmd.Stdout + localCmd.Stderr = cmd.Stderr + + // Start it. If it doesn't work, then error right away. + if err := localCmd.Start(); err != nil { + return err + } + + // We've started successfully. Start a goroutine to wait for + // it to complete and track exit status. + go func() { + var exitStatus int + err := localCmd.Wait() + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + exitStatus = 1 + + // There is no process-independent way to get the REAL + // exit status so we just try to go deeper. + if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { + exitStatus = status.ExitStatus() + } + } + } + + cmd.SetExited(exitStatus) + }() + + return nil +} + +func (c *Communicator) Upload(string, io.Reader, *os.FileInfo) error { + return fmt.Errorf("upload not supported") +} + +func (c *Communicator) UploadDir(string, string, []string) error { + return fmt.Errorf("uploadDir not supported") +} + +func (c *Communicator) Download(string, io.Writer) error { + return fmt.Errorf("download not supported") +} + +type ExecuteCommandTemplate struct { + Command string +} diff --git a/provisioner/shell-local/provisioner.go b/provisioner/shell-local/provisioner.go new file mode 100644 index 000000000..499be7f1d --- /dev/null +++ b/provisioner/shell-local/provisioner.go @@ -0,0 +1,109 @@ +package shell + +import ( + "errors" + "fmt" + "runtime" + + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/helper/config" + "github.com/mitchellh/packer/packer" + "github.com/mitchellh/packer/template/interpolate" +) + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + + // Command is the command to execute + Command string + + // ExecuteCommand is the command used to execute the command. + ExecuteCommand []string `mapstructure:"execute_command"` + + ctx interpolate.Context +} + +type Provisioner struct { + config Config +} + +func (p *Provisioner) Prepare(raws ...interface{}) error { + err := config.Decode(&p.config, &config.DecodeOpts{ + Interpolate: true, + InterpolateFilter: &interpolate.RenderFilter{ + Exclude: []string{ + "execute_command", + }, + }, + }, raws...) + if err != nil { + return err + } + + if len(p.config.ExecuteCommand) == 0 { + if runtime.GOOS == "windows" { + p.config.ExecuteCommand = []string{ + "cmd", + "/C", + "{{.Command}}", + } + } else { + p.config.ExecuteCommand = []string{ + "/bin/sh", + "-c", + "{{.Command}}", + } + } + } + + var errs *packer.MultiError + if p.config.Command == "" { + errs = packer.MultiErrorAppend(errs, + errors.New("command must be specified")) + } + + if len(p.config.ExecuteCommand) == 0 { + errs = packer.MultiErrorAppend(errs, + errors.New("execute_command must not be empty")) + } + + if errs != nil && len(errs.Errors) > 0 { + return errs + } + + return nil +} + +func (p *Provisioner) Provision(ui packer.Ui, _ packer.Communicator) error { + // Make another communicator for local + comm := &Communicator{ + Ctx: p.config.ctx, + ExecuteCommand: p.config.ExecuteCommand, + } + + // Build the remote command + cmd := &packer.RemoteCmd{Command: p.config.Command} + + ui.Say(fmt.Sprintf( + "Executing local command: %s", + p.config.Command)) + if err := cmd.StartWithUi(comm, ui); err != nil { + return fmt.Errorf( + "Error executing command: %s\n\n"+ + "Please see output above for more information.", + p.config.Command) + } + if cmd.ExitStatus != 0 { + return fmt.Errorf( + "Erroneous exit code %s while executing command: %s\n\n"+ + "Please see output above for more information.", + cmd.ExitStatus, + p.config.Command) + } + + return nil +} + +func (p *Provisioner) Cancel() { + // Just do nothing. When the process ends, so will our provisioner +} diff --git a/provisioner/shell-local/provisioner_test.go b/provisioner/shell-local/provisioner_test.go new file mode 100644 index 000000000..ad8f3065d --- /dev/null +++ b/provisioner/shell-local/provisioner_test.go @@ -0,0 +1,67 @@ +package shell + +import ( + "testing" + + "github.com/mitchellh/packer/packer" +) + +func TestProvisioner_impl(t *testing.T) { + var _ packer.Provisioner = new(Provisioner) +} + +func TestConfigPrepare(t *testing.T) { + cases := []struct { + Key string + Value interface{} + Err bool + }{ + { + "unknown_key", + "bad", + true, + }, + + { + "command", + nil, + true, + }, + } + + for _, tc := range cases { + raw := testConfig(t) + + if tc.Value == nil { + delete(raw, tc.Key) + } else { + raw[tc.Key] = tc.Value + } + + var p Provisioner + err := p.Prepare(raw) + if tc.Err { + testConfigErr(t, err, tc.Key) + } else { + testConfigOk(t, err) + } + } +} + +func testConfig(t *testing.T) map[string]interface{} { + return map[string]interface{}{ + "command": "echo foo", + } +} + +func testConfigErr(t *testing.T, err error, extra string) { + if err == nil { + t.Fatalf("should error: %s", extra) + } +} + +func testConfigOk(t *testing.T, err error) { + if err != nil { + t.Fatalf("bad: %s", err) + } +} From 2dc6155c30b243efc85067b8629bdd33da26952f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 19 Jun 2015 15:27:44 -0700 Subject: [PATCH 2/4] provisioner/shell-local: tests --- provisioner/shell-local/communicator_test.go | 45 ++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 provisioner/shell-local/communicator_test.go diff --git a/provisioner/shell-local/communicator_test.go b/provisioner/shell-local/communicator_test.go new file mode 100644 index 000000000..90402324b --- /dev/null +++ b/provisioner/shell-local/communicator_test.go @@ -0,0 +1,45 @@ +package shell + +import ( + "bytes" + "runtime" + "strings" + "testing" + + "github.com/mitchellh/packer/packer" +) + +func TestCommunicator_impl(t *testing.T) { + var _ packer.Communicator = new(Communicator) +} + +func TestCommunicator(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("windows not supported for this test") + return + } + + c := &Communicator{ + ExecuteCommand: []string{"/bin/sh", "-c", "{{.Command}}"}, + } + + var buf bytes.Buffer + cmd := &packer.RemoteCmd{ + Command: "echo foo", + Stdout: &buf, + } + + if err := c.Start(cmd); err != nil { + t.Fatalf("err: %s", err) + } + + cmd.Wait() + + if cmd.ExitStatus != 0 { + t.Fatalf("err bad exit status: %d", cmd.ExitStatus) + } + + if strings.TrimSpace(buf.String()) != "foo" { + t.Fatalf("bad: %s", buf.String()) + } +} From d5c0f13d8f14d88e576818ce7217eaeb36e11fac Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 19 Jun 2015 15:31:17 -0700 Subject: [PATCH 3/4] website: document local shell --- .../docs/provisioners/shell-local.html.md | 45 +++++++++++++++++++ website/source/layouts/docs.erb | 3 +- 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 website/source/docs/provisioners/shell-local.html.md diff --git a/website/source/docs/provisioners/shell-local.html.md b/website/source/docs/provisioners/shell-local.html.md new file mode 100644 index 000000000..b986cd5ef --- /dev/null +++ b/website/source/docs/provisioners/shell-local.html.md @@ -0,0 +1,45 @@ +--- +layout: "docs" +page_title: "Local Shell Provisioner" +description: |- + The shell Packer provisioner provisions machines built by Packer using shell scripts. Shell provisioning is the easiest way to get software installed and configured on a machine. +--- + +# Local Shell Provisioner + +Type: `shell-local` + +The local shell provisioner executes a local shell script on the machine +running Packer. The [remote shell](/docs/provisioners/shell.html) +provisioner executes shell scripts on a remote machine. + +## Basic Example + +The example below is fully functional. + +```javascript +{ + "type": "shell-local", + "command": "echo foo" +} +``` + +## Configuration Reference + +The reference of available configuration options is listed below. The only +required element is "command". + +Required: + +* `command` (string) - The command to execute. This will be executed + within the context of a shell as specified by `execute_command`. + +Optional parameters: + +* `execute_command` (array of strings) - The command to use to execute the script. + By default this is `["/bin/sh", "-c", "{{.Command}"]`. The value is an array + of arguments executed directly by the OS. + The value of this is + treated as [configuration template](/docs/templates/configuration-templates.html). + The only available variable is `Command` which is the command to execute. + diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 8099b461a..b4101422c 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -47,7 +47,8 @@
  • Provisioners

  • -
  • Shell Scripts
  • +
  • Remote Shell
  • +
  • Local Shell
  • File Uploads
  • PowerShell
  • Windows Shell
  • From a0b6928dceb22395074ba80f4c1eb7df9bf359fc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 19 Jun 2015 15:31:40 -0700 Subject: [PATCH 4/4] plugin: add the plugin --- plugin/provisioner-shell-local/main.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 plugin/provisioner-shell-local/main.go diff --git a/plugin/provisioner-shell-local/main.go b/plugin/provisioner-shell-local/main.go new file mode 100644 index 000000000..4f46a3ed3 --- /dev/null +++ b/plugin/provisioner-shell-local/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "github.com/mitchellh/packer/packer/plugin" + "github.com/mitchellh/packer/provisioner/shell-local" +) + +func main() { + server, err := plugin.Server() + if err != nil { + panic(err) + } + server.RegisterProvisioner(new(shell.Provisioner)) + server.Serve() +}