diff --git a/builder/lxd/artifact.go b/builder/lxd/artifact.go new file mode 100644 index 000000000..481f0758e --- /dev/null +++ b/builder/lxd/artifact.go @@ -0,0 +1,34 @@ +package lxd + +import ( + "fmt" +) + +type Artifact struct { + id string +} + +func (*Artifact) BuilderId() string { + return BuilderId +} + +func (a *Artifact) Files() []string { + return nil +} + +func (a *Artifact) Id() string { + return a.id +} + +func (a *Artifact) String() string { + return fmt.Sprintf("image: %s", a.id) +} + +func (a *Artifact) State(name string) interface{} { + return nil +} + +func (a *Artifact) Destroy() error { + _, err := LXDCommand("image", "delete", a.id) + return err +} diff --git a/builder/lxd/builder.go b/builder/lxd/builder.go new file mode 100644 index 000000000..d59cf5bcf --- /dev/null +++ b/builder/lxd/builder.go @@ -0,0 +1,74 @@ +package lxd + +import ( + "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/packer" + "github.com/hashicorp/packer/template/interpolate" + "github.com/mitchellh/multistep" + "log" +) + +// The unique ID for this builder +const BuilderId = "lxd" + +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{ + &stepLxdLaunch{}, + &StepProvision{}, + &stepPublish{}, + } + + // 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) + } + + artifact := &Artifact{ + id: state.Get("imageFingerprint").(string), + } + + return artifact, nil +} + +func (b *Builder) Cancel() { + if b.runner != nil { + log.Println("Cancelling the step runner...") + b.runner.Cancel() + } +} diff --git a/builder/lxd/builder_test.go b/builder/lxd/builder_test.go new file mode 100644 index 000000000..b2ea70e55 --- /dev/null +++ b/builder/lxd/builder_test.go @@ -0,0 +1,77 @@ +package lxd + +import ( + "os" + "testing" + + "github.com/hashicorp/packer/packer" +) + +func testConfig() map[string]interface{} { + return map[string]interface{}{ + "output_image": "foo", + "image": "bar", + } +} + +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) + } + + // Good, remote image + config = testConfig() + config["image"] = "remote:bar" + 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) + } + + // Good, remote output image + config = testConfig() + config["output_image"] = "remote:foo" + 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 image name + config = testConfig() + delete(config, "image") + 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/lxd/command.go b/builder/lxd/command.go new file mode 100644 index 000000000..c9a39279d --- /dev/null +++ b/builder/lxd/command.go @@ -0,0 +1,43 @@ +package lxd + +import ( + "bytes" + "fmt" + "log" + "os/exec" + "strings" +) + +// 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) +} + +// Yeah...LXD calls `lxc` because the command line is different between the +// packages. This should also avoid a naming collision between the LXC builder. +func LXDCommand(args ...string) (string, error) { + var stdout, stderr bytes.Buffer + + log.Printf("Executing lxc command: %#v", args) + cmd := exec.Command("lxc", 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("LXD command error: %s", stderrString) + } + + log.Printf("stdout: %s", stdoutString) + log.Printf("stderr: %s", stderrString) + + return stdoutString, err +} diff --git a/builder/lxd/communicator.go b/builder/lxd/communicator.go new file mode 100644 index 000000000..8eaa47a5f --- /dev/null +++ b/builder/lxd/communicator.go @@ -0,0 +1,142 @@ +package lxd + +import ( + "fmt" + "github.com/hashicorp/packer/packer" + "io" + "log" + "os" + "os/exec" + "path/filepath" + "syscall" +) + +type Communicator struct { + ContainerName string + CmdWrapper CommandWrapper +} + +func (c *Communicator) 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 exec execution exited with '%d': '%s'", + exitStatus, cmd.Command) + cmd.SetExited(exitStatus) + }() + + return nil +} + +func (c *Communicator) Upload(dst string, r io.Reader, fi *os.FileInfo) error { + cpCmd, err := c.CmdWrapper(fmt.Sprintf("lxc file push - %s", filepath.Join(c.ContainerName, dst))) + if err != nil { + return err + } + + log.Printf("Running copy command: %s", cpCmd) + command := ShellCommand(cpCmd) + command.Stdin = r + + return command.Run() +} + +func (c *Communicator) UploadDir(dst string, src string, exclude []string) error { + // NOTE:lxc file push doesn't yet support directory uploads. + // As a work around, we tar up the folder, upload it as a file, then extract it + + // Don't use 'z' flag as compressing may take longer and the transfer is likely local. + // If this isn't the case, it is possible for the user to compress in another step then transfer. + // It wouldn't be possibe to disable compression, without exposing this option. + tar, err := c.CmdWrapper(fmt.Sprintf("tar -cf - -C %s .", src)) + if err != nil { + return err + } + + cp, err := c.CmdWrapper(fmt.Sprintf("lxc exec %s -- tar -xf - -C %s", c.ContainerName, dst)) + if err != nil { + return err + } + + tarCmd := ShellCommand(tar) + cpCmd := ShellCommand(cp) + + cpCmd.Stdin, _ = tarCmd.StdoutPipe() + log.Printf("Starting tar command: %s", tar) + err = tarCmd.Start() + if err != nil { + return err + } + + log.Printf("Running cp command: %s", cp) + err = cpCmd.Run() + if err != nil { + log.Printf("Error running cp command: %s", err) + return err + } + + err = tarCmd.Wait() + if err != nil { + log.Printf("Error running tar command: %s", err) + return err + } + + return nil +} + +func (c *Communicator) Download(src string, w io.Writer) error { + cpCmd, err := c.CmdWrapper(fmt.Sprintf("lxc file pull %s -", filepath.Join(c.ContainerName, src))) + if err != nil { + return err + } + + log.Printf("Running copy command: %s", cpCmd) + command := ShellCommand(cpCmd) + command.Stdout = w + + return command.Run() +} + +func (c *Communicator) DownloadDir(src string, dst string, exclude []string) error { + // TODO This could probably be "lxc exec -- cd && tar -czf - | tar -xzf - -C " + return fmt.Errorf("DownloadDir is not implemented for lxc") +} + +func (c *Communicator) Execute(commandString string) (*exec.Cmd, error) { + log.Printf("Executing with lxc exec in container: %s %s", c.ContainerName, commandString) + command, err := c.CmdWrapper( + fmt.Sprintf("lxc exec %s -- /bin/sh -c \"%s\"", c.ContainerName, commandString)) + if err != nil { + return nil, err + } + + localCmd := ShellCommand(command) + log.Printf("Executing lxc exec: %s %#v", localCmd.Path, localCmd.Args) + + return localCmd, nil +} diff --git a/builder/lxd/communicator_test.go b/builder/lxd/communicator_test.go new file mode 100644 index 000000000..4a70160b7 --- /dev/null +++ b/builder/lxd/communicator_test.go @@ -0,0 +1,20 @@ +package lxd + +import ( + "github.com/hashicorp/packer/packer" + "testing" +) + +func TestCommunicator_ImplementsCommunicator(t *testing.T) { + var raw interface{} + raw = &Communicator{} + if _, ok := raw.(packer.Communicator); !ok { + t.Fatalf("Communicator should be a communicator") + } +} + +// Acceptance tests +// TODO Execute a command +// TODO Upload a file +// TODO Download a file +// TODO Upload a Directory diff --git a/builder/lxd/config.go b/builder/lxd/config.go new file mode 100644 index 000000000..73de7212e --- /dev/null +++ b/builder/lxd/config.go @@ -0,0 +1,60 @@ +package lxd + +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" + "time" +) + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + OutputImage string `mapstructure:"output_image"` + ContainerName string `mapstructure:"container_name"` + CommandWrapper string `mapstructure:"command_wrapper"` + Image string `mapstructure:"image"` + 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.ContainerName == "" { + c.ContainerName = fmt.Sprintf("packer-%s", c.PackerBuildName) + } + + if c.OutputImage == "" { + c.OutputImage = c.ContainerName + } + + if c.CommandWrapper == "" { + c.CommandWrapper = "{{.Command}}" + } + + if c.Image == "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("`image` is a required parameter for LXD. Please specify an image by alias or fingerprint. e.g. `ubuntu-daily:x`")) + } + + if errs != nil && len(errs.Errors) > 0 { + return nil, errs + } + + return &c, nil +} diff --git a/builder/lxd/step_lxd_launch.go b/builder/lxd/step_lxd_launch.go new file mode 100644 index 000000000..1ec573b18 --- /dev/null +++ b/builder/lxd/step_lxd_launch.go @@ -0,0 +1,50 @@ +package lxd + +import ( + "fmt" + "github.com/hashicorp/packer/packer" + "github.com/mitchellh/multistep" + "time" +) + +type stepLxdLaunch struct{} + +func (s *stepLxdLaunch) Run(state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*Config) + ui := state.Get("ui").(packer.Ui) + + name := config.ContainerName + image := config.Image + + args := []string{ + "launch", "--ephemeral=false", image, name, + } + + ui.Say("Creating container...") + _, err := LXDCommand(args...) + if err != nil { + err := fmt.Errorf("Error creating container: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + // TODO: Should we check `lxc info ` for "Running"? + // We have to do this so /tmp doens't get cleared and lose our provisioner scripts. + time.Sleep(1 * time.Second) + + return multistep.ActionContinue +} + +func (s *stepLxdLaunch) Cleanup(state multistep.StateBag) { + config := state.Get("config").(*Config) + ui := state.Get("ui").(packer.Ui) + + args := []string{ + "delete", "--force", config.ContainerName, + } + + ui.Say("Unregistering and deleting deleting container...") + if _, err := LXDCommand(args...); err != nil { + ui.Error(fmt.Sprintf("Error deleting container: %s", err)) + } +} diff --git a/builder/lxd/step_provision.go b/builder/lxd/step_provision.go new file mode 100644 index 000000000..c46b900e7 --- /dev/null +++ b/builder/lxd/step_provision.go @@ -0,0 +1,34 @@ +package lxd + +import ( + "github.com/hashicorp/packer/packer" + "github.com/mitchellh/multistep" + "log" +) + +// StepProvision provisions the container +type StepProvision struct{} + +func (s *StepProvision) Run(state multistep.StateBag) multistep.StepAction { + hook := state.Get("hook").(packer.Hook) + config := state.Get("config").(*Config) + ui := state.Get("ui").(packer.Ui) + wrappedCommand := state.Get("wrappedCommand").(CommandWrapper) + + // Create our communicator + comm := &Communicator{ + ContainerName: config.ContainerName, + 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/lxd/step_publish.go b/builder/lxd/step_publish.go new file mode 100644 index 000000000..0c0aabd3f --- /dev/null +++ b/builder/lxd/step_publish.go @@ -0,0 +1,54 @@ +package lxd + +import ( + "fmt" + "github.com/hashicorp/packer/packer" + "github.com/mitchellh/multistep" + "regexp" +) + +type stepPublish struct{} + +func (s *stepPublish) Run(state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*Config) + ui := state.Get("ui").(packer.Ui) + + name := config.ContainerName + stop_args := []string{ + // We created the container with "--ephemeral=false" so we know it is safe to stop. + "stop", name, + } + + ui.Say("Stopping container...") + _, err := LXDCommand(stop_args...) + if err != nil { + err := fmt.Errorf("Error stopping container: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + publish_args := []string{ + "publish", name, "--alias", config.OutputImage, + } + + ui.Say("Publishing container...") + stdoutString, err := LXDCommand(publish_args...) + if err != nil { + err := fmt.Errorf("Error publishing container: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + r := regexp.MustCompile("([0-9a-fA-F]+)$") + fingerprint := r.FindAllStringSubmatch(stdoutString, -1)[0][0] + + ui.Say(fmt.Sprintf("Created image: %s", fingerprint)) + + state.Put("imageFingerprint", fingerprint) + + return multistep.ActionContinue +} + +func (s *stepPublish) Cleanup(state multistep.StateBag) {} diff --git a/command/plugin.go b/command/plugin.go index e915ab521..af2fa2985 100644 --- a/command/plugin.go +++ b/command/plugin.go @@ -69,6 +69,8 @@ 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 { @@ -89,6 +91,7 @@ var Builders = map[string]packer.Builder{ "file": new(filebuilder.Builder), "googlecompute": new(googlecomputebuilder.Builder), "hyperv-iso": new(hypervisobuilder.Builder), + "lxd": new(lxdbuilder.Builder), "null": new(nullbuilder.Builder), "oneandone": new(oneandonebuilder.Builder), "openstack": new(openstackbuilder.Builder), diff --git a/website/source/docs/builders/lxd.html.md b/website/source/docs/builders/lxd.html.md new file mode 100644 index 000000000..dd386b1b0 --- /dev/null +++ b/website/source/docs/builders/lxd.html.md @@ -0,0 +1,52 @@ +--- +description: | + The `lxd` Packer builder builds containers for LXD. The builder starts an LXD + container, runs provisioners within this container, then saves the container + as an LXD image. +layout: docs +page_title: LXD Builder +... + +# LXD Builder + +Type: `lxd` + +The `lxd` Packer builder builds containers for LXD. The builder starts an LXD +container, runs provisioners within this container, then saves the container +as an LXD image. + +The LXD builder requires a modern linux kernel and the `lxd` package. +This builder does not work with LXC. + +## Basic Example + +Below is a fully functioning example. + +``` {.javascript} +{ + "builders": [ + { + "type": "lxd", + "name": "lxd-xenial", + "image": "ubuntu-daily:xenial", + "output_image": "ubuntu-xenial" + } + ] +} +``` + +## Configuration Reference + +### Required: + +- `image` (string) - The source image to use when creating the build container. This can be a (local or remote) image (name or fingerprint). E.G. my-base-image, ubuntu-daily:x, 08fababf6f27... + Note: The builder may appear to pause if required to download a remote image, as they are usually 100-200MB. `/var/log/lxd/lxd.log` will mention starting such downloads. + +### Optional: + +- `name` (string) - The name of the started container. Defaults to `packer-$PACKER_BUILD_NAME`. + +- `output_image` (string) - The name of the output artifact. Defaults to `name`. + +- `command_wrapper` (string) - lets you prefix all builder commands, such as with `ssh` for a remote build host. Defaults to `""`. + diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 8cd3159f8..880f1ede9 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -190,8 +190,11 @@ > File + > + LXD + > - PowerShell + webPowerShell > Puppet Masterless