From ea3677387ede6011210eb9cc4b8c6b6f12d6e946 Mon Sep 17 00:00:00 2001 From: Kelsey Hightower Date: Sun, 29 Sep 2013 13:38:37 -0700 Subject: [PATCH 1/6] Add ansible-local provisioner --- plugin/provisioner-ansible-local/main.go | 10 + plugin/provisioner-ansible-local/main_test.go | 1 + provisioner/ansible-local/provisioner.go | 223 ++++++++++++++++++ 3 files changed, 234 insertions(+) create mode 100644 plugin/provisioner-ansible-local/main.go create mode 100644 plugin/provisioner-ansible-local/main_test.go create mode 100644 provisioner/ansible-local/provisioner.go diff --git a/plugin/provisioner-ansible-local/main.go b/plugin/provisioner-ansible-local/main.go new file mode 100644 index 000000000..0caf0427f --- /dev/null +++ b/plugin/provisioner-ansible-local/main.go @@ -0,0 +1,10 @@ +package main + +import ( + "github.com/mitchellh/packer/packer/plugin" + "github.com/mitchellh/packer/provisioner/ansible-local" +) + +func main() { + plugin.ServeProvisioner(new(ansiblelocal.Provisioner)) +} diff --git a/plugin/provisioner-ansible-local/main_test.go b/plugin/provisioner-ansible-local/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/plugin/provisioner-ansible-local/main_test.go @@ -0,0 +1 @@ +package main diff --git a/provisioner/ansible-local/provisioner.go b/provisioner/ansible-local/provisioner.go new file mode 100644 index 000000000..33441412b --- /dev/null +++ b/provisioner/ansible-local/provisioner.go @@ -0,0 +1,223 @@ +package ansiblelocal + +import ( + "fmt" + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/packer" + "os" + "path/filepath" +) + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + tpl *packer.ConfigTemplate + + // The main playbook file to execute. + PlaybookFile string `mapstructure:"playbook_file"` + + // An array of local paths of playbook files to upload. + PlaybookPaths []string `mapstructure:"playbook_paths"` + + // An array of local paths of roles to upload. + RolePaths []string `mapstructure:"role_paths"` + + // The directory where files will be uploaded. Packer requires write + // permissions in this directory. + StagingDir string `mapstructure:"staging_directory"` +} + +type Provisioner struct { + config Config +} + +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-provisioner-ansible-local" + } + + // Templates + templates := map[string]*string{ + "staging_dir": &p.config.StagingDir, + } + + 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)) + } + } + + // Validation + err = validateFileConfig(p.config.PlaybookFile, "playbook_file", true) + if err != nil { + errs = packer.MultiErrorAppend(errs, err) + } + for _, path := range p.config.PlaybookPaths { + err := validateFileConfig(path, "playbook_paths", false) + if err != nil { + errs = packer.MultiErrorAppend(errs, err) + } + } + for _, path := range p.config.RolePaths { + if err := validateDirConfig(path, "role_paths"); err != nil { + errs = packer.MultiErrorAppend(errs, err) + } + } + 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 Ansible...") + + ui.Message("Creating Ansible staging directory...") + if err := p.createDir(ui, comm, p.config.StagingDir); err != nil { + return fmt.Errorf("Error creating staging directory: %s", err) + } + + ui.Message("Uploading main Playbook file...") + src := p.config.PlaybookFile + dst := filepath.Join(p.config.StagingDir, filepath.Base(src)) + if err := p.uploadFile(ui, comm, dst, src); err != nil { + return fmt.Errorf("Error uploading main playbook: %s", err) + } + + if len(p.config.RolePaths) > 0 { + ui.Message("Uploading role directories...") + for _, src := range p.config.RolePaths { + dst := filepath.Join(p.config.StagingDir, "roles", filepath.Base(src)) + if err := p.uploadDir(ui, comm, dst, src); err != nil { + return fmt.Errorf("Error uploading roles: %s", err) + } + } + } + + if len(p.config.PlaybookPaths) > 0 { + ui.Message("Uploading additional Playbooks...") + if err := p.createDir(ui, comm, filepath.Join(p.config.StagingDir, "playbooks")); err != nil { + return fmt.Errorf("Error creating playbooks directory: %s", err) + } + for _, src := range p.config.PlaybookPaths { + dst := filepath.Join(p.config.StagingDir, "playbooks", filepath.Base(src)) + if err := p.uploadFile(ui, comm, dst, src); err != nil { + return fmt.Errorf("Error uploading playbooks: %s", err) + } + } + } + + if err := p.executeAnsible(ui, comm); err != nil { + return fmt.Errorf("Error executing Ansible: %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 (p *Provisioner) executeAnsible(ui packer.Ui, comm packer.Communicator) error { + playbook := filepath.Join(p.config.StagingDir, filepath.Base(p.config.PlaybookFile)) + + // The inventory must be set to "127.0.0.1,". The comma is important + // as its the only way to override the ansible inventory when dealing + // with a single host. + command := fmt.Sprintf("ansible-playbook %s -c local -i %s", playbook, `"127.0.0.1,"`) + + ui.Message(fmt.Sprintf("Executing Ansible: %s", command)) + cmd := &packer.RemoteCmd{ + Command: command, + } + if err := cmd.StartWithUi(comm, ui); err != nil { + return err + } + if cmd.ExitStatus != 0 { + return fmt.Errorf("Non-zero exit status: %d", cmd.ExitStatus) + } + return nil +} + +func validateDirConfig(path string, config string) error { + info, err := os.Stat(path) + if err != nil { + return fmt.Errorf("%s: %s is invalid: %s", config, path, err) + } else if !info.IsDir() { + return fmt.Errorf("%s: %s must point to a directory", config, path) + } + return nil +} + +func validateFileConfig(name string, config string, req bool) error { + if req { + if name == "" { + return fmt.Errorf("%s must be specified.", config) + } + } + info, err := os.Stat(name) + if err != nil { + return fmt.Errorf("%s: %s is invalid: %s", config, name, err) + } else if info.IsDir() { + return fmt.Errorf("%s: %s must point to a file", config, name) + } + return nil +} + +func (p *Provisioner) uploadFile(ui packer.Ui, comm packer.Communicator, dst, src string) error { + f, err := os.Open(src) + if err != nil { + return fmt.Errorf("Error opening: %s", err) + } + defer f.Close() + + if err = comm.Upload(dst, f); err != nil { + return fmt.Errorf("Error uploading %s: %s", src, err) + } + return nil +} + +func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error { + ui.Message(fmt.Sprintf("Creating directory: %s", dir)) + 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) uploadDir(ui packer.Ui, comm packer.Communicator, dst, 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) +} From fd7f1c8b9aefed8214fc808ac39bc4b51b41fc69 Mon Sep 17 00:00:00 2001 From: Kelsey Hightower Date: Sun, 29 Sep 2013 13:56:43 -0700 Subject: [PATCH 2/6] Add ansible-local provisioner website docs --- .../provisioners/ansible-local.html.markdown | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 website/source/docs/provisioners/ansible-local.html.markdown diff --git a/website/source/docs/provisioners/ansible-local.html.markdown b/website/source/docs/provisioners/ansible-local.html.markdown new file mode 100644 index 000000000..2be51662f --- /dev/null +++ b/website/source/docs/provisioners/ansible-local.html.markdown @@ -0,0 +1,50 @@ +--- +layout: "docs" +page_title: "Ansible (Local) Provisioner" +--- + +# Ansible Local Provisioner + +Type: `ansible-local` + +The `ansible-local` provisioner configures Ansible to run on the machine by +Packer from local Playbook and Role files. Playbooks and Roles can be uploaded +from your local machine to the remote machine. Ansible is run in [local mode](http://www.ansibleworks.com/docs/playbooks2.html#local-playbooks) via the ansible-playbook command. + +## Basic Example + +The example below is fully functional. + +
+{
+    "type": "ansible-local",
+    "playbook_file": "local.yml"
+}
+
+ +## Configuration Reference + +The reference of available configuration options is listed below. + +Required: + +* `playbook_file` (string) - The playbook file to be executed by ansible. + This file must exist on your local system and will be uploaded to the + remote machine. + +Optional: + +* `playbook_paths` (array of strings) - An array of paths to playbook files on + your local system. These will be uploaded to the remote machine under + `staging_directory`/playbooks. By default, this is empty. + +* `role_paths` (array of strings) - An array of paths to role directories on + your local system. These will be uploaded to the remote machine under + `staging_directory`/roles. By default, this is empty. + +* `staging_directory` (string) - The directory where all the configuration of + Ansible by Packer will be placed. By default this is "/tmp/packer-provisioner-ansible-local". + 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. From 365f356a0ba98c00d05cae6868b21c81d082b501 Mon Sep 17 00:00:00 2001 From: Kelsey Hightower Date: Sun, 29 Sep 2013 14:54:49 -0700 Subject: [PATCH 3/6] Add tests for ansible-local provisioner --- provisioner/ansible-local/provisioner_test.go | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 provisioner/ansible-local/provisioner_test.go diff --git a/provisioner/ansible-local/provisioner_test.go b/provisioner/ansible-local/provisioner_test.go new file mode 100644 index 000000000..a16474a50 --- /dev/null +++ b/provisioner/ansible-local/provisioner_test.go @@ -0,0 +1,49 @@ +package ansiblelocal + +import ( + "github.com/mitchellh/packer/packer" + "io/ioutil" + "os" + "testing" +) + +func testConfig() map[string]interface{} { + m := make(map[string]interface{}) + return m +} + +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_PlaybookFile(t *testing.T) { + var p Provisioner + config := testConfig() + + err := p.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + config["playbook_file"] = "" + err = p.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + playbook_file, err := ioutil.TempFile("", "playbook") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(playbook_file.Name()) + + config["playbook_file"] = playbook_file.Name() + err = p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } +} From 4d241b96c1303dc642d3ff53a9e820e5af0551a6 Mon Sep 17 00:00:00 2001 From: Kelsey Hightower Date: Sun, 29 Sep 2013 15:42:42 -0700 Subject: [PATCH 4/6] Define the default staging dir using a const var --- provisioner/ansible-local/provisioner.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/provisioner/ansible-local/provisioner.go b/provisioner/ansible-local/provisioner.go index 33441412b..cde290125 100644 --- a/provisioner/ansible-local/provisioner.go +++ b/provisioner/ansible-local/provisioner.go @@ -8,6 +8,8 @@ import ( "path/filepath" ) +const DefaultStagingDir = "/tmp/packer-provisioner-ansible-local" + type Config struct { common.PackerConfig `mapstructure:",squash"` tpl *packer.ConfigTemplate @@ -47,7 +49,7 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { errs := common.CheckUnusedConfig(md) if p.config.StagingDir == "" { - p.config.StagingDir = "/tmp/packer-provisioner-ansible-local" + p.config.StagingDir = DefaultStagingDir } // Templates From e7b7d71f1675458b7f0dc3722f2ec76171031eb4 Mon Sep 17 00:00:00 2001 From: Kelsey Hightower Date: Sun, 29 Sep 2013 15:44:21 -0700 Subject: [PATCH 5/6] Add tests for ansible provisioner default settings --- provisioner/ansible-local/provisioner_test.go | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/provisioner/ansible-local/provisioner_test.go b/provisioner/ansible-local/provisioner_test.go index a16474a50..5a4b780ef 100644 --- a/provisioner/ansible-local/provisioner_test.go +++ b/provisioner/ansible-local/provisioner_test.go @@ -20,6 +20,28 @@ func TestProvisioner_Impl(t *testing.T) { } } +func TestProvisionerPrepare_Defaults(t *testing.T) { + var p Provisioner + config := testConfig() + + playbook_file, err := ioutil.TempFile("", "playbook") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(playbook_file.Name()) + + config["playbook_file"] = playbook_file.Name() + err = p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + if p.config.StagingDir != DefaultStagingDir { + t.Fatalf("unexpected staging dir %s, expected %s", + p.config.StagingDir, DefaultStagingDir) + } +} + func TestProvisionerPrepare_PlaybookFile(t *testing.T) { var p Provisioner config := testConfig() From ce8869279b666e68ef55ab95a256ec55ca0139d9 Mon Sep 17 00:00:00 2001 From: Kelsey Hightower Date: Sun, 29 Sep 2013 16:24:21 -0700 Subject: [PATCH 6/6] Enable ansible-local provisioner --- config.go | 1 + 1 file changed, 1 insertion(+) diff --git a/config.go b/config.go index 671f75b1f..b4989ae1a 100644 --- a/config.go +++ b/config.go @@ -40,6 +40,7 @@ const defaultConfig = ` }, "provisioners": { + "ansible-local": "packer-provisioner-ansible-local", "chef-solo": "packer-provisioner-chef-solo", "file": "packer-provisioner-file", "puppet-masterless": "packer-provisioner-puppet-masterless",