diff --git a/config.go b/config.go index e2b701374..d40981b72 100644 --- a/config.go +++ b/config.go @@ -39,7 +39,8 @@ const defaultConfig = ` "provisioners": { "file": "packer-provisioner-file", - "shell": "packer-provisioner-shell" + "shell": "packer-provisioner-shell", + "salt-masterless": "packer-provisioner-salt-masterless" } } ` diff --git a/plugin/provisioner-salt-masterless/main.go b/plugin/provisioner-salt-masterless/main.go new file mode 100644 index 000000000..8547a7a88 --- /dev/null +++ b/plugin/provisioner-salt-masterless/main.go @@ -0,0 +1,10 @@ +package main + +import ( + "github.com/mitchellh/packer/packer/plugin" + "github.com/mitchellh/packer/provisioner/salt-masterless" +) + +func main() { + plugin.ServeProvisioner(new(saltMasterless.Provisioner)) +} diff --git a/provisioner/salt-masterless/provisioner.go b/provisioner/salt-masterless/provisioner.go new file mode 100644 index 000000000..6a0b1f189 --- /dev/null +++ b/provisioner/salt-masterless/provisioner.go @@ -0,0 +1,165 @@ +// This package implements a provisioner for Packer that executes a +// saltstack highstate within the remote machine +package saltMasterless + +import ( + "errors" + "fmt" + "github.com/mitchellh/mapstructure" + "github.com/mitchellh/packer/packer" + "os" + "path/filepath" + "sort" + "strings" +) + +var Ui packer.Ui + +const DefaultTempConfigDir = "/tmp/salt" + +type config struct { + // If true, run the salt-bootstrap script + SkipBootstrap bool `mapstructure:"skip_bootstrap"` + BootstrapArgs string `mapstructure:"bootstrap_args"` + + // Local path to the salt state tree + LocalStateTree string `mapstructure:"local_state_tree"` + + // Where files will be copied before moving to the /srv/salt directory + TempConfigDir string `mapstructure:"temp_config_dir"` +} + +type Provisioner struct { + config config +} + +func (p *Provisioner) Prepare(raws ...interface{}) error { + var md mapstructure.Metadata + decoderConfig := &mapstructure.DecoderConfig{ + Metadata: &md, + Result: &p.config, + } + + decoder, err := mapstructure.NewDecoder(decoderConfig) + if err != nil { + return err + } + + for _, raw := range raws { + err := decoder.Decode(raw) + if err != nil { + return err + } + } + + // Accumulate any errors + errs := make([]error, 0) + + // Unused keys are errors + if len(md.Unused) > 0 { + sort.Strings(md.Unused) + for _, unused := range md.Unused { + if unused != "type" && !strings.HasPrefix(unused, "packer_") { + errs = append( + errs, fmt.Errorf("Unknown configuration key: %s", unused)) + } + } + } + + if p.config.LocalStateTree == "" { + errs = append(errs, errors.New("Please specify a local_state_tree")) + } + + if p.config.TempConfigDir == "" { + p.config.TempConfigDir = DefaultTempConfigDir + } + + 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 + + if !p.config.SkipBootstrap { + cmd := &packer.RemoteCmd{ + Command: fmt.Sprintf("wget -O - http://bootstrap.saltstack.org | sudo sh -s %s", p.config.BootstrapArgs), + } + Ui.Say(fmt.Sprintf("Installing Salt with command %s", cmd)) + if err = cmd.StartWithUi(comm, ui); err != nil { + return fmt.Errorf("Unable to install Salt: %d", err) + } + } + + Ui.Say(fmt.Sprintf("Creating remote directory: %s", p.config.TempConfigDir)) + cmd := &packer.RemoteCmd{Command: fmt.Sprintf("mkdir -p %s", p.config.TempConfigDir)} + if err = cmd.StartWithUi(comm, ui); err != nil { + return fmt.Errorf("Error creating remote salt state directory: %s", err) + } + + Ui.Say(fmt.Sprintf("Uploading local state tree: %s", p.config.LocalStateTree)) + if err = UploadLocalDirectory(p.config.LocalStateTree, p.config.TempConfigDir, comm); err != nil { + return fmt.Errorf("Error uploading local state tree to remote: %s", err) + } + + Ui.Say(fmt.Sprintf("Moving %s to /srv/salt", p.config.TempConfigDir)) + cmd = &packer.RemoteCmd{Command: fmt.Sprintf("sudo mv %s /srv/salt", p.config.TempConfigDir)} + if err = cmd.StartWithUi(comm, ui); err != nil { + return fmt.Errorf("Unable to move %s to /srv/salt: %d", p.config.TempConfigDir, err) + } + + Ui.Say("Running highstate") + cmd = &packer.RemoteCmd{Command: "sudo salt-call --local state.highstate -l info"} + if err = cmd.StartWithUi(comm, ui); err != nil { + return fmt.Errorf("Error executing highstate: %s", err) + } + + Ui.Say("Removing /srv/salt") + cmd = &packer.RemoteCmd{Command: "sudo rm -r /srv/salt"} + if err = cmd.StartWithUi(comm, ui); err != nil { + return fmt.Errorf("Unable to remove /srv/salt: %d", err) + } + + return nil +} + +func UploadLocalDirectory(localDir string, remoteDir string, comm packer.Communicator) (err error) { + visitPath := func(localPath string, f os.FileInfo, err error) (err2 error) { + localRelPath := strings.Replace(localPath, localDir, "", 1) + remotePath := fmt.Sprintf("%s%s", remoteDir, localRelPath) + if f.IsDir() && f.Name() == ".git" { + return filepath.SkipDir + } + if f.IsDir() { + // Make remote directory + cmd := &packer.RemoteCmd{Command: fmt.Sprintf("mkdir -p %s", remotePath)} + if err = cmd.StartWithUi(comm, Ui); err != nil { + return err + } + } else { + // Upload file to existing directory + file, err := os.Open(localPath) + if err != nil { + return fmt.Errorf("Error opening file: %s", err) + } + defer file.Close() + + Ui.Say(fmt.Sprintf("Uploading file %s: %s", localPath, remotePath)) + if err = comm.Upload(remotePath, file); err != nil { + return fmt.Errorf("Error uploading file: %s", err) + } + } + return + } + + err = filepath.Walk(localDir, visitPath) + if err != nil { + return fmt.Errorf("Error uploading local directory %s: %s", localDir, err) + } + + return nil +} diff --git a/provisioner/salt-masterless/provisioner_test.go b/provisioner/salt-masterless/provisioner_test.go new file mode 100644 index 000000000..99ce05d25 --- /dev/null +++ b/provisioner/salt-masterless/provisioner_test.go @@ -0,0 +1,46 @@ +package saltMasterless + +import ( + "github.com/mitchellh/packer/packer" + "testing" +) + +func testConfig() map[string]interface{} { + return map[string]interface{}{ + "local_state_tree": "/Users/me/salt", + } +} + +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_Defaults(t *testing.T) { + var p Provisioner + config := testConfig() + + err := p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + if p.config.TempConfigDir != DefaultTempConfigDir { + t.Errorf("unexpected temp config dir: %s", p.config.TempConfigDir) + } +} + +func TestProvisionerPrepare_InvalidKey(t *testing.T) { + var p Provisioner + config := testConfig() + + // Add a random key + config["i_should_not_be_valid"] = true + err := p.Prepare(config) + if err == nil { + t.Fatal("should have error") + } +} diff --git a/website/source/docs/provisioners/salt.html.markdown b/website/source/docs/provisioners/salt.html.markdown new file mode 100644 index 000000000..0ed1ea51e --- /dev/null +++ b/website/source/docs/provisioners/salt.html.markdown @@ -0,0 +1,38 @@ +--- +layout: "docs" +--- + +# Salt Masterless Provisioner + +Type: `salt-masterless` + +The salt-masterless provisioner provisions machines built by Packer using [Salt](http://saltstack.com/) states. + +## Basic Example + +The example below is fully functional. + +
+{ + "type": "salt-masterless", + "bootstrap_args": "git v0.16.0" + "local_state_tree": "/Users/me/salt" +} ++ +## Configuration Reference + +The reference of available configuration options is listed below. The only required argument is the path to your local salt state tree. + +Required: + +* `local_state_tree` (string) - The path to your local [state tree](http://docs.saltstack.com/ref/states/highstate.html#the-salt-state-tree). This will be uploaded to the `/srv/salt` on the remote, and removed before shutdown. + +Optional: + +* `skip_bootstrap` (boolean) - By default the salt provisioner runs [salt bootstrap](https://github.com/saltstack/salt-bootstrap) to install salt. Set this to true to skip this step. + +* `boostrap_args` (string) - + Arguments to send to the bootstrap script. Usage is somewhat documented on [github](https://github.com/saltstack/salt-bootstrap), but the [script itself](https://github.com/saltstack/salt-bootstrap/blob/develop/bootstrap-salt.sh) has more detailed usage instructions. By default, no arguments are sent to the script. + +* `temp_config_dir` (string) - Where your local state tree will be copied before moving to the `/srv/salt` directory. Default is `/tmp/salt`.