From d37f287f8c7f2693b8aa03fa392a4a94459aa52f Mon Sep 17 00:00:00 2001 From: James Turnbull Date: Thu, 1 Aug 2013 20:05:23 -0400 Subject: [PATCH] Added basic Puppet provisioner --- config.go | 3 +- plugin/provisioner-puppet/main.go | 10 + provisioner/puppet/provisioner.go | 262 +++++++++++++++++++++++++ provisioner/puppet/provisioner_test.go | 20 ++ 4 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 plugin/provisioner-puppet/main.go create mode 100644 provisioner/puppet/provisioner.go create mode 100644 provisioner/puppet/provisioner_test.go diff --git a/config.go b/config.go index 9b2c22a44..207662e4d 100644 --- a/config.go +++ b/config.go @@ -43,7 +43,8 @@ const defaultConfig = ` "chef-solo": "packer-provisioner-chef-solo", "file": "packer-provisioner-file", "shell": "packer-provisioner-shell", - "salt-masterless": "packer-provisioner-salt-masterless" + "salt-masterless": "packer-provisioner-salt-masterless", + "puppet": "packer-provisioner-puppet" } } ` diff --git a/plugin/provisioner-puppet/main.go b/plugin/provisioner-puppet/main.go new file mode 100644 index 000000000..09f330bce --- /dev/null +++ b/plugin/provisioner-puppet/main.go @@ -0,0 +1,10 @@ +package main + +import ( + "github.com/mitchellh/packer/packer/plugin" + "github.com/mitchellh/packer/provisioner/puppet" +) + +func main() { + plugin.ServeProvisioner(new(puppet.Provisioner)) +} diff --git a/provisioner/puppet/provisioner.go b/provisioner/puppet/provisioner.go new file mode 100644 index 000000000..5eb6d7320 --- /dev/null +++ b/provisioner/puppet/provisioner.go @@ -0,0 +1,262 @@ +// This package implements a provisioner for Packer that executes +// Puppet within the remote machine +package puppet + +import ( + "bytes" + "fmt" + "github.com/mitchellh/iochan" + "github.com/mitchellh/mapstructure" + "github.com/mitchellh/packer/packer" + "io" + "log" + "os" + "path/filepath" + "strings" + "text/template" +) + +const ( + RemoteStagingPath = "/tmp/provision/puppet" + DefaultModulePath = "modules" + DefaultManifestPath = "manifests" + DefaultManifestFile = "site.pp" +) + +var Ui packer.Ui + +type config struct { + // An array of local paths of modules to upload. + ModulePath string `mapstructure:"module_path"` + + // Path to the manifests + ManifestPath string `mapstructure:"manifest_path"` + + // Manifest file + ManifestFile string `mapstructure:"manifest_file"` + + // Option to avoid sudo use when executing commands. Defaults to false. + PreventSudo bool `mapstructure:"prevent_sudo"` +} + +type Provisioner struct { + config config +} + +type ExecuteManifestTemplate struct { + Sudo bool + Modulepath string + Manifest string +} + +func (p *Provisioner) Prepare(raws ...interface{}) error { + errs := make([]error, 0) + for _, raw := range raws { + if err := mapstructure.Decode(raw, &p.config); err != nil { + return err + } + } + + if p.config.ModulePath == "" { + p.config.ModulePath = DefaultModulePath + } + + if p.config.ManifestPath == "" { + p.config.ManifestPath = DefaultManifestPath + } + + if p.config.ManifestFile == "" { + p.config.ManifestFile = DefaultManifestFile + } + + if p.config.ModulePath != "" { + pFileInfo, err := os.Stat(p.config.ModulePath) + + if err != nil || !pFileInfo.IsDir() { + errs = append(errs, fmt.Errorf("Bad module path '%s': %s", p.config.ModulePath, err)) + } + } + + if p.config.ManifestPath != "" { + pFileInfo, err := os.Stat(p.config.ManifestPath) + + if err != nil || !pFileInfo.IsDir() { + errs = append(errs, fmt.Errorf("Bad manifest path '%s': %s", p.config.ManifestPath, err)) + } + } + + if p.config.ManifestFile != "" { + path := filepath.Join(p.config.ManifestPath, p.config.ManifestFile) + if _, err := os.Stat(path); os.IsNotExist(err) { + errs = append(errs, fmt.Errorf("No manifest file '%s': %s", path, err)) + } + } + + if len(errs) > 0 { + return &packer.MultiError{errs} + } + + return nil +} + +func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { + var err error + Ui = ui + + err = CreateRemoteDirectory(RemoteStagingPath, comm) + if err != nil { + return fmt.Errorf("Error creating remote staging directory: %s", err) + } + + // Upload all modules + ui.Say(fmt.Sprintf("Copying module path: %s", p.config.ModulePath)) + err = UploadLocalDirectory(p.config.ModulePath, comm) + if err != nil { + return fmt.Errorf("Error uploading modules: %s", err) + } + + // Upload manifests + ui.Say(fmt.Sprintf("Copying manifests: %s", p.config.ManifestPath)) + err = UploadLocalDirectory(p.config.ManifestPath, comm) + if err != nil { + return fmt.Errorf("Error uploading manifests: %s", err) + } + + // Execute Puppet + ui.Say("Beginning Puppet run") + + // Compile the command + var command bytes.Buffer + mpath := filepath.Join(RemoteStagingPath, p.config.ManifestPath) + manifest := filepath.Join(mpath, p.config.ManifestFile) + modulepath := filepath.Join(RemoteStagingPath, p.config.ModulePath) + t := template.Must(template.New("puppet-run").Parse("{{if .Sudo}}sudo {{end}}puppet apply --verbose --modulepath={{.Modulepath}} {{.Manifest}}")) + t.Execute(&command, &ExecuteManifestTemplate{!p.config.PreventSudo, modulepath, manifest}) + + err = executeCommand(command.String(), comm) + if err != nil { + return fmt.Errorf("Error running Puppet: %s", err) + } + + return nil +} + +func (p *Provisioner) Cancel() { + // Just hard quit. It isn't a big deal if what we're doing keeps + // running on the other side. + os.Exit(0) +} + +func UploadLocalDirectory(localDir string, comm packer.Communicator) (err error) { + visitPath := func(path string, f os.FileInfo, err error) (err2 error) { + var remotePath = RemoteStagingPath + "/" + path + if f.IsDir() { + // Make remote directory + err = CreateRemoteDirectory(remotePath, comm) + if err != nil { + return err + } + } else { + // Upload file to existing directory + file, err := os.Open(path) + if err != nil { + return fmt.Errorf("Error opening file: %s", err) + } + + err = comm.Upload(remotePath, file) + if err != nil { + return fmt.Errorf("Error uploading file: %s", err) + } + } + return + } + + log.Printf("Uploading directory %s", localDir) + err = filepath.Walk(localDir, visitPath) + if err != nil { + return fmt.Errorf("Error uploading modules %s: %s", localDir, err) + } + + return nil +} + +func CreateRemoteDirectory(path string, comm packer.Communicator) (err error) { + log.Printf("Creating remote directory: %s ", path) + + var copyCommand = []string{"mkdir -p", path} + + var cmd packer.RemoteCmd + cmd.Command = strings.Join(copyCommand, " ") + + var stdout bytes.Buffer + cmd.Stdout = &stdout + + // Start the command + if err := comm.Start(&cmd); err != nil { + return fmt.Errorf("Unable to create remote directory %s: %d", path, err) + } + + // Wait for it to complete + cmd.Wait() + + return +} + +func executeCommand(command string, comm packer.Communicator) (err error) { + // Setup the remote command + stdout_r, stdout_w := io.Pipe() + stderr_r, stderr_w := io.Pipe() + + var cmd packer.RemoteCmd + cmd.Command = command + cmd.Stdout = stdout_w + cmd.Stderr = stderr_w + + log.Printf("Executing command: %s", cmd.Command) + err = comm.Start(&cmd) + if err != nil { + return fmt.Errorf("Failed executing command: %s", err) + } + + exitChan := make(chan int, 1) + stdoutChan := iochan.DelimReader(stdout_r, '\n') + stderrChan := iochan.DelimReader(stderr_r, '\n') + + go func() { + defer stdout_w.Close() + defer stderr_w.Close() + + cmd.Wait() + exitChan <- cmd.ExitStatus + }() + +OutputLoop: + for { + select { + case output := <-stderrChan: + Ui.Message(strings.TrimSpace(output)) + case output := <-stdoutChan: + Ui.Message(strings.TrimSpace(output)) + case exitStatus := <-exitChan: + log.Printf("Puppet provisioner exited with status %d", exitStatus) + + if exitStatus != 0 { + return fmt.Errorf("Command exited with non-zero exit status: %d", exitStatus) + } + + break OutputLoop + } + } + + // Make sure we finish off stdout/stderr because we may have gotten + // a message from the exit channel first. + for output := range stdoutChan { + Ui.Message(output) + } + + for output := range stderrChan { + Ui.Message(output) + } + + return nil +} diff --git a/provisioner/puppet/provisioner_test.go b/provisioner/puppet/provisioner_test.go new file mode 100644 index 000000000..8e62e4649 --- /dev/null +++ b/provisioner/puppet/provisioner_test.go @@ -0,0 +1,20 @@ +package puppet + +import ( + "github.com/mitchellh/packer/packer" + "testing" +) + +func testConfig() map[string]interface{} { + return map[string]interface{}{ + // "inline": []interface{}{"foo", "bar"}, + } +} + +func TestProvisioner_Impl(t *testing.T) { + var raw interface{} + raw = &Provisioner{} + if _, ok := raw.(packer.Provisioner); !ok { + t.Fatalf("must be a Provisioner") + } +}