diff --git a/config.go b/config.go index 424064c3a..1597032d2 100644 --- a/config.go +++ b/config.go @@ -53,6 +53,7 @@ const defaultConfig = ` "chef-solo": "packer-provisioner-chef-solo", "file": "packer-provisioner-file", "puppet-masterless": "packer-provisioner-puppet-masterless", + "puppet-server": "packer-provisioner-puppet-server", "shell": "packer-provisioner-shell", "salt-masterless": "packer-provisioner-salt-masterless" } diff --git a/plugin/provisioner-puppet-server/main.go b/plugin/provisioner-puppet-server/main.go new file mode 100644 index 000000000..597b04e7f --- /dev/null +++ b/plugin/provisioner-puppet-server/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "github.com/mitchellh/packer/packer/plugin" + "github.com/mitchellh/packer/provisioner/puppet-server" +) + +func main() { + server, err := plugin.Server() + if err != nil { + panic(err) + } + server.RegisterProvisioner(new(puppetserver.Provisioner)) + server.Serve() +} diff --git a/plugin/provisioner-puppet-server/main_test.go b/plugin/provisioner-puppet-server/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/plugin/provisioner-puppet-server/main_test.go @@ -0,0 +1 @@ +package main diff --git a/provisioner/puppet-server/provisioner.go b/provisioner/puppet-server/provisioner.go new file mode 100644 index 000000000..72863f2ee --- /dev/null +++ b/provisioner/puppet-server/provisioner.go @@ -0,0 +1,256 @@ +// This package implements a provisioner for Packer that executes +// Puppet on the remote machine connecting to a Puppet master. +package puppetserver + +import ( + "fmt" + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/packer" + "os" + "strings" +) + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + tpl *packer.ConfigTemplate + + // Additional facts to set when executing Puppet + Facter map[string]string + + // A path to the client certificate + ClientCertPath string `mapstructure:"client_cert_path"` + + // A path to a directory containing the client private keys + ClientPrivateKeyPath string `mapstructure:"client_private_key_path"` + + // The hostname of the Puppet node. + PuppetNode string `mapstructure:"puppet_node"` + + // The hostname of the Puppet server. + PuppetServer string `mapstructure:"puppet_server"` + + // Additional options to be passed to `puppet agent`. + Options string `mapstructure:"options"` + + // If true, `sudo` will NOT be used to execute Puppet. + PreventSudo bool `mapstructure:"prevent_sudo"` + + // The directory where files will be uploaded. Packer requires write + // permissions in this directory. + StagingDir string `mapstructure:"staging_dir"` +} + +type Provisioner struct { + config Config +} + +type ExecuteTemplate struct { + FacterVars string + ClientCertPath string + ClientPrivateKeyPath string + PuppetNode string + PuppetServer string + Options string + Sudo bool +} + +func (p *Provisioner) Prepare(raws ...interface{}) error { + md, err := common.DecodeConfig(&p.config, raws...) + if err != nil { + return err + } + + p.config.tpl, err = packer.NewConfigTemplate() + if err != nil { + return err + } + p.config.tpl.UserVars = p.config.PackerUserVars + + // Accumulate any errors + errs := common.CheckUnusedConfig(md) + + if p.config.StagingDir == "" { + p.config.StagingDir = "/tmp/packer-puppet-server" + } + + // Templates + templates := map[string]*string{ + "client_cert_dir": &p.config.ClientCertPath, + "client_private_key_dir": &p.config.ClientPrivateKeyPath, + "puppet_server": &p.config.PuppetServer, + "puppet_node": &p.config.PuppetNode, + "options": &p.config.Options, + } + + for n, ptr := range templates { + var err error + *ptr, err = p.config.tpl.Process(*ptr, nil) + if err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Error processing %s: %s", n, err)) + } + } + + newFacts := make(map[string]string) + for k, v := range p.config.Facter { + k, err := p.config.tpl.Process(k, nil) + if err != nil { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("Error processing facter key %s: %s", k, err)) + continue + } + + v, err := p.config.tpl.Process(v, nil) + if err != nil { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("Error processing facter value '%s': %s", v, err)) + continue + } + + newFacts[k] = v + } + + p.config.Facter = newFacts + + if p.config.ClientCertPath != "" { + info, err := os.Stat(p.config.ClientCertPath) + if err != nil { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("client_cert_dir is invalid: %s", err)) + } else if !info.IsDir() { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("client_cert_dir must point to a directory")) + } + } + + if p.config.ClientPrivateKeyPath != "" { + info, err := os.Stat(p.config.ClientPrivateKeyPath) + if err != nil { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("client_private_key_dir is invalid: %s", err)) + } else if !info.IsDir() { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("client_private_key_dir must point to a directory")) + } + } + + if errs != nil && len(errs.Errors) > 0 { + return errs + } + + return nil +} + +func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { + ui.Say("Provisioning with Puppet...") + ui.Message("Creating Puppet staging directory...") + if err := p.createDir(ui, comm, p.config.StagingDir); err != nil { + return fmt.Errorf("Error creating staging directory: %s", err) + } + + // Upload client cert dir if set + remoteClientCertPath := "" + if p.config.ClientCertPath != "" { + ui.Message(fmt.Sprintf( + "Uploading client cert from: %s", p.config.ClientCertPath)) + remoteClientCertPath = fmt.Sprintf("%s/certs", p.config.StagingDir) + err := p.uploadDirectory(ui, comm, remoteClientCertPath, p.config.ClientCertPath) + if err != nil { + return fmt.Errorf("Error uploading client cert: %s", err) + } + } + + // Upload client cert dir if set + remoteClientPrivateKeyPath := "" + if p.config.ClientPrivateKeyPath != "" { + ui.Message(fmt.Sprintf( + "Uploading client private keys from: %s", p.config.ClientPrivateKeyPath)) + remoteClientPrivateKeyPath = fmt.Sprintf("%s/private_keys", p.config.StagingDir) + err := p.uploadDirectory(ui, comm, remoteClientPrivateKeyPath, p.config.ClientPrivateKeyPath) + if err != nil { + return fmt.Errorf("Error uploading client private keys: %s", err) + } + } + + // Compile the facter variables + facterVars := make([]string, 0, len(p.config.Facter)) + for k, v := range p.config.Facter { + facterVars = append(facterVars, fmt.Sprintf("FACTER_%s='%s'", k, v)) + } + + // Execute Puppet + command, err := p.config.tpl.Process(p.commandTemplate(), &ExecuteTemplate{ + FacterVars: strings.Join(facterVars, " "), + ClientCertPath: remoteClientCertPath, + ClientPrivateKeyPath: remoteClientPrivateKeyPath, + PuppetNode: p.config.PuppetNode, + PuppetServer: p.config.PuppetServer, + Options: p.config.Options, + Sudo: !p.config.PreventSudo, + }) + if err != nil { + return err + } + + cmd := &packer.RemoteCmd{ + Command: command, + } + + ui.Message(fmt.Sprintf("Running Puppet: %s", command)) + if err := cmd.StartWithUi(comm, ui); err != nil { + return err + } + + if cmd.ExitStatus != 0 && cmd.ExitStatus != 2 { + return fmt.Errorf("Puppet exited with a non-zero exit status: %d", cmd.ExitStatus) + } + + 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 (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error { + cmd := &packer.RemoteCmd{ + Command: fmt.Sprintf("mkdir -p '%s'", dir), + } + + if err := cmd.StartWithUi(comm, ui); err != nil { + return err + } + + if cmd.ExitStatus != 0 { + return fmt.Errorf("Non-zero exit status.") + } + + return nil +} + +func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, dst string, src string) error { + if err := p.createDir(ui, comm, dst); err != nil { + return err + } + + // Make sure there is a trailing "/" so that the directory isn't + // created on the other side. + if src[len(src)-1] != '/' { + src = src + "/" + } + + return comm.UploadDir(dst, src, nil) +} + +func (p *Provisioner) commandTemplate() string { + return "{{.FacterVars}} {{if .Sudo}} sudo -E {{end}}" + + "puppet agent --onetime --no-daemonize " + + "{{if ne .PuppetServer \"\"}}--server='{{.PuppetServer}}' {{end}}" + + "{{if ne .Options \"\"}}{{.Options}} {{end}}" + + "{{if ne .PuppetNode \"\"}}--certname={{.PuppetNode}} {{end}}" + + "{{if ne .ClientCertPath \"\"}}--certdir='{{.ClientCertPath}}' {{end}}" + + "{{if ne .ClientPrivateKeyPath \"\"}}--privatekeydir='{{.ClientPrivateKeyPath}}' {{end}}" + + "--detailed-exitcodes" +} diff --git a/provisioner/puppet-server/provisioner_test.go b/provisioner/puppet-server/provisioner_test.go new file mode 100644 index 000000000..66183e970 --- /dev/null +++ b/provisioner/puppet-server/provisioner_test.go @@ -0,0 +1,93 @@ +package puppetserver + +import ( + "github.com/mitchellh/packer/packer" + "io/ioutil" + "os" + "testing" +) + +func testConfig() map[string]interface{} { + tf, err := ioutil.TempFile("", "packer") + if err != nil { + panic(err) + } + + return map[string]interface{}{ + "puppet_server": tf.Name(), + } +} + +func TestProvisioner_Impl(t *testing.T) { + var raw interface{} + raw = &Provisioner{} + if _, ok := raw.(packer.Provisioner); !ok { + t.Fatalf("must be a Provisioner") + } +} + +func TestProvisionerPrepare_clientPrivateKeyPath(t *testing.T) { + config := testConfig() + + delete(config, "client_private_key_path") + p := new(Provisioner) + err := p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + // Test with bad paths + config["client_private_key_path"] = "i-should-not-exist" + p = new(Provisioner) + err = p.Prepare(config) + if err == nil { + t.Fatal("should be an error") + } + + // Test with a good one + td, err := ioutil.TempDir("", "packer") + if err != nil { + t.Fatalf("error: %s", err) + } + defer os.RemoveAll(td) + + config["client_private_key_path"] = td + p = new(Provisioner) + err = p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestProvisionerPrepare_clientCertPath(t *testing.T) { + config := testConfig() + + delete(config, "client_cert_path") + p := new(Provisioner) + err := p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + // Test with bad paths + config["client_cert_path"] = "i-should-not-exist" + p = new(Provisioner) + err = p.Prepare(config) + if err == nil { + t.Fatal("should be an error") + } + + // Test with a good one + td, err := ioutil.TempDir("", "packer") + if err != nil { + t.Fatalf("error: %s", err) + } + defer os.RemoveAll(td) + + config["client_cert_path"] = td + p = new(Provisioner) + err = p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } +} diff --git a/website/source/docs/provisioners/puppet-server.html.markdown b/website/source/docs/provisioners/puppet-server.html.markdown new file mode 100644 index 000000000..c87a11795 --- /dev/null +++ b/website/source/docs/provisioners/puppet-server.html.markdown @@ -0,0 +1,72 @@ +--- +layout: "docs" +page_title: "Puppet Server Provisioner" +--- + +# Puppet Server Provisioner + +Type: `puppet-server` + +The Puppet provisioner configures Puppet to run on the machines +communicating with a Puppet master. + +
+{
+ "type": "puppet-server",
+ "options": "--test --pluginsync",
+ "facter": {
+ "server_role": "webserver"
+ }
+}
+
+
+## Configuration Reference
+
+The reference of available configuration options is listed below.
+
+The provisioner takes various options. None are strictly
+required. They are listed below:
+
+* `client_cert_path` (string) - Path to the client certificate for the
+ node on your disk. This defaults to nothing, in which case a client
+ cert won't be uploaded.
+
+* `client_private_key_path` (string) - Path to the client private key for
+ the node on your disk. This defaults to nothing, in which case a client
+ private key won't be uploaded.
+
+* `facter` (hash) - Additional Facter facts to make available to the
+ Puppet run.
+
+* `options` (string) - Additional command line options to pass
+ to `puppet agent` when Puppet is ran.
+
+* `puppet_node` (string) - The name of the node. If this isn't set,
+ the fully qualified domain name will be used.
+
+* `puppet_server` (string) - Hostname of the Puppet server. By default
+ "puppet" will be used.
+
+* `prevent_sudo` (boolean) - By default, the configured commands that are
+ executed to run Puppet are executed with `sudo`. If this is true,
+ then the sudo will be omitted.
+
+* `staging_directory` (string) - This is the directory where all the configuration
+ of Puppet by Packer will be placed. By default this is "/tmp/packer-puppet-server".
+ This directory doesn't need to exist but must have proper permissions so that
+ the SSH user that Packer uses is able to create directories and write into
+ this folder. If the permissions are not correct, use a shell provisioner
+ prior to this to configure it properly.
diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb
index 20839a143..3c0bae143 100644
--- a/website/source/layouts/docs.erb
+++ b/website/source/layouts/docs.erb
@@ -48,7 +48,8 @@