From 646f4a6dd330486cfff20ec4ab0ed627742bd667 Mon Sep 17 00:00:00 2001 From: Jan Brauer Date: Tue, 7 Jan 2014 17:32:57 +0100 Subject: [PATCH 1/6] Implement puppet-server provisioner This closely modeled after the same provisioner in Vagrant. --- config.go | 1 + plugin/provisioner-puppet-server/main.go | 15 ++ provisioner/puppet-server/provisioner.go | 244 ++++++++++++++++++ provisioner/puppet-server/provisioner_test.go | 93 +++++++ 4 files changed, 353 insertions(+) create mode 100644 plugin/provisioner-puppet-server/main.go create mode 100644 provisioner/puppet-server/provisioner.go create mode 100644 provisioner/puppet-server/provisioner_test.go diff --git a/config.go b/config.go index a2fe49297..483bfe090 100644 --- a/config.go +++ b/config.go @@ -50,6 +50,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/provisioner/puppet-server/provisioner.go b/provisioner/puppet-server/provisioner.go new file mode 100644 index 000000000..04676fa71 --- /dev/null +++ b/provisioner/puppet-server/provisioner.go @@ -0,0 +1,244 @@ +// 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"` + + PuppetNode string `mapstructure:"puppet_node"` + PuppetServer string `mapstructure:"puppet_server"` + Options string `mapstructure:"options"` + + 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 +} + +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, + }) + 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}} " + + "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) + } +} From 864f4b58512ff2294a24724a1770bb23effc717b Mon Sep 17 00:00:00 2001 From: Jan Brauer Date: Wed, 8 Jan 2014 12:46:34 +0100 Subject: [PATCH 2/6] Add docs for puppet-server provisioner --- .../provisioners/puppet-server.html.markdown | 68 +++++++++++++++++++ website/source/layouts/docs.erb | 3 +- 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 website/source/docs/provisioners/puppet-server.html.markdown 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..5b6d7a7ae --- /dev/null +++ b/website/source/docs/provisioners/puppet-server.html.markdown @@ -0,0 +1,68 @@ +--- +layout: "docs" +page_title: "Puppet Provisioner" +--- + +# Puppet Provisioner + +Type: `puppet-server` + +The Puppet provisioner configures Puppet to run on the machines +communicating with a Puppet master. + +
+Note that Puppet will not be installed automatically +by this provisioner. This provisioner expects that Puppet is already +installed on the machine. It is common practice to use the +shell provisioner before the +Puppet provisioner to do this. +
+ +## Basic Example + +The example below is fully functional and expects the configured manifest +file to exist relative to your working directory: + +
+{
+   "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. + +* `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 d8a70dc56..ed9a45b58 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -47,7 +47,8 @@
  • File Uploads
  • Ansible
  • Chef Solo
  • -
  • Puppet
  • +
  • Puppet Masterless
  • +
  • Puppet Server
  • Salt
  • Custom
  • From 58b06526f5da4297a4b6fe9ce82490e3112bfb4a Mon Sep 17 00:00:00 2001 From: Jan Brauer Date: Wed, 8 Jan 2014 13:03:03 +0100 Subject: [PATCH 3/6] Update docs for puppet-server provisioner --- provisioner/puppet-server/provisioner.go | 11 +++++++++-- .../docs/provisioners/puppet-server.html.markdown | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/provisioner/puppet-server/provisioner.go b/provisioner/puppet-server/provisioner.go index 04676fa71..0166827f0 100644 --- a/provisioner/puppet-server/provisioner.go +++ b/provisioner/puppet-server/provisioner.go @@ -23,10 +23,17 @@ type Config struct { // A path to a directory containing the client private keys ClientPrivateKeyPath string `mapstructure:"client_private_key_path"` - PuppetNode string `mapstructure:"puppet_node"` + // The hostname of the Puppet node. + PuppetNode string `mapstructure:"puppet_node"` + + // The hostname of the Puppet server. PuppetServer string `mapstructure:"puppet_server"` - Options string `mapstructure:"options"` + // Additional options to be passed to `puppet agent`. + Options string `mapstructure:"options"` + + // The directory where files will be uploaded. Packer requires write + // permissions in this directory. StagingDir string `mapstructure:"staging_dir"` } diff --git a/website/source/docs/provisioners/puppet-server.html.markdown b/website/source/docs/provisioners/puppet-server.html.markdown index 5b6d7a7ae..d9c4bdc8e 100644 --- a/website/source/docs/provisioners/puppet-server.html.markdown +++ b/website/source/docs/provisioners/puppet-server.html.markdown @@ -1,9 +1,9 @@ --- layout: "docs" -page_title: "Puppet Provisioner" +page_title: "Puppet Server Provisioner" --- -# Puppet Provisioner +# Puppet Server Provisioner Type: `puppet-server` From 9d01684c1cca9ecf37ca636857f5cbca68d2ea2f Mon Sep 17 00:00:00 2001 From: Jan Brauer Date: Thu, 9 Jan 2014 08:51:48 +0100 Subject: [PATCH 4/6] Use sudo by default, as in puppet-masterless --- provisioner/puppet-server/provisioner.go | 7 ++++++- .../source/docs/provisioners/puppet-server.html.markdown | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/provisioner/puppet-server/provisioner.go b/provisioner/puppet-server/provisioner.go index 0166827f0..72863f2ee 100644 --- a/provisioner/puppet-server/provisioner.go +++ b/provisioner/puppet-server/provisioner.go @@ -32,6 +32,9 @@ type Config struct { // 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"` @@ -48,6 +51,7 @@ type ExecuteTemplate struct { PuppetNode string PuppetServer string Options string + Sudo bool } func (p *Provisioner) Prepare(raws ...interface{}) error { @@ -182,6 +186,7 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { PuppetNode: p.config.PuppetNode, PuppetServer: p.config.PuppetServer, Options: p.config.Options, + Sudo: !p.config.PreventSudo, }) if err != nil { return err @@ -240,7 +245,7 @@ func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, ds } func (p *Provisioner) commandTemplate() string { - return "{{.FacterVars}} " + + return "{{.FacterVars}} {{if .Sudo}} sudo -E {{end}}" + "puppet agent --onetime --no-daemonize " + "{{if ne .PuppetServer \"\"}}--server='{{.PuppetServer}}' {{end}}" + "{{if ne .Options \"\"}}{{.Options}} {{end}}" + diff --git a/website/source/docs/provisioners/puppet-server.html.markdown b/website/source/docs/provisioners/puppet-server.html.markdown index d9c4bdc8e..fce35bb03 100644 --- a/website/source/docs/provisioners/puppet-server.html.markdown +++ b/website/source/docs/provisioners/puppet-server.html.markdown @@ -60,6 +60,10 @@ required. They are listed below: * `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 From 5bee1b2699ec149214443686da2c7664123b1023 Mon Sep 17 00:00:00 2001 From: Mathias Lafeldt Date: Thu, 9 Jan 2014 11:25:12 +0100 Subject: [PATCH 5/6] Add missing main_test.go for puppet-server provisioner This is required to pick up the tests with `make test`. --- plugin/provisioner-puppet-server/main_test.go | 1 + 1 file changed, 1 insertion(+) create mode 100644 plugin/provisioner-puppet-server/main_test.go 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 From 3dd0f9d6ac4a7a5f7fcdeef4a75d8d171a2d7044 Mon Sep 17 00:00:00 2001 From: Jan Brauer Date: Mon, 3 Feb 2014 21:07:26 +0100 Subject: [PATCH 6/6] Update documentation --- website/source/docs/provisioners/puppet-server.html.markdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/source/docs/provisioners/puppet-server.html.markdown b/website/source/docs/provisioners/puppet-server.html.markdown index fce35bb03..c87a11795 100644 --- a/website/source/docs/provisioners/puppet-server.html.markdown +++ b/website/source/docs/provisioners/puppet-server.html.markdown @@ -20,8 +20,8 @@ Puppet provisioner to do this. ## Basic Example -The example below is fully functional and expects the configured manifest -file to exist relative to your working directory: +The example below is fully functional and expects a Puppet server to be accessible +from your network.:
     {