Merge pull request #432 from mwhooker/chroot_cmd

build/amazon/chroot: command_wrapper to support sudo-less
This commit is contained in:
Mitchell Hashimoto 2013-09-30 09:00:22 -07:00
commit 68fb788c97
10 changed files with 178 additions and 119 deletions

View File

@ -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 {
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 {
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 {
} }
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
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{

View File

@ -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)
}
}

View File

@ -1,8 +1,12 @@
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 (
"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 {
} }
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 {
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() defer os.Remove(tf.Name())
io.Copy(tf, r)
if _, err := io.Copy(f, r); err != nil { cpCmd := fmt.Sprintf("cp %s %s", tf.Name(), dst)
return err return (c.WrappedCommand(cpCmd)).Run()
}
return nil
} }
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)
if err != nil {
return err
}
for _, e := range exclude {
if e == path {
log.Printf("Skipping excluded file: %s", path)
return nil
} }
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) // TODO: remove any file copied if it appears in `exclude`
f, err := os.Open(fullPath) chrootDest := filepath.Join(c.Chroot, dst)
if err != nil { log.Printf("Uploading directory '%s' to '%s'", src, chrootDest)
return err cpCmd := fmt.Sprintf("cp -R %s* %s", src, chrootDest)
} return c.WrappedCommand(cpCmd).Run()
defer f.Close()
return c.Upload(dstPath, f)
}
log.Printf("Uploading directory '%s' to '%s'", src, dst)
return filepath.Walk(src, walkFn)
} }
func (c *Communicator) Download(src string, w io.Writer) error { func (c *Communicator) Download(src string, w io.Writer) error {

View File

@ -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
}

View File

@ -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.")
}
}

View File

@ -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
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

View File

@ -1,12 +1,11 @@
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 {
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 {
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) {
} }
} }
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 {
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
}

View File

@ -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 {
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 {
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)
} }

View File

@ -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 {
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 {
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 {
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(

View File

@ -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:
* `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: