diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go index c940a7bfb..de3a840b1 100644 --- a/builder/amazon/chroot/builder.go +++ b/builder/amazon/chroot/builder.go @@ -13,6 +13,7 @@ import ( "github.com/mitchellh/packer/common" "github.com/mitchellh/packer/packer" "log" + "os/exec" "runtime" ) @@ -29,14 +30,17 @@ type Config struct { ChrootMounts [][]string `mapstructure:"chroot_mounts"` CopyFiles []string `mapstructure:"copy_files"` DevicePath string `mapstructure:"device_path"` - MountCommand string `mapstructure:"mount_command"` + CommandWrapper string `mapstructure:"command_wrapper"` MountPath string `mapstructure:"mount_path"` SourceAmi string `mapstructure:"source_ami"` - UnmountCommand string `mapstructure:"unmount_command"` tpl *packer.ConfigTemplate } +type wrappedCommandTemplate struct { + Command string +} + type Builder struct { config Config runner multistep.Runner @@ -78,18 +82,14 @@ func (b *Builder) Prepare(raws ...interface{}) error { b.config.CopyFiles = []string{"/etc/resolv.conf"} } - if b.config.MountCommand == "" { - b.config.MountCommand = "mount" + if b.config.CommandWrapper == "" { + b.config.CommandWrapper = "{{.Command}}" } if b.config.MountPath == "" { b.config.MountPath = "packer-amazon-chroot-volumes/{{.Device}}" } - if b.config.UnmountCommand == "" { - b.config.UnmountCommand = "umount" - } - // Accumulate any errors errs := common.CheckUnusedConfig(md) errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(b.config.tpl)...) @@ -127,10 +127,8 @@ func (b *Builder) Prepare(raws ...interface{}) error { } templates := map[string]*string{ - "device_path": &b.config.DevicePath, - "mount_command": &b.config.MountCommand, - "source_ami": &b.config.SourceAmi, - "unmount_command": &b.config.UnmountCommand, + "device_path": &b.config.DevicePath, + "source_ami": &b.config.SourceAmi, } for n, ptr := range templates { @@ -167,12 +165,24 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ec2conn := ec2.New(auth, region) + wrappedCommand := func(command string) *exec.Cmd { + wrapped, err := b.config.tpl.Process( + b.config.CommandWrapper, &wrappedCommandTemplate{ + Command: command, + }) + if err != nil { + ui.Error(err.Error()) + } + return ShellCommand(wrapped) + } + // Setup the state bag and initial state for the steps state := new(multistep.BasicStateBag) state.Put("config", &b.config) state.Put("ec2", ec2conn) state.Put("hook", hook) state.Put("ui", ui) + state.Put("wrappedCommand", Command(wrappedCommand)) // Build the steps steps := []multistep.Step{ diff --git a/builder/amazon/chroot/builder_test.go b/builder/amazon/chroot/builder_test.go index 2f13bae54..6bdf7dc28 100644 --- a/builder/amazon/chroot/builder_test.go +++ b/builder/amazon/chroot/builder_test.go @@ -82,3 +82,14 @@ func TestBuilderPrepare_SourceAmi(t *testing.T) { t.Errorf("err: %s", err) } } + +func TestBuilderPrepare_CommandWrapper(t *testing.T) { + b := &Builder{} + config := testConfig() + + config["command_wrapper"] = "echo hi; {{.Command}}" + err := b.Prepare(config) + if err != nil { + t.Errorf("err: %s", err) + } +} diff --git a/builder/amazon/chroot/communicator.go b/builder/amazon/chroot/communicator.go index 111adb42e..8e6bc90bf 100644 --- a/builder/amazon/chroot/communicator.go +++ b/builder/amazon/chroot/communicator.go @@ -1,8 +1,12 @@ package chroot +// pf := func () { somefunc("a str", 1) } + import ( + "fmt" "github.com/mitchellh/packer/packer" "io" + "io/ioutil" "log" "os" "os/exec" @@ -10,19 +14,18 @@ import ( "syscall" ) +type Command func(string) *exec.Cmd + // Communicator is a special communicator that works by executing // commands locally but within a chroot. type Communicator struct { - Chroot string + Chroot string + ChrootCmd Command + WrappedCommand Command } func (c *Communicator) Start(cmd *packer.RemoteCmd) error { - chrootCmdPath, err := exec.LookPath("chroot") - if err != nil { - return err - } - - localCmd := exec.Command(chrootCmdPath, c.Chroot, "/bin/sh", "-c", cmd.Command) + localCmd := c.ChrootCmd(cmd.Command) localCmd.Stdin = cmd.Stdin localCmd.Stdout = cmd.Stdout localCmd.Stderr = cmd.Stderr @@ -46,7 +49,7 @@ func (c *Communicator) Start(cmd *packer.RemoteCmd) error { } log.Printf( - "Chroot executation ended with '%d': '%s'", + "Chroot executation exited with '%d': '%s'", exitStatus, cmd.Command) cmd.SetExited(exitStatus) }() @@ -57,49 +60,47 @@ func (c *Communicator) Start(cmd *packer.RemoteCmd) error { func (c *Communicator) Upload(dst string, r io.Reader) error { dst = filepath.Join(c.Chroot, dst) log.Printf("Uploading to chroot dir: %s", dst) - f, err := os.Create(dst) + tf, err := ioutil.TempFile("", "packer-amazon-chroot") if err != nil { - return err + return fmt.Errorf("Error preparing shell script: %s", err) } - defer f.Close() - - if _, err := io.Copy(f, r); err != nil { - return err - } - - return nil + defer os.Remove(tf.Name()) + io.Copy(tf, r) + cpCmd := fmt.Sprintf("cp %s %s", tf.Name(), dst) + return (c.WrappedCommand(cpCmd)).Run() } func (c *Communicator) UploadDir(dst string, src string, exclude []string) error { - walkFn := func(fullPath string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - path, err := filepath.Rel(src, fullPath) - if err != nil { - return err - } - - for _, e := range exclude { - if e == path { - log.Printf("Skipping excluded file: %s", path) - return nil + /* + walkFn := func(fullPath string, info os.FileInfo, err error) error { + if err != nil { + return err } + + path, err := filepath.Rel(src, fullPath) + if err != nil { + return err + } + + for _, e := range exclude { + if e == path { + log.Printf("Skipping excluded file: %s", path) + return nil + } + } + + chrootDest := filepath.Join(c.Chroot, dst, path) + log.Printf("Uploading dir %s to chroot dir: %s", src, dst) + cpCmd := fmt.Sprintf("cp %s %s", fullPath, chrootDest) + return c.WrappedCommand(cpCmd).Run() } + */ - dstPath := filepath.Join(dst, path) - f, err := os.Open(fullPath) - if err != nil { - return err - } - defer f.Close() - - return c.Upload(dstPath, f) - } - - log.Printf("Uploading directory '%s' to '%s'", src, dst) - return filepath.Walk(src, walkFn) + // TODO: remove any file copied if it appears in `exclude` + chrootDest := filepath.Join(c.Chroot, dst) + log.Printf("Uploading directory '%s' to '%s'", src, chrootDest) + cpCmd := fmt.Sprintf("cp -R %s* %s", src, chrootDest) + return c.WrappedCommand(cpCmd).Run() } func (c *Communicator) Download(src string, w io.Writer) error { diff --git a/builder/amazon/chroot/copy_files.go b/builder/amazon/chroot/copy_files.go new file mode 100644 index 000000000..b8807d095 --- /dev/null +++ b/builder/amazon/chroot/copy_files.go @@ -0,0 +1,19 @@ +package chroot + +import ( + "fmt" + "log" + "os/exec" +) + +func ChrootCommand(chroot string, command string) *exec.Cmd { + cmd := fmt.Sprintf("sudo chroot %s", chroot) + return ShellCommand(cmd, command) +} + +func ShellCommand(commands ...string) *exec.Cmd { + cmds := append([]string{"-c"}, commands...) + cmd := exec.Command("/bin/sh", cmds...) + log.Printf("ShellCommand: %s %v", cmd.Path, cmd.Args[1:]) + return cmd +} diff --git a/builder/amazon/chroot/copy_files_test.go b/builder/amazon/chroot/copy_files_test.go new file mode 100644 index 000000000..c7e00018e --- /dev/null +++ b/builder/amazon/chroot/copy_files_test.go @@ -0,0 +1,44 @@ +package chroot + +import ( + "fmt" + "io/ioutil" + "os" + "testing" +) + +func TestCopyFile(t *testing.T) { + first, err := ioutil.TempFile("", "copy_files_test") + if err != nil { + t.Fatalf("couldn't create temp file.") + } + defer os.Remove(first.Name()) + newName := first.Name() + "-new" + + payload := "copy_files_test.go payload" + if _, err = first.WriteString(payload); err != nil { + t.Fatalf("Couldn't write payload to first file.") + } + first.Sync() + + cmd := ShellCommand(fmt.Sprintf("cp %s %s", first.Name(), newName)) + if err := cmd.Run(); err != nil { + t.Fatalf("Couldn't copy file") + } + defer os.Remove(newName) + + second, err := os.Open(newName) + if err != nil { + t.Fatalf("Couldn't open copied file.") + } + defer second.Close() + + var copiedPayload = make([]byte, len(payload)) + if _, err := second.Read(copiedPayload); err != nil { + t.Fatalf("Couldn't open copied file for reading.") + } + + if string(copiedPayload) != payload { + t.Fatalf("payload not copied.") + } +} diff --git a/builder/amazon/chroot/step_chroot_provision.go b/builder/amazon/chroot/step_chroot_provision.go index 5ad87836d..f4612c807 100644 --- a/builder/amazon/chroot/step_chroot_provision.go +++ b/builder/amazon/chroot/step_chroot_provision.go @@ -4,6 +4,7 @@ import ( "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" "log" + "os/exec" ) // StepChrootProvision provisions the instance within a chroot. @@ -15,10 +16,16 @@ func (s *StepChrootProvision) Run(state multistep.StateBag) multistep.StepAction hook := state.Get("hook").(packer.Hook) mountPath := state.Get("mount_path").(string) ui := state.Get("ui").(packer.Ui) + wrappedCommand := state.Get("wrappedCommand").(Command) + chrootCmd := func(command string) *exec.Cmd { + return ChrootCommand(mountPath, command) + } // Create our communicator comm := &Communicator{ - Chroot: mountPath, + Chroot: mountPath, + ChrootCmd: chrootCmd, + WrappedCommand: wrappedCommand, } // Provision diff --git a/builder/amazon/chroot/step_copy_files.go b/builder/amazon/chroot/step_copy_files.go index d729ee234..3cb096d10 100644 --- a/builder/amazon/chroot/step_copy_files.go +++ b/builder/amazon/chroot/step_copy_files.go @@ -1,12 +1,11 @@ package chroot import ( + "bytes" "fmt" "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" - "io" "log" - "os" "path/filepath" ) @@ -23,6 +22,8 @@ func (s *StepCopyFiles) Run(state multistep.StateBag) multistep.StepAction { config := state.Get("config").(*Config) mountPath := state.Get("mount_path").(string) ui := state.Get("ui").(packer.Ui) + wrappedCommand := state.Get("wrappedCommand").(Command) + stderr := new(bytes.Buffer) s.files = make([]string, 0, len(config.CopyFiles)) if len(config.CopyFiles) > 0 { @@ -32,8 +33,12 @@ func (s *StepCopyFiles) Run(state multistep.StateBag) multistep.StepAction { chrootPath := filepath.Join(mountPath, path) log.Printf("Copying '%s' to '%s'", path, chrootPath) - if err := s.copySingle(chrootPath, path); err != nil { - err := fmt.Errorf("Error copying file: %s", err) + cmd := wrappedCommand(fmt.Sprintf("cp %s %s", path, chrootPath)) + stderr.Reset() + cmd.Stderr = stderr + if err := cmd.Run(); err != nil { + err := fmt.Errorf( + "Error copying file: %s\nnStderr: %s", err, stderr.String()) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt @@ -54,11 +59,13 @@ func (s *StepCopyFiles) Cleanup(state multistep.StateBag) { } } -func (s *StepCopyFiles) CleanupFunc(multistep.StateBag) error { +func (s *StepCopyFiles) CleanupFunc(state multistep.StateBag) error { + wrappedCommand := state.Get("wrappedCommand").(Command) if s.files != nil { for _, file := range s.files { log.Printf("Removing: %s", file) - if err := os.Remove(file); err != nil { + localCmd := wrappedCommand(fmt.Sprintf("rm -f %s", file)) + if err := localCmd.Run(); err != nil { return err } } @@ -67,41 +74,3 @@ func (s *StepCopyFiles) CleanupFunc(multistep.StateBag) error { s.files = nil return nil } - -func (s *StepCopyFiles) copySingle(dst, src string) error { - // Stat the src file so we can copy the mode later - srcInfo, err := os.Stat(src) - if err != nil { - return err - } - - // Remove any existing destination file - if err := os.Remove(dst); err != nil { - return err - } - - // Copy the files - 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 - } - dstF.Close() - - // Match the mode - if err := os.Chmod(dst, srcInfo.Mode()); err != nil { - return err - } - - return nil -} diff --git a/builder/amazon/chroot/step_mount_device.go b/builder/amazon/chroot/step_mount_device.go index 6079a0293..f9a823623 100644 --- a/builder/amazon/chroot/step_mount_device.go +++ b/builder/amazon/chroot/step_mount_device.go @@ -7,7 +7,6 @@ import ( "github.com/mitchellh/packer/packer" "log" "os" - "os/exec" "path/filepath" ) @@ -59,8 +58,9 @@ func (s *StepMountDevice) Run(state multistep.StateBag) multistep.StepAction { ui.Say("Mounting the root device...") stderr := new(bytes.Buffer) - mountCommand := fmt.Sprintf("%s %s %s", config.MountCommand, device, mountPath) - cmd := exec.Command("/bin/sh", "-c", mountCommand) + mountCommand := fmt.Sprintf("mount %s %s", device, mountPath) + wrappedCommand := state.Get("wrappedCommand").(Command) + cmd := wrappedCommand(mountCommand) cmd.Stderr = stderr if err := cmd.Run(); err != nil { err := fmt.Errorf( @@ -90,12 +90,12 @@ func (s *StepMountDevice) CleanupFunc(state multistep.StateBag) error { return nil } - config := state.Get("config").(*Config) ui := state.Get("ui").(packer.Ui) ui.Say("Unmounting the root device...") - unmountCommand := fmt.Sprintf("%s %s", config.UnmountCommand, s.mountPath) - cmd := exec.Command("/bin/sh", "-c", unmountCommand) + unmountCommand := fmt.Sprintf("umount %s", s.mountPath) + wrappedCommand := state.Get("wrappedCommand").(Command) + cmd := wrappedCommand(unmountCommand) if err := cmd.Run(); err != nil { return fmt.Errorf("Error unmounting root device: %s", err) } diff --git a/builder/amazon/chroot/step_mount_extra.go b/builder/amazon/chroot/step_mount_extra.go index 3fc35f4ac..a5216e055 100644 --- a/builder/amazon/chroot/step_mount_extra.go +++ b/builder/amazon/chroot/step_mount_extra.go @@ -6,7 +6,6 @@ import ( "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" "os" - "os/exec" ) // StepMountExtra mounts the attached device. @@ -21,6 +20,7 @@ func (s *StepMountExtra) Run(state multistep.StateBag) multistep.StepAction { config := state.Get("config").(*Config) mountPath := state.Get("mount_path").(string) ui := state.Get("ui").(packer.Ui) + wrappedCommand := state.Get("wrappedCommand").(Command) s.mounts = make([]string, 0, len(config.ChrootMounts)) @@ -43,12 +43,11 @@ func (s *StepMountExtra) Run(state multistep.StateBag) multistep.StepAction { ui.Message(fmt.Sprintf("Mounting: %s", mountInfo[2])) stderr := new(bytes.Buffer) mountCommand := fmt.Sprintf( - "%s %s %s %s", - config.MountCommand, + "mount %s %s %s", flags, mountInfo[1], innerPath) - cmd := exec.Command("/bin/sh", "-c", mountCommand) + cmd := wrappedCommand(mountCommand) cmd.Stderr = stderr if err := cmd.Run(); err != nil { err := fmt.Errorf( @@ -79,15 +78,15 @@ func (s *StepMountExtra) CleanupFunc(state multistep.StateBag) error { return nil } - config := state.Get("config").(*Config) + wrappedCommand := state.Get("wrappedCommand").(Command) for len(s.mounts) > 0 { var path string lastIndex := len(s.mounts) - 1 path, s.mounts = s.mounts[lastIndex], s.mounts[:lastIndex] - unmountCommand := fmt.Sprintf("%s %s", config.UnmountCommand, path) + unmountCommand := fmt.Sprintf("umount %s", path) stderr := new(bytes.Buffer) - cmd := exec.Command("/bin/sh", "-c", unmountCommand) + cmd := wrappedCommand(unmountCommand) cmd.Stderr = stderr if err := cmd.Run(); err != nil { return fmt.Errorf( diff --git a/website/source/docs/builders/amazon-chroot.html.markdown b/website/source/docs/builders/amazon-chroot.html.markdown index b23607470..2ea9e875d 100644 --- a/website/source/docs/builders/amazon-chroot.html.markdown +++ b/website/source/docs/builders/amazon-chroot.html.markdown @@ -111,9 +111,11 @@ Optional: of the source AMI will be attached. This defaults to "" (empty string), which forces Packer to find an open device automatically. -* `mount_command` (string) - The command to use to mount devices. This - defaults to "mount". This may be useful to set if you want to set - environmental variables or perhaps run it with `sudo` or so on. +* `command_wrapper` (string) - How to run shell commands. This + defaults to "{{.Command}}". This may be useful to set if you want to set + environmental variables or perhaps run it with `sudo` or so on. This is a + configuration template where the `.Command` variable is replaced with the + command to be run.. * `mount_path` (string) - The path where the volume will be mounted. This is where the chroot environment will be. This defaults to @@ -123,9 +125,6 @@ Optional: * `tags` (object of key/value strings) - Tags applied to the AMI. -* `unmount_command` (string) - Just like `mount_command`, except this is - the command to unmount devices. - ## Basic Example Here is a basic example. It is completely valid except for the access keys: