Commit 707fe57e authored by Mitchell Hashimoto's avatar Mitchell Hashimoto

Merge pull request #432 from mwhooker/chroot_cmd

build/amazon/chroot: command_wrapper to support sudo-less
parents b00e3955 4ab4dbac
...@@ -13,6 +13,7 @@ import ( ...@@ -13,6 +13,7 @@ import (
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"log" "log"
"os/exec"
"runtime" "runtime"
) )
...@@ -29,14 +30,17 @@ type Config struct { ...@@ -29,14 +30,17 @@ type Config struct {
ChrootMounts [][]string `mapstructure:"chroot_mounts"` ChrootMounts [][]string `mapstructure:"chroot_mounts"`
CopyFiles []string `mapstructure:"copy_files"` CopyFiles []string `mapstructure:"copy_files"`
DevicePath string `mapstructure:"device_path"` DevicePath string `mapstructure:"device_path"`
MountCommand string `mapstructure:"mount_command"` CommandWrapper string `mapstructure:"command_wrapper"`
MountPath string `mapstructure:"mount_path"` MountPath string `mapstructure:"mount_path"`
SourceAmi string `mapstructure:"source_ami"` SourceAmi string `mapstructure:"source_ami"`
UnmountCommand string `mapstructure:"unmount_command"`
tpl *packer.ConfigTemplate tpl *packer.ConfigTemplate
} }
type wrappedCommandTemplate struct {
Command string
}
type Builder struct { type Builder struct {
config Config config Config
runner multistep.Runner runner multistep.Runner
...@@ -78,18 +82,14 @@ func (b *Builder) Prepare(raws ...interface{}) error { ...@@ -78,18 +82,14 @@ func (b *Builder) Prepare(raws ...interface{}) error {
b.config.CopyFiles = []string{"/etc/resolv.conf"} b.config.CopyFiles = []string{"/etc/resolv.conf"}
} }
if b.config.MountCommand == "" { if b.config.CommandWrapper == "" {
b.config.MountCommand = "mount" b.config.CommandWrapper = "{{.Command}}"
} }
if b.config.MountPath == "" { if b.config.MountPath == "" {
b.config.MountPath = "packer-amazon-chroot-volumes/{{.Device}}" b.config.MountPath = "packer-amazon-chroot-volumes/{{.Device}}"
} }
if b.config.UnmountCommand == "" {
b.config.UnmountCommand = "umount"
}
// Accumulate any errors // Accumulate any errors
errs := common.CheckUnusedConfig(md) errs := common.CheckUnusedConfig(md)
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(b.config.tpl)...) errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(b.config.tpl)...)
...@@ -127,10 +127,8 @@ func (b *Builder) Prepare(raws ...interface{}) error { ...@@ -127,10 +127,8 @@ func (b *Builder) Prepare(raws ...interface{}) error {
} }
templates := map[string]*string{ templates := map[string]*string{
"device_path": &b.config.DevicePath, "device_path": &b.config.DevicePath,
"mount_command": &b.config.MountCommand, "source_ami": &b.config.SourceAmi,
"source_ami": &b.config.SourceAmi,
"unmount_command": &b.config.UnmountCommand,
} }
for n, ptr := range templates { for n, ptr := range templates {
...@@ -167,12 +165,24 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -167,12 +165,24 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
ec2conn := ec2.New(auth, region) 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 // Setup the state bag and initial state for the steps
state := new(multistep.BasicStateBag) state := new(multistep.BasicStateBag)
state.Put("config", &b.config) state.Put("config", &b.config)
state.Put("ec2", ec2conn) state.Put("ec2", ec2conn)
state.Put("hook", hook) state.Put("hook", hook)
state.Put("ui", ui) state.Put("ui", ui)
state.Put("wrappedCommand", Command(wrappedCommand))
// Build the steps // Build the steps
steps := []multistep.Step{ steps := []multistep.Step{
......
...@@ -82,3 +82,14 @@ func TestBuilderPrepare_SourceAmi(t *testing.T) { ...@@ -82,3 +82,14 @@ func TestBuilderPrepare_SourceAmi(t *testing.T) {
t.Errorf("err: %s", err) 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)
}
}
package chroot package chroot
// pf := func () { somefunc("a str", 1) }
import ( import (
"fmt"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"io" "io"
"io/ioutil"
"log" "log"
"os" "os"
"os/exec" "os/exec"
...@@ -10,19 +14,18 @@ import ( ...@@ -10,19 +14,18 @@ import (
"syscall" "syscall"
) )
type Command func(string) *exec.Cmd
// Communicator is a special communicator that works by executing // Communicator is a special communicator that works by executing
// commands locally but within a chroot. // commands locally but within a chroot.
type Communicator struct { type Communicator struct {
Chroot string Chroot string
ChrootCmd Command
WrappedCommand Command
} }
func (c *Communicator) Start(cmd *packer.RemoteCmd) error { func (c *Communicator) Start(cmd *packer.RemoteCmd) error {
chrootCmdPath, err := exec.LookPath("chroot") localCmd := c.ChrootCmd(cmd.Command)
if err != nil {
return err
}
localCmd := exec.Command(chrootCmdPath, c.Chroot, "/bin/sh", "-c", cmd.Command)
localCmd.Stdin = cmd.Stdin localCmd.Stdin = cmd.Stdin
localCmd.Stdout = cmd.Stdout localCmd.Stdout = cmd.Stdout
localCmd.Stderr = cmd.Stderr localCmd.Stderr = cmd.Stderr
...@@ -46,7 +49,7 @@ func (c *Communicator) Start(cmd *packer.RemoteCmd) error { ...@@ -46,7 +49,7 @@ func (c *Communicator) Start(cmd *packer.RemoteCmd) error {
} }
log.Printf( log.Printf(
"Chroot executation ended with '%d': '%s'", "Chroot executation exited with '%d': '%s'",
exitStatus, cmd.Command) exitStatus, cmd.Command)
cmd.SetExited(exitStatus) cmd.SetExited(exitStatus)
}() }()
...@@ -57,49 +60,47 @@ func (c *Communicator) Start(cmd *packer.RemoteCmd) error { ...@@ -57,49 +60,47 @@ func (c *Communicator) Start(cmd *packer.RemoteCmd) error {
func (c *Communicator) Upload(dst string, r io.Reader) error { func (c *Communicator) Upload(dst string, r io.Reader) error {
dst = filepath.Join(c.Chroot, dst) dst = filepath.Join(c.Chroot, dst)
log.Printf("Uploading to chroot dir: %s", dst) log.Printf("Uploading to chroot dir: %s", dst)
f, err := os.Create(dst) tf, err := ioutil.TempFile("", "packer-amazon-chroot")
if err != nil { 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
} }
defer os.Remove(tf.Name())
return nil 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 { func (c *Communicator) UploadDir(dst string, src string, exclude []string) error {
walkFn := func(fullPath string, info os.FileInfo, err error) error { /*
if err != nil { walkFn := func(fullPath string, info os.FileInfo, err error) error {
return err if err != nil {
} return err
}
path, err := filepath.Rel(src, fullPath) path, err := filepath.Rel(src, fullPath)
if err != nil { if err != nil {
return err return err
} }
for _, e := range exclude { for _, e := range exclude {
if e == path { if e == path {
log.Printf("Skipping excluded file: %s", path) log.Printf("Skipping excluded file: %s", path)
return nil return nil
}
} }
}
dstPath := filepath.Join(dst, path) chrootDest := filepath.Join(c.Chroot, dst, path)
f, err := os.Open(fullPath) log.Printf("Uploading dir %s to chroot dir: %s", src, dst)
if err != nil { cpCmd := fmt.Sprintf("cp %s %s", fullPath, chrootDest)
return err return c.WrappedCommand(cpCmd).Run()
} }
defer f.Close() */
return c.Upload(dstPath, f)
}
log.Printf("Uploading directory '%s' to '%s'", src, dst) // TODO: remove any file copied if it appears in `exclude`
return filepath.Walk(src, walkFn) 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 { func (c *Communicator) Download(src string, w io.Writer) error {
......
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
}
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.")
}
}
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"log" "log"
"os/exec"
) )
// StepChrootProvision provisions the instance within a chroot. // StepChrootProvision provisions the instance within a chroot.
...@@ -15,10 +16,16 @@ func (s *StepChrootProvision) Run(state multistep.StateBag) multistep.StepAction ...@@ -15,10 +16,16 @@ func (s *StepChrootProvision) Run(state multistep.StateBag) multistep.StepAction
hook := state.Get("hook").(packer.Hook) hook := state.Get("hook").(packer.Hook)
mountPath := state.Get("mount_path").(string) mountPath := state.Get("mount_path").(string)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
wrappedCommand := state.Get("wrappedCommand").(Command)
chrootCmd := func(command string) *exec.Cmd {
return ChrootCommand(mountPath, command)
}
// Create our communicator // Create our communicator
comm := &Communicator{ comm := &Communicator{
Chroot: mountPath, Chroot: mountPath,
ChrootCmd: chrootCmd,
WrappedCommand: wrappedCommand,
} }
// Provision // Provision
......
package chroot package chroot
import ( import (
"bytes"
"fmt" "fmt"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"io"
"log" "log"
"os"
"path/filepath" "path/filepath"
) )
...@@ -23,6 +22,8 @@ func (s *StepCopyFiles) Run(state multistep.StateBag) multistep.StepAction { ...@@ -23,6 +22,8 @@ func (s *StepCopyFiles) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config) config := state.Get("config").(*Config)
mountPath := state.Get("mount_path").(string) mountPath := state.Get("mount_path").(string)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
wrappedCommand := state.Get("wrappedCommand").(Command)
stderr := new(bytes.Buffer)
s.files = make([]string, 0, len(config.CopyFiles)) s.files = make([]string, 0, len(config.CopyFiles))
if len(config.CopyFiles) > 0 { if len(config.CopyFiles) > 0 {
...@@ -32,8 +33,12 @@ func (s *StepCopyFiles) Run(state multistep.StateBag) multistep.StepAction { ...@@ -32,8 +33,12 @@ func (s *StepCopyFiles) Run(state multistep.StateBag) multistep.StepAction {
chrootPath := filepath.Join(mountPath, path) chrootPath := filepath.Join(mountPath, path)
log.Printf("Copying '%s' to '%s'", path, chrootPath) log.Printf("Copying '%s' to '%s'", path, chrootPath)
if err := s.copySingle(chrootPath, path); err != nil { cmd := wrappedCommand(fmt.Sprintf("cp %s %s", path, chrootPath))
err := fmt.Errorf("Error copying file: %s", err) 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) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
...@@ -54,11 +59,13 @@ func (s *StepCopyFiles) Cleanup(state multistep.StateBag) { ...@@ -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 { if s.files != nil {
for _, file := range s.files { for _, file := range s.files {
log.Printf("Removing: %s", file) 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 return err
} }
} }
...@@ -67,41 +74,3 @@ func (s *StepCopyFiles) CleanupFunc(multistep.StateBag) error { ...@@ -67,41 +74,3 @@ func (s *StepCopyFiles) CleanupFunc(multistep.StateBag) error {
s.files = nil s.files = nil
return 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
}
...@@ -7,7 +7,6 @@ import ( ...@@ -7,7 +7,6 @@ import (
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"log" "log"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
) )
...@@ -59,8 +58,9 @@ func (s *StepMountDevice) Run(state multistep.StateBag) multistep.StepAction { ...@@ -59,8 +58,9 @@ func (s *StepMountDevice) Run(state multistep.StateBag) multistep.StepAction {
ui.Say("Mounting the root device...") ui.Say("Mounting the root device...")
stderr := new(bytes.Buffer) stderr := new(bytes.Buffer)
mountCommand := fmt.Sprintf("%s %s %s", config.MountCommand, device, mountPath) mountCommand := fmt.Sprintf("mount %s %s", device, mountPath)
cmd := exec.Command("/bin/sh", "-c", mountCommand) wrappedCommand := state.Get("wrappedCommand").(Command)
cmd := wrappedCommand(mountCommand)
cmd.Stderr = stderr cmd.Stderr = stderr
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
err := fmt.Errorf( err := fmt.Errorf(
...@@ -90,12 +90,12 @@ func (s *StepMountDevice) CleanupFunc(state multistep.StateBag) error { ...@@ -90,12 +90,12 @@ func (s *StepMountDevice) CleanupFunc(state multistep.StateBag) error {
return nil return nil
} }
config := state.Get("config").(*Config)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
ui.Say("Unmounting the root device...") ui.Say("Unmounting the root device...")
unmountCommand := fmt.Sprintf("%s %s", config.UnmountCommand, s.mountPath) unmountCommand := fmt.Sprintf("umount %s", s.mountPath)
cmd := exec.Command("/bin/sh", "-c", unmountCommand) wrappedCommand := state.Get("wrappedCommand").(Command)
cmd := wrappedCommand(unmountCommand)
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
return fmt.Errorf("Error unmounting root device: %s", err) return fmt.Errorf("Error unmounting root device: %s", err)
} }
......
...@@ -6,7 +6,6 @@ import ( ...@@ -6,7 +6,6 @@ import (
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"os" "os"
"os/exec"
) )
// StepMountExtra mounts the attached device. // StepMountExtra mounts the attached device.
...@@ -21,6 +20,7 @@ func (s *StepMountExtra) Run(state multistep.StateBag) multistep.StepAction { ...@@ -21,6 +20,7 @@ func (s *StepMountExtra) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config) config := state.Get("config").(*Config)
mountPath := state.Get("mount_path").(string) mountPath := state.Get("mount_path").(string)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
wrappedCommand := state.Get("wrappedCommand").(Command)
s.mounts = make([]string, 0, len(config.ChrootMounts)) s.mounts = make([]string, 0, len(config.ChrootMounts))
...@@ -43,12 +43,11 @@ func (s *StepMountExtra) Run(state multistep.StateBag) multistep.StepAction { ...@@ -43,12 +43,11 @@ func (s *StepMountExtra) Run(state multistep.StateBag) multistep.StepAction {
ui.Message(fmt.Sprintf("Mounting: %s", mountInfo[2])) ui.Message(fmt.Sprintf("Mounting: %s", mountInfo[2]))
stderr := new(bytes.Buffer) stderr := new(bytes.Buffer)
mountCommand := fmt.Sprintf( mountCommand := fmt.Sprintf(
"%s %s %s %s", "mount %s %s %s",
config.MountCommand,
flags, flags,
mountInfo[1], mountInfo[1],
innerPath) innerPath)
cmd := exec.Command("/bin/sh", "-c", mountCommand) cmd := wrappedCommand(mountCommand)
cmd.Stderr = stderr cmd.Stderr = stderr
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
err := fmt.Errorf( err := fmt.Errorf(
...@@ -79,15 +78,15 @@ func (s *StepMountExtra) CleanupFunc(state multistep.StateBag) error { ...@@ -79,15 +78,15 @@ func (s *StepMountExtra) CleanupFunc(state multistep.StateBag) error {
return nil return nil
} }
config := state.Get("config").(*Config) wrappedCommand := state.Get("wrappedCommand").(Command)
for len(s.mounts) > 0 { for len(s.mounts) > 0 {
var path string var path string
lastIndex := len(s.mounts) - 1 lastIndex := len(s.mounts) - 1
path, s.mounts = s.mounts[lastIndex], s.mounts[:lastIndex] 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) stderr := new(bytes.Buffer)
cmd := exec.Command("/bin/sh", "-c", unmountCommand) cmd := wrappedCommand(unmountCommand)
cmd.Stderr = stderr cmd.Stderr = stderr
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
return fmt.Errorf( return fmt.Errorf(
......
...@@ -111,9 +111,11 @@ Optional: ...@@ -111,9 +111,11 @@ Optional:
of the source AMI will be attached. This defaults to "" (empty string), of the source AMI will be attached. This defaults to "" (empty string),
which forces Packer to find an open device automatically. which forces Packer to find an open device automatically.
* `mount_command` (string) - The command to use to mount devices. This * `command_wrapper` (string) - How to run shell commands. This
defaults to "mount". This may be useful to set if you want to set 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. 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 * `mount_path` (string) - The path where the volume will be mounted. This is
where the chroot environment will be. This defaults to where the chroot environment will be. This defaults to
...@@ -123,9 +125,6 @@ Optional: ...@@ -123,9 +125,6 @@ Optional:
* `tags` (object of key/value strings) - Tags applied to the AMI. * `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 ## Basic Example
Here is a basic example. It is completely valid except for the access keys: Here is a basic example. It is completely valid except for the access keys:
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment