commit
4d89ab3943
|
@ -10,6 +10,7 @@
|
||||||
test/.env
|
test/.env
|
||||||
*~
|
*~
|
||||||
*.received.*
|
*.received.*
|
||||||
|
*.swp
|
||||||
|
|
||||||
website/.bundle
|
website/.bundle
|
||||||
website/vendor
|
website/vendor
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {}
|
|
@ -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
|
||||||
|
}
|
|
@ -26,6 +26,8 @@ import (
|
||||||
filebuilder "github.com/hashicorp/packer/builder/file"
|
filebuilder "github.com/hashicorp/packer/builder/file"
|
||||||
googlecomputebuilder "github.com/hashicorp/packer/builder/googlecompute"
|
googlecomputebuilder "github.com/hashicorp/packer/builder/googlecompute"
|
||||||
hypervisobuilder "github.com/hashicorp/packer/builder/hyperv/iso"
|
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"
|
nullbuilder "github.com/hashicorp/packer/builder/null"
|
||||||
oneandonebuilder "github.com/hashicorp/packer/builder/oneandone"
|
oneandonebuilder "github.com/hashicorp/packer/builder/oneandone"
|
||||||
openstackbuilder "github.com/hashicorp/packer/builder/openstack"
|
openstackbuilder "github.com/hashicorp/packer/builder/openstack"
|
||||||
|
@ -69,8 +71,6 @@ import (
|
||||||
shelllocalprovisioner "github.com/hashicorp/packer/provisioner/shell-local"
|
shelllocalprovisioner "github.com/hashicorp/packer/provisioner/shell-local"
|
||||||
windowsrestartprovisioner "github.com/hashicorp/packer/provisioner/windows-restart"
|
windowsrestartprovisioner "github.com/hashicorp/packer/provisioner/windows-restart"
|
||||||
windowsshellprovisioner "github.com/hashicorp/packer/provisioner/windows-shell"
|
windowsshellprovisioner "github.com/hashicorp/packer/provisioner/windows-shell"
|
||||||
|
|
||||||
lxdbuilder "github.com/hashicorp/packer/builder/lxd"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type PluginCommand struct {
|
type PluginCommand struct {
|
||||||
|
@ -91,6 +91,7 @@ var Builders = map[string]packer.Builder{
|
||||||
"file": new(filebuilder.Builder),
|
"file": new(filebuilder.Builder),
|
||||||
"googlecompute": new(googlecomputebuilder.Builder),
|
"googlecompute": new(googlecomputebuilder.Builder),
|
||||||
"hyperv-iso": new(hypervisobuilder.Builder),
|
"hyperv-iso": new(hypervisobuilder.Builder),
|
||||||
|
"lxc": new(lxcbuilder.Builder),
|
||||||
"lxd": new(lxdbuilder.Builder),
|
"lxd": new(lxdbuilder.Builder),
|
||||||
"null": new(nullbuilder.Builder),
|
"null": new(nullbuilder.Builder),
|
||||||
"oneandone": new(oneandonebuilder.Builder),
|
"oneandone": new(oneandonebuilder.Builder),
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
|
@ -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 ]
|
||||||
|
}
|
|
@ -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`}}" ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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-<BuildName>` in the current directory.
|
||||||
|
|
||||||
|
- `container_name` (string) - The name of the LXC container. Usually stored in `/var/lib/lxc/containers/<container_name>`. Defaults to `packer-<BuildName>`.
|
||||||
|
|
||||||
|
- `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-<template_name>``. 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 `[]`.
|
||||||
|
|
Loading…
Reference in New Issue