diff --git a/.gitignore b/.gitignore index a23e18aec..31246ce8e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ test/.env *~ *.received.* +*.swp website/.bundle website/vendor diff --git a/builder/lxc/artifact.go b/builder/lxc/artifact.go new file mode 100644 index 000000000..0a53f70e0 --- /dev/null +++ b/builder/lxc/artifact.go @@ -0,0 +1,35 @@ +package lxc + +import ( + "fmt" + "os" +) + +type Artifact struct { + dir string + f []string +} + +func (*Artifact) BuilderId() string { + return BuilderId +} + +func (a *Artifact) Files() []string { + return a.f +} + +func (*Artifact) Id() string { + return "VM" +} + +func (a *Artifact) String() string { + return fmt.Sprintf("VM files in directory: %s", a.dir) +} + +func (a *Artifact) State(name string) interface{} { + return nil +} + +func (a *Artifact) Destroy() error { + return os.RemoveAll(a.dir) +} diff --git a/builder/lxc/builder.go b/builder/lxc/builder.go new file mode 100644 index 000000000..31770b740 --- /dev/null +++ b/builder/lxc/builder.go @@ -0,0 +1,95 @@ +package lxc + +import ( + "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/packer" + "github.com/hashicorp/packer/template/interpolate" + "github.com/mitchellh/multistep" + "log" + "os" + "path/filepath" +) + +// The unique ID for this builder +const BuilderId = "ustream.lxc" + +type wrappedCommandTemplate struct { + Command string +} + +type Builder struct { + config *Config + runner multistep.Runner +} + +func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { + c, errs := NewConfig(raws...) + if errs != nil { + return nil, errs + } + b.config = c + + return nil, nil +} + +func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { + wrappedCommand := func(command string) (string, error) { + b.config.ctx.Data = &wrappedCommandTemplate{Command: command} + return interpolate.Render(b.config.CommandWrapper, &b.config.ctx) + } + + steps := []multistep.Step{ + new(stepPrepareOutputDir), + new(stepLxcCreate), + &StepWaitInit{ + WaitTimeout: b.config.InitTimeout, + }, + new(StepProvision), + new(stepExport), + } + + // Setup the state bag + state := new(multistep.BasicStateBag) + state.Put("config", b.config) + state.Put("cache", cache) + state.Put("hook", hook) + state.Put("ui", ui) + state.Put("wrappedCommand", CommandWrapper(wrappedCommand)) + + // Run + b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state) + b.runner.Run(state) + + // If there was an error, return that + if rawErr, ok := state.GetOk("error"); ok { + return nil, rawErr.(error) + } + + // Compile the artifact list + files := make([]string, 0, 5) + visit := func(path string, info os.FileInfo, err error) error { + if !info.IsDir() { + files = append(files, path) + } + + return err + } + + if err := filepath.Walk(b.config.OutputDir, visit); err != nil { + return nil, err + } + + artifact := &Artifact{ + dir: b.config.OutputDir, + f: files, + } + + return artifact, nil +} + +func (b *Builder) Cancel() { + if b.runner != nil { + log.Println("Cancelling the step runner...") + b.runner.Cancel() + } +} diff --git a/builder/lxc/builder_test.go b/builder/lxc/builder_test.go new file mode 100644 index 000000000..4eeb27594 --- /dev/null +++ b/builder/lxc/builder_test.go @@ -0,0 +1,56 @@ +package lxc + +import ( + "os" + "testing" + + "github.com/hashicorp/packer/packer" +) + +func testConfig() map[string]interface{} { + return map[string]interface{}{ + "config_file": "builder_test.go", + "template_name": "debian", + "template_environment_vars": "SUITE=jessie", + } +} + +func TestBuilder_Foo(t *testing.T) { + if os.Getenv("PACKER_ACC") == "" { + t.Skip("This test is only run with PACKER_ACC=1") + } +} + +func TestBuilderPrepare_ConfigFile(t *testing.T) { + var b Builder + // Good + config := testConfig() + warnings, err := b.Prepare(config) + if len(warnings) > 0 { + t.Fatalf("bad: %#v", warnings) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + // Bad, missing config file + config = testConfig() + delete(config, "config_file") + b = Builder{} + warnings, err = b.Prepare(config) + if len(warnings) > 0 { + t.Fatalf("bad: %#v", warnings) + } + if err == nil { + t.Fatalf("should have error") + } + +} + +func TestBuilder_ImplementsBuilder(t *testing.T) { + var raw interface{} + raw = &Builder{} + if _, ok := raw.(packer.Builder); !ok { + t.Fatalf("Builder should be a builder") + } +} diff --git a/builder/lxc/command.go b/builder/lxc/command.go new file mode 100644 index 000000000..af81cff83 --- /dev/null +++ b/builder/lxc/command.go @@ -0,0 +1,15 @@ +package lxc + +import ( + "os/exec" +) + +// CommandWrapper is a type that given a command, will possibly modify that +// command in-flight. This might return an error. +type CommandWrapper func(string) (string, error) + +// ShellCommand takes a command string and returns an *exec.Cmd to execute +// it within the context of a shell (/bin/sh). +func ShellCommand(command string) *exec.Cmd { + return exec.Command("/bin/sh", "-c", command) +} diff --git a/builder/lxc/communicator.go b/builder/lxc/communicator.go new file mode 100644 index 000000000..8d9765979 --- /dev/null +++ b/builder/lxc/communicator.go @@ -0,0 +1,151 @@ +package lxc + +import ( + "fmt" + "github.com/hashicorp/packer/packer" + "io" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" +) + +type LxcAttachCommunicator struct { + RootFs string + ContainerName string + CmdWrapper CommandWrapper +} + +func (c *LxcAttachCommunicator) Start(cmd *packer.RemoteCmd) error { + localCmd, err := c.Execute(cmd.Command) + + if err != nil { + return err + } + + localCmd.Stdin = cmd.Stdin + localCmd.Stdout = cmd.Stdout + localCmd.Stderr = cmd.Stderr + if err := localCmd.Start(); err != nil { + return err + } + + go func() { + exitStatus := 0 + if err := localCmd.Wait(); 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() + } + } + } + + log.Printf( + "lxc-attach execution exited with '%d': '%s'", + exitStatus, cmd.Command) + cmd.SetExited(exitStatus) + }() + + return nil +} + +func (c *LxcAttachCommunicator) Upload(dst string, r io.Reader, fi *os.FileInfo) error { + dst = filepath.Join(c.RootFs, dst) + log.Printf("Uploading to rootfs: %s", dst) + tf, err := ioutil.TempFile("", "packer-lxc-attach") + if err != nil { + return fmt.Errorf("Error uploading file to rootfs: %s", err) + } + defer os.Remove(tf.Name()) + io.Copy(tf, r) + + cpCmd, err := c.CmdWrapper(fmt.Sprintf("sudo cp %s %s", tf.Name(), dst)) + if err != nil { + return err + } + + log.Printf("Running copy command: %s", dst) + + return ShellCommand(cpCmd).Run() +} + +func (c *LxcAttachCommunicator) UploadDir(dst string, src string, exclude []string) error { + // TODO: remove any file copied if it appears in `exclude` + dest := filepath.Join(c.RootFs, dst) + log.Printf("Uploading directory '%s' to rootfs '%s'", src, dest) + cpCmd, err := c.CmdWrapper(fmt.Sprintf("sudo cp -R %s/. %s", src, dest)) + if err != nil { + return err + } + + return ShellCommand(cpCmd).Run() +} + +func (c *LxcAttachCommunicator) Download(src string, w io.Writer) error { + src = filepath.Join(c.RootFs, src) + log.Printf("Downloading from rootfs dir: %s", src) + f, err := os.Open(src) + if err != nil { + return err + } + defer f.Close() + + if _, err := io.Copy(w, f); err != nil { + return err + } + + return nil +} + +func (c *LxcAttachCommunicator) DownloadDir(src string, dst string, exclude []string) error { + return fmt.Errorf("DownloadDir is not implemented for lxc") +} + +func (c *LxcAttachCommunicator) Execute(commandString string) (*exec.Cmd, error) { + log.Printf("Executing with lxc-attach in container: %s %s %s", c.ContainerName, c.RootFs, commandString) + command, err := c.CmdWrapper( + fmt.Sprintf("sudo lxc-attach --name %s -- /bin/sh -c \"%s\"", c.ContainerName, commandString)) + if err != nil { + return nil, err + } + + localCmd := ShellCommand(command) + log.Printf("Executing lxc-attach: %s %#v", localCmd.Path, localCmd.Args) + + return localCmd, nil +} + +func (c *LxcAttachCommunicator) CheckInit() (string, error) { + log.Printf("Debug runlevel exec") + localCmd, err := c.Execute("/sbin/runlevel") + + if err != nil { + return "", err + } + + pr, _ := localCmd.StdoutPipe() + if err = localCmd.Start(); err != nil { + return "", err + } + + output, err := ioutil.ReadAll(pr) + + if err != nil { + return "", err + } + + err = localCmd.Wait() + + if err != nil { + return "", err + } + + return strings.TrimSpace(string(output)), nil +} diff --git a/builder/lxc/communicator_test.go b/builder/lxc/communicator_test.go new file mode 100644 index 000000000..854ba6680 --- /dev/null +++ b/builder/lxc/communicator_test.go @@ -0,0 +1,14 @@ +package lxc + +import ( + "github.com/hashicorp/packer/packer" + "testing" +) + +func TestCommunicator_ImplementsCommunicator(t *testing.T) { + var raw interface{} + raw = &LxcAttachCommunicator{} + if _, ok := raw.(packer.Communicator); !ok { + t.Fatalf("Communicator should be a communicator") + } +} diff --git a/builder/lxc/config.go b/builder/lxc/config.go new file mode 100644 index 000000000..c3c28d4fb --- /dev/null +++ b/builder/lxc/config.go @@ -0,0 +1,79 @@ +package lxc + +import ( + "fmt" + "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/helper/config" + "github.com/hashicorp/packer/packer" + "github.com/hashicorp/packer/template/interpolate" + "github.com/mitchellh/mapstructure" + "os" + "time" +) + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + ConfigFile string `mapstructure:"config_file"` + OutputDir string `mapstructure:"output_directory"` + ContainerName string `mapstructure:"container_name"` + CommandWrapper string `mapstructure:"command_wrapper"` + RawInitTimeout string `mapstructure:"init_timeout"` + Name string `mapstructure:"template_name"` + Parameters []string `mapstructure:"template_parameters"` + EnvVars []string `mapstructure:"template_environment_vars"` + TargetRunlevel int `mapstructure:"target_runlevel"` + InitTimeout time.Duration + + ctx interpolate.Context +} + +func NewConfig(raws ...interface{}) (*Config, error) { + var c Config + + var md mapstructure.Metadata + err := config.Decode(&c, &config.DecodeOpts{ + Metadata: &md, + Interpolate: true, + }, raws...) + if err != nil { + return nil, err + } + + // Accumulate any errors + var errs *packer.MultiError + + if c.OutputDir == "" { + c.OutputDir = fmt.Sprintf("output-%s", c.PackerBuildName) + } + + if c.ContainerName == "" { + c.ContainerName = fmt.Sprintf("packer-%s", c.PackerBuildName) + } + + if c.TargetRunlevel == 0 { + c.TargetRunlevel = 3 + } + + if c.CommandWrapper == "" { + c.CommandWrapper = "{{.Command}}" + } + + if c.RawInitTimeout == "" { + c.RawInitTimeout = "20s" + } + + c.InitTimeout, err = time.ParseDuration(c.RawInitTimeout) + if err != nil { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed parsing init_timeout: %s", err)) + } + + if _, err := os.Stat(c.ConfigFile); os.IsNotExist(err) { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("LXC Config file appears to be missing: %s", c.ConfigFile)) + } + + if errs != nil && len(errs.Errors) > 0 { + return nil, errs + } + + return &c, nil +} diff --git a/builder/lxc/step_export.go b/builder/lxc/step_export.go new file mode 100644 index 000000000..59e4b79e9 --- /dev/null +++ b/builder/lxc/step_export.go @@ -0,0 +1,98 @@ +package lxc + +import ( + "bytes" + "fmt" + "github.com/hashicorp/packer/packer" + "github.com/mitchellh/multistep" + "io" + "log" + "os" + "os/exec" + "path/filepath" + "strings" +) + +type stepExport struct{} + +func (s *stepExport) Run(state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*Config) + ui := state.Get("ui").(packer.Ui) + + name := config.ContainerName + + containerDir := fmt.Sprintf("/var/lib/lxc/%s", name) + outputPath := filepath.Join(config.OutputDir, "rootfs.tar.gz") + configFilePath := filepath.Join(config.OutputDir, "lxc-config") + + configFile, err := os.Create(configFilePath) + + if err != nil { + err := fmt.Errorf("Error creating config file: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + originalConfigFile, err := os.Open(config.ConfigFile) + + if err != nil { + err := fmt.Errorf("Error opening config file: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + _, err = io.Copy(configFile, originalConfigFile) + + commands := make([][]string, 4) + commands[0] = []string{ + "lxc-stop", "--name", name, + } + commands[1] = []string{ + "tar", "-C", containerDir, "--numeric-owner", "--anchored", "--exclude=./rootfs/dev/log", "-czf", outputPath, "./rootfs", + } + commands[2] = []string{ + "chmod", "+x", configFilePath, + } + commands[3] = []string{ + "sh", "-c", "chown $USER:`id -gn` " + filepath.Join(config.OutputDir, "*"), + } + + ui.Say("Exporting container...") + for _, command := range commands { + err := s.SudoCommand(command...) + if err != nil { + err := fmt.Errorf("Error exporting container: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + return multistep.ActionContinue +} + +func (s *stepExport) Cleanup(state multistep.StateBag) {} + +func (s *stepExport) SudoCommand(args ...string) error { + var stdout, stderr bytes.Buffer + + log.Printf("Executing sudo command: %#v", args) + cmd := exec.Command("sudo", args...) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + + stdoutString := strings.TrimSpace(stdout.String()) + stderrString := strings.TrimSpace(stderr.String()) + + if _, ok := err.(*exec.ExitError); ok { + err = fmt.Errorf("Sudo command error: %s", stderrString) + } + + log.Printf("stdout: %s", stdoutString) + log.Printf("stderr: %s", stderrString) + + return err +} diff --git a/builder/lxc/step_lxc_create.go b/builder/lxc/step_lxc_create.go new file mode 100644 index 000000000..a98926ffa --- /dev/null +++ b/builder/lxc/step_lxc_create.go @@ -0,0 +1,89 @@ +package lxc + +import ( + "bytes" + "fmt" + "github.com/hashicorp/packer/packer" + "github.com/mitchellh/multistep" + "log" + "os/exec" + "path/filepath" + "strings" +) + +type stepLxcCreate struct{} + +func (s *stepLxcCreate) Run(state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*Config) + ui := state.Get("ui").(packer.Ui) + + name := config.ContainerName + + // TODO: read from env + lxc_dir := "/var/lib/lxc" + rootfs := filepath.Join(lxc_dir, name, "rootfs") + + if config.PackerForce { + s.Cleanup(state) + } + + commands := make([][]string, 3) + commands[0] = append(config.EnvVars, []string{"lxc-create", "-n", name, "-t", config.Name, "--"}...) + commands[0] = append(commands[0], config.Parameters...) + // prevent tmp from being cleaned on boot, we put provisioning scripts there + // todo: wait for init to finish before moving on to provisioning instead of this + commands[1] = []string{"touch", filepath.Join(rootfs, "tmp", ".tmpfs")} + commands[2] = []string{"lxc-start", "-d", "--name", name} + + ui.Say("Creating container...") + for _, command := range commands { + log.Printf("Executing sudo command: %#v", command) + err := s.SudoCommand(command...) + if err != nil { + err := fmt.Errorf("Error creating container: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + state.Put("mount_path", rootfs) + + return multistep.ActionContinue +} + +func (s *stepLxcCreate) Cleanup(state multistep.StateBag) { + config := state.Get("config").(*Config) + ui := state.Get("ui").(packer.Ui) + + command := []string{ + "lxc-destroy", "-f", "-n", config.ContainerName, + } + + ui.Say("Unregistering and deleting virtual machine...") + if err := s.SudoCommand(command...); err != nil { + ui.Error(fmt.Sprintf("Error deleting virtual machine: %s", err)) + } +} + +func (s *stepLxcCreate) SudoCommand(args ...string) error { + var stdout, stderr bytes.Buffer + + log.Printf("Executing sudo command: %#v", args) + cmd := exec.Command("sudo", args...) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + + stdoutString := strings.TrimSpace(stdout.String()) + stderrString := strings.TrimSpace(stderr.String()) + + if _, ok := err.(*exec.ExitError); ok { + err = fmt.Errorf("Sudo command error: %s", stderrString) + } + + log.Printf("stdout: %s", stdoutString) + log.Printf("stderr: %s", stderrString) + + return err +} diff --git a/builder/lxc/step_prepare_output_dir.go b/builder/lxc/step_prepare_output_dir.go new file mode 100644 index 000000000..07c6f08b7 --- /dev/null +++ b/builder/lxc/step_prepare_output_dir.go @@ -0,0 +1,49 @@ +package lxc + +import ( + "github.com/hashicorp/packer/packer" + "github.com/mitchellh/multistep" + "log" + "os" + "time" +) + +type stepPrepareOutputDir struct{} + +func (stepPrepareOutputDir) Run(state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*Config) + ui := state.Get("ui").(packer.Ui) + + if _, err := os.Stat(config.OutputDir); err == nil && config.PackerForce { + ui.Say("Deleting previous output directory...") + os.RemoveAll(config.OutputDir) + } + + if err := os.MkdirAll(config.OutputDir, 0755); err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (stepPrepareOutputDir) Cleanup(state multistep.StateBag) { + _, cancelled := state.GetOk(multistep.StateCancelled) + _, halted := state.GetOk(multistep.StateHalted) + + if cancelled || halted { + config := state.Get("config").(*Config) + ui := state.Get("ui").(packer.Ui) + + ui.Say("Deleting output directory...") + for i := 0; i < 5; i++ { + err := os.RemoveAll(config.OutputDir) + if err == nil { + break + } + + log.Printf("Error removing output dir: %s", err) + time.Sleep(2 * time.Second) + } + } +} diff --git a/builder/lxc/step_provision.go b/builder/lxc/step_provision.go new file mode 100644 index 000000000..f91eb56ce --- /dev/null +++ b/builder/lxc/step_provision.go @@ -0,0 +1,36 @@ +package lxc + +import ( + "github.com/hashicorp/packer/packer" + "github.com/mitchellh/multistep" + "log" +) + +// StepProvision provisions the instance within a chroot. +type StepProvision struct{} + +func (s *StepProvision) Run(state multistep.StateBag) multistep.StepAction { + hook := state.Get("hook").(packer.Hook) + config := state.Get("config").(*Config) + mountPath := state.Get("mount_path").(string) + ui := state.Get("ui").(packer.Ui) + wrappedCommand := state.Get("wrappedCommand").(CommandWrapper) + + // Create our communicator + comm := &LxcAttachCommunicator{ + ContainerName: config.ContainerName, + RootFs: mountPath, + CmdWrapper: wrappedCommand, + } + + // Provision + log.Println("Running the provision hook") + if err := hook.Run(packer.HookProvision, ui, comm, nil); err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *StepProvision) Cleanup(state multistep.StateBag) {} diff --git a/builder/lxc/step_wait_init.go b/builder/lxc/step_wait_init.go new file mode 100644 index 000000000..e5d375312 --- /dev/null +++ b/builder/lxc/step_wait_init.go @@ -0,0 +1,102 @@ +package lxc + +import ( + "errors" + "fmt" + "github.com/hashicorp/packer/packer" + "github.com/mitchellh/multistep" + "log" + "strings" + "time" +) + +type StepWaitInit struct { + WaitTimeout time.Duration +} + +func (s *StepWaitInit) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + + var err error + + cancel := make(chan struct{}) + waitDone := make(chan bool, 1) + go func() { + ui.Say("Waiting for container to finish init...") + err = s.waitForInit(state, cancel) + waitDone <- true + }() + + log.Printf("Waiting for container to finish init, up to timeout: %s", s.WaitTimeout) + timeout := time.After(s.WaitTimeout) +WaitLoop: + for { + select { + case <-waitDone: + if err != nil { + ui.Error(fmt.Sprintf("Error waiting for container to finish init: %s", err)) + return multistep.ActionHalt + } + + ui.Say("Container finished init!") + break WaitLoop + case <-timeout: + err := fmt.Errorf("Timeout waiting for container to finish init.") + state.Put("error", err) + ui.Error(err.Error()) + close(cancel) + return multistep.ActionHalt + case <-time.After(1 * time.Second): + if _, ok := state.GetOk(multistep.StateCancelled); ok { + close(cancel) + log.Println("Interrupt detected, quitting waiting for container to finish init.") + return multistep.ActionHalt + } + } + } + + return multistep.ActionContinue +} + +func (s *StepWaitInit) Cleanup(multistep.StateBag) { +} + +func (s *StepWaitInit) waitForInit(state multistep.StateBag, cancel <-chan struct{}) error { + config := state.Get("config").(*Config) + mountPath := state.Get("mount_path").(string) + wrappedCommand := state.Get("wrappedCommand").(CommandWrapper) + + for { + select { + case <-cancel: + log.Println("Cancelled. Exiting loop.") + return errors.New("Wait cancelled") + case <-time.After(1 * time.Second): + } + + comm := &LxcAttachCommunicator{ + ContainerName: config.ContainerName, + RootFs: mountPath, + CmdWrapper: wrappedCommand, + } + + runlevel, _ := comm.CheckInit() + currentRunlevel := "unknown" + if arr := strings.Split(runlevel, " "); len(arr) >= 2 { + currentRunlevel = arr[1] + } + + log.Printf("Current runlevel in container: '%s'", runlevel) + + targetRunlevel := fmt.Sprintf("%d", config.TargetRunlevel) + if currentRunlevel == targetRunlevel { + log.Printf("Container finished init.") + break + } else if currentRunlevel > targetRunlevel { + log.Printf("Expected Runlevel %s, Got Runlevel %s, continuing", targetRunlevel, currentRunlevel) + break + } + } + + return nil +} diff --git a/command/plugin.go b/command/plugin.go index af2fa2985..cc87e81dc 100644 --- a/command/plugin.go +++ b/command/plugin.go @@ -26,6 +26,8 @@ import ( filebuilder "github.com/hashicorp/packer/builder/file" googlecomputebuilder "github.com/hashicorp/packer/builder/googlecompute" hypervisobuilder "github.com/hashicorp/packer/builder/hyperv/iso" + lxcbuilder "github.com/hashicorp/packer/builder/lxc" + lxdbuilder "github.com/hashicorp/packer/builder/lxd" nullbuilder "github.com/hashicorp/packer/builder/null" oneandonebuilder "github.com/hashicorp/packer/builder/oneandone" openstackbuilder "github.com/hashicorp/packer/builder/openstack" @@ -69,8 +71,6 @@ import ( shelllocalprovisioner "github.com/hashicorp/packer/provisioner/shell-local" windowsrestartprovisioner "github.com/hashicorp/packer/provisioner/windows-restart" windowsshellprovisioner "github.com/hashicorp/packer/provisioner/windows-shell" - - lxdbuilder "github.com/hashicorp/packer/builder/lxd" ) type PluginCommand struct { @@ -91,6 +91,7 @@ var Builders = map[string]packer.Builder{ "file": new(filebuilder.Builder), "googlecompute": new(googlecomputebuilder.Builder), "hyperv-iso": new(hypervisobuilder.Builder), + "lxc": new(lxcbuilder.Builder), "lxd": new(lxdbuilder.Builder), "null": new(nullbuilder.Builder), "oneandone": new(oneandonebuilder.Builder), diff --git a/plugin/builder-lxc/main.go b/plugin/builder-lxc/main.go new file mode 100644 index 000000000..0329b8c8a --- /dev/null +++ b/plugin/builder-lxc/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "github.com/hashicorp/packer/builder/lxc" + "github.com/hashicorp/packer/packer/plugin" +) + +func main() { + server, err := plugin.Server() + if err != nil { + panic(err) + } + server.RegisterBuilder(new(lxc.Builder)) + server.Serve() +} diff --git a/test/builder_lxc.bats b/test/builder_lxc.bats new file mode 100644 index 000000000..c29030424 --- /dev/null +++ b/test/builder_lxc.bats @@ -0,0 +1,40 @@ +#!/usr/bin/env bats +# +# This tests the lxc builder. The teardown function will +# delete any images in the output-lxc-* folders. + +#load test_helper +#fixtures builder-lxc +FIXTURE_ROOT="$BATS_TEST_DIRNAME/fixtures/builder-lxc" + +# Required parameters +command -v lxc-create >/dev/null 2>&1 || { + echo "'lxc-create' must be installed via the lxc (or lxc1 for ubuntu >=16.04) package" >&2 + exit 1 +} + +teardown() { + rm -rf output-lxc-* +} + +@test "lxc: build centos minimal.json" { + run packer build -var template_name=centos $FIXTURE_ROOT/minimal.json + [ "$status" -eq 0 ] + [ -f output-lxc-centos/rootfs.tar.gz ] + [ -f output-lxc-centos/lxc-config ] +} + + +@test "lxc: build trusty minimal.json" { + run packer build -var template_name=ubuntu -var template_parameters="SUITE=trusty" $FIXTURE_ROOT/minimal.json + [ "$status" -eq 0 ] + [ -f output-lxc-ubuntu/rootfs.tar.gz ] + [ -f output-lxc-ubuntu/lxc-config ] +} + +@test "lxc: build debian minimal.json" { + run packer build -var template_name=debian -var template_parameters="SUITE=jessie" $FIXTURE_ROOT/minimal.json + [ "$status" -eq 0 ] + [ -f output-lxc-debian/rootfs.tar.gz ] + [ -f output-lxc-debian/lxc-config ] +} diff --git a/test/fixtures/builder-lxc/minimal.json b/test/fixtures/builder-lxc/minimal.json new file mode 100644 index 000000000..5bf7998fd --- /dev/null +++ b/test/fixtures/builder-lxc/minimal.json @@ -0,0 +1,15 @@ +{ + "variables": { + "template_name": "debian", + "template_parameters": "SUITE=jessie" + }, + "builders": [ + { + "type": "lxc", + "name": "lxc-{{user `template_name`}}", + "template_name": "{{user `template_name`}}", + "config_file": "/usr/share/lxc/config/{{user `template_name`}}.common.conf", + "template_environment_vars": [ "{{user `template_parameters`}}" ] + } + ] +} diff --git a/website/source/docs/builders/lxc.html.md b/website/source/docs/builders/lxc.html.md new file mode 100644 index 000000000..0dc207a9e --- /dev/null +++ b/website/source/docs/builders/lxc.html.md @@ -0,0 +1,97 @@ +--- +description: | + The `lxc` Packer builder builds containers for lxc1. The builder starts an LXC + container, runs provisioners within this container, then exports the container + as a tar.gz of the root file system. +layout: docs +page_title: LXC Builder +... + +# LXC Builder + +Type: `lxc` + +The `lxc` Packer builder builds containers for lxc1. The builder starts an LXC +container, runs provisioners within this container, then exports the container +as a tar.gz of the root file system. + +The LXC builder requires a modern linux kernel and the `lxc` or `lxc1` package. +This builder does not work with LXD. + +Note to build Centos images on a Debian family host, you will need the `yum` package installed. + +Some provisioners such as `ansible-local` get confused when running in a container of a different family. +E.G. it will attempt to use `apt-get` to install packages, when running in a Centos container if the parent OS is Debian based. + +## Basic Example + +Below is a fully functioning example. + +``` {.javascript} +{ + "builders": [ + { + "type": "lxc", + "name": "lxc-trusty", + "config_file": "/tmp/lxc/config", + "template_name": "ubuntu", + "template_environment_vars": [ + "SUITE=trusty" + ] + }, + { + "type": "lxc", + "name": "lxc-xenial", + "config_file": "/tmp/lxc/config", + "template_name": "ubuntu", + "template_environment_vars": [ + "SUITE=xenial" + ] + }, + { + "type": "lxc", + "name": "lxc-jessie", + "config_file": "/tmp/lxc/config", + "template_name": "debian", + "template_environment_vars": [ + "SUITE=jessie" + ] + }, + { + "type": "lxc", + "name": "lxc-centos-7-x64", + "config_file": "/tmp/lxc/config", + "template_name": "centos", + "template_parameters": [ + "-R","7", + "-a","x86_64" + ] + } + ] +} +``` + +## Configuration Reference + +### Required: + +- `config_file` (string) - The path to the lxc configuration file. + +- `template_name` (string) - The LXC template name to use. + +- `template_environment_vars` (array of strings) - Environmental variables to use to build the template with. + +### Optional: + +- `target_runlevel` (int) - The minimum run level to wait for the container to reach. Note some distributions (Ubuntu) simulate run levels and may report 5 rather than 3. + +- `output_directory` (string) - The directory in which to save the exported tar.gz. Defaults to `output-` in the current directory. + +- `container_name` (string) - The name of the LXC container. Usually stored in `/var/lib/lxc/containers/`. Defaults to `packer-`. + +- `command_wrapper` (string) - Allows you to specify a wrapper command, such as `ssh` so you can execute packer builds on a remote host. Defaults to Empty. + +- `init_timeout` (string) - The timeout in seconds to wait for the the container to start. Defaults to 20 seconds. + +- `template_parameters` (array of strings) - Options to pass to the given `lxc-template` command, usually located in `/usr/share/lxc/templates/lxc-``. Note: This gets passed as ARGV to the template command. Ensure you have an array of strings, as a single string with spaces probably won't work. Defaults to `[]`. +