From cc8ca3098ea662029dc244066ce243bf1bac4572 Mon Sep 17 00:00:00 2001 From: "Billie H. Cleek" Date: Mon, 9 Mar 2015 21:11:57 -0700 Subject: [PATCH 1/6] add ansible provisioner --- plugin/provisioner-ansible/main.go | 15 + plugin/provisioner-ansible/main_test.go | 1 + provisioner/ansible/adapter.go | 283 ++++++++++++++++++ provisioner/ansible/provisioner.go | 277 +++++++++++++++++ provisioner/ansible/provisioner_test.go | 218 ++++++++++++++ provisioner/shell/provisioner.go | 5 - .../docs/provisioners/ansible.html.markdown | 66 ++++ 7 files changed, 860 insertions(+), 5 deletions(-) create mode 100644 plugin/provisioner-ansible/main.go create mode 100644 plugin/provisioner-ansible/main_test.go create mode 100644 provisioner/ansible/adapter.go create mode 100644 provisioner/ansible/provisioner.go create mode 100644 provisioner/ansible/provisioner_test.go create mode 100644 website/source/docs/provisioners/ansible.html.markdown diff --git a/plugin/provisioner-ansible/main.go b/plugin/provisioner-ansible/main.go new file mode 100644 index 000000000..789339591 --- /dev/null +++ b/plugin/provisioner-ansible/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "github.com/bhcleek/packer-provisioner-ansible/provisioner/ansible" + "github.com/mitchellh/packer/packer/plugin" +) + +func main() { + server, err := plugin.Server() + if err != nil { + panic(err) + } + server.RegisterProvisioner(new(ansible.Provisioner)) + server.Serve() +} diff --git a/plugin/provisioner-ansible/main_test.go b/plugin/provisioner-ansible/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/plugin/provisioner-ansible/main_test.go @@ -0,0 +1 @@ +package main diff --git a/provisioner/ansible/adapter.go b/provisioner/ansible/adapter.go new file mode 100644 index 000000000..62cf54c60 --- /dev/null +++ b/provisioner/ansible/adapter.go @@ -0,0 +1,283 @@ +package ansible + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "net" + + "github.com/mitchellh/packer/packer" + "golang.org/x/crypto/ssh" +) + +type adapter struct { + done <-chan struct{} + l net.Listener + config *ssh.ServerConfig + sftpCmd string + ui packer.Ui + comm packer.Communicator +} + +func newAdapter(done <-chan struct{}, l net.Listener, config *ssh.ServerConfig, sftpCmd string, ui packer.Ui, comm packer.Communicator) *adapter { + return &adapter{ + done: done, + l: l, + config: config, + sftpCmd: sftpCmd, + ui: ui, + comm: comm, + } +} + +func (c *adapter) Serve() { + c.ui.Say(fmt.Sprintf("SSH proxy: serving on %s", c.l.Addr())) + + errc := make(chan error, 1) + + go func(errc chan error) { + for err := range errc { + if err != nil { + c.ui.Error(err.Error()) + } + } + }(errc) + + for { + // Accept will return if either the underlying connection is closed or if a connection is made. + // after returning, check to see if c.done can be received. If so, then Accept() returned because + // the connection has been closed. + conn, err := c.l.Accept() + select { + case <-c.done: + return + default: + if err != nil { + c.ui.Error(fmt.Sprintf("listen.Accept failed: %v", err)) + } + go func(conn net.Conn) { + errc <- c.Handle(conn, errc) + }(conn) + } + } + + close(errc) +} + +func (c *adapter) Handle(conn net.Conn, errc chan<- error) error { + c.ui.Say("SSH proxy: accepted connection") + _, chans, reqs, err := ssh.NewServerConn(conn, c.config) + if err != nil { + return errors.New("failed to handshake") + } + + // discard all global requests + go ssh.DiscardRequests(reqs) + + // Service the incoming NewChannels + for newChannel := range chans { + if newChannel.ChannelType() != "session" { + newChannel.Reject(ssh.UnknownChannelType, "unknown channel type") + continue + } + + go func(errc chan<- error) { + errc <- c.handleSession(newChannel) + }(errc) + } + + return nil +} + +func (c *adapter) handleSession(newChannel ssh.NewChannel) error { + channel, requests, err := newChannel.Accept() + if err != nil { + return err + } + defer channel.Close() + + done := make(chan struct{}) + + // Sessions have requests such as "pty-req", "shell", "env", and "exec". + // see RFC 4254, section 6 + go func(in <-chan *ssh.Request) { + env := make([]envRequestPayload, 4) + for req := range in { + switch req.Type { + case "pty-req": + // accept pty-req requests, but don't actually do anything. Necessary for OpenSSH and sudo. + req.Reply(true, nil) + + case "env": + req.Reply(true, nil) + + req, err := newEnvRequest(req) + if err != nil { + c.ui.Error(err.Error()) + continue + } + env = append(env, req.Payload) + case "exec": + req.Reply(true, nil) + + req, err := newExecRequest(req) + if err != nil { + c.ui.Error(err.Error()) + close(done) + continue + } + + if len(req.Payload) > 0 { + cmd := &packer.RemoteCmd{ + Stdin: channel, + Stdout: channel, + Stderr: channel.Stderr(), + Command: string(req.Payload), + } + + if err := c.comm.Start(cmd); err != nil { + c.ui.Error(err.Error()) + close(done) + return + } + go func(cmd *packer.RemoteCmd, channel ssh.Channel) { + cmd.Wait() + + exitStatus := make([]byte, 4) + binary.BigEndian.PutUint32(exitStatus, uint32(cmd.ExitStatus)) + channel.SendRequest("exit-status", false, exitStatus) + close(done) + }(cmd, channel) + } + + case "subsystem": + req, err := newSubsystemRequest(req) + if err != nil { + c.ui.Error(err.Error()) + continue + } + + switch req.Payload { + case "sftp": + c.ui.Say("starting sftp subsystem") + req.Reply(true, nil) + sftpCmd := c.sftpCmd + if len(sftpCmd) == 0 { + sftpCmd = "/usr/lib/sftp-server -e" + } + cmd := &packer.RemoteCmd{ + Stdin: channel, + Stdout: channel, + Stderr: channel.Stderr(), + Command: sftpCmd, + } + + if err := c.comm.Start(cmd); err != nil { + c.ui.Error(err.Error()) + } + + go func() { + cmd.Wait() + close(done) + }() + + default: + req.Reply(false, nil) + + } + default: + c.ui.Message(fmt.Sprintf("rejecting %s request", req.Type)) + req.Reply(false, nil) + } + } + }(requests) + + <-done + return nil +} + +func (c *adapter) Shutdown() { + c.l.Close() +} + +type envRequest struct { + *ssh.Request + Payload envRequestPayload +} + +type envRequestPayload struct { + Name string + Value string +} + +func newEnvRequest(raw *ssh.Request) (*envRequest, error) { + r := new(envRequest) + r.Request = raw + + if err := ssh.Unmarshal(raw.Payload, &r.Payload); err != nil { + return nil, err + } + + return r, nil +} + +func sshString(buf io.Reader) (string, error) { + var size uint32 + err := binary.Read(buf, binary.BigEndian, &size) + if err != nil { + return "", err + } + + b := make([]byte, size) + err = binary.Read(buf, binary.BigEndian, b) + if err != nil { + return "", err + } + return string(b), nil +} + +type execRequest struct { + *ssh.Request + Payload execRequestPayload +} + +type execRequestPayload string + +func newExecRequest(raw *ssh.Request) (*execRequest, error) { + r := new(execRequest) + r.Request = raw + buf := bytes.NewReader(r.Request.Payload) + + var err error + var payload string + if payload, err = sshString(buf); err != nil { + return nil, err + } + + r.Payload = execRequestPayload(payload) + return r, nil +} + +type subsystemRequest struct { + *ssh.Request + Payload subsystemRequestPayload +} + +type subsystemRequestPayload string + +func newSubsystemRequest(raw *ssh.Request) (*subsystemRequest, error) { + r := new(subsystemRequest) + r.Request = raw + buf := bytes.NewReader(r.Request.Payload) + + var err error + var payload string + if payload, err = sshString(buf); err != nil { + return nil, err + } + + r.Payload = subsystemRequestPayload(payload) + return r, nil +} diff --git a/provisioner/ansible/provisioner.go b/provisioner/ansible/provisioner.go new file mode 100644 index 000000000..063c9334e --- /dev/null +++ b/provisioner/ansible/provisioner.go @@ -0,0 +1,277 @@ +package ansible + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "net" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "sync" + + "golang.org/x/crypto/ssh" + + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/helper/config" + "github.com/mitchellh/packer/packer" + "github.com/mitchellh/packer/template/interpolate" +) + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + ctx interpolate.Context + + // The command to run ansible + Command string + + // Extra options to pass to the ansible command + ExtraArguments []string `mapstructure:"extra_arguments"` + + // The main playbook file to execute. + PlaybookFile string `mapstructure:"playbook_file"` + LocalPort string `mapstructure:"local_port"` + SSHHostKeyFile string `mapstructure:"ssh_host_key_file"` + SSHAuthorizedKeyFile string `mapstructure:"ssh_authorized_key_file"` + SFTPCmd string `mapstructure:"sftp_command"` + inventoryFile string +} + +type Provisioner struct { + config Config + adapter *adapter + done chan struct{} +} + +func (p *Provisioner) Prepare(raws ...interface{}) error { + p.done = make(chan struct{}) + + err := config.Decode(&p.config, &config.DecodeOpts{ + Interpolate: true, + InterpolateContext: &p.config.ctx, + InterpolateFilter: &interpolate.RenderFilter{ + Exclude: []string{}, + }, + }, raws...) + if err != nil { + return err + } + + // Defaults + if p.config.Command == "" { + p.config.Command = "ansible-playbook" + } + + var errs *packer.MultiError + err = validateFileConfig(p.config.PlaybookFile, "playbook_file", true) + if err != nil { + errs = packer.MultiErrorAppend(errs, err) + } + + err = validateFileConfig(p.config.SSHAuthorizedKeyFile, "ssh_authorized_key_file", true) + if err != nil { + errs = packer.MultiErrorAppend(errs, err) + } + + // Check that the host key file exists, if configured + if len(p.config.SSHHostKeyFile) > 0 { + err = validateFileConfig(p.config.SSHHostKeyFile, "ssh_host_key_file", true) + if err != nil { + log.Println(p.config.SSHHostKeyFile, "does not exist") + errs = packer.MultiErrorAppend(errs, err) + } + } + + if len(p.config.LocalPort) > 0 { + if _, err := strconv.ParseUint(p.config.LocalPort, 10, 16); err != nil { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("local_port: %s must be a valid port", p.config.LocalPort)) + } + } + + 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...") + + pubKeyBytes, err := ioutil.ReadFile(p.config.SSHAuthorizedKeyFile) + if err != nil { + return errors.New("Failed to load authorized key file") + } + + public, _, _, _, err := ssh.ParseAuthorizedKey(pubKeyBytes) + if err != nil { + return errors.New("Failed to parse authorized key") + } + + keyChecker := ssh.CertChecker{ + UserKeyFallback: func(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) { + if conn.User() != "packer-ansible" { + ui.Say(fmt.Sprintf("%s is not a valid user")) + return nil, errors.New("authentication failed") + } + + if !bytes.Equal(public.Marshal(), pubKey.Marshal()) { + ui.Say("unauthorized key") + return nil, errors.New("authentication failed") + } + + return nil, nil + }, + } + config := &ssh.ServerConfig{ + AuthLogCallback: func(conn ssh.ConnMetadata, method string, err error) { + ui.Say(fmt.Sprintf("authentication attempt from %s to %s as %s using %s", conn.RemoteAddr(), conn.LocalAddr(), conn.User(), method)) + }, + PublicKeyCallback: keyChecker.Authenticate, + //NoClientAuth: true, + } + + privateBytes, err := ioutil.ReadFile(p.config.SSHHostKeyFile) + if err != nil { + return errors.New("Failed to load private host key") + } + + private, err := ssh.ParsePrivateKey(privateBytes) + if err != nil { + return errors.New("Failed to parse private host key") + } + + config.AddHostKey(private) + + localListener, err := func() (net.Listener, error) { + port, _ := strconv.ParseUint(p.config.LocalPort, 10, 16) + if port == 0 { + port = 2200 + } + for i := 0; i < 10; i++ { + port++ + l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port)) + if err == nil { + p.config.LocalPort = strconv.FormatUint(port, 10) + return l, nil + } + + ui.Say(err.Error()) + } + return nil, errors.New("Error setting up SSH proxy connection") + }() + + if err != nil { + return err + } + + p.adapter = newAdapter(p.done, localListener, config, p.config.SFTPCmd, ui, comm) + + defer func() { + ui.Say("shutting down the SSH proxy") + close(p.done) + p.adapter.Shutdown() + }() + + go p.adapter.Serve() + + if len(p.config.inventoryFile) == 0 { + tf, err := ioutil.TempFile("", "packer-provisioner-ansible") + if err != nil { + return fmt.Errorf("Error preparing inventory file: %s", err) + } + defer os.Remove(tf.Name()) + inv := fmt.Sprintf("default ansible_ssh_host=127.0.0.1 ansible_ssh_user=packer-ansible ansible_ssh_port=%s", p.config.LocalPort) + _, err = tf.Write([]byte(inv)) + if err != nil { + tf.Close() + return fmt.Errorf("Error preparing inventory file: %s", err) + } + tf.Close() + p.config.inventoryFile = tf.Name() + defer func() { + p.config.inventoryFile = "" + }() + } + + if err := p.executeAnsible(ui, comm); err != nil { + return fmt.Errorf("Error executing Ansible: %s", err) + } + + return nil + +} + +func (p *Provisioner) Cancel() { + if p.done != nil { + close(p.done) + } + if p.adapter != nil { + p.adapter.Shutdown() + } + os.Exit(0) +} + +func (p *Provisioner) executeAnsible(ui packer.Ui, comm packer.Communicator) error { + playbook, _ := filepath.Abs(p.config.PlaybookFile) + inventory := p.config.inventoryFile + + args := []string{playbook, "-i", inventory} + args = append(args, p.config.ExtraArguments...) + + cmd := exec.Command(p.config.Command, args...) + + stdout, err := cmd.StdoutPipe() + if err != nil { + return err + } + stderr, err := cmd.StderrPipe() + if err != nil { + return err + } + + wg := sync.WaitGroup{} + repeat := func(r io.ReadCloser) { + scanner := bufio.NewScanner(r) + for scanner.Scan() { + ui.Message(scanner.Text()) + } + if err := scanner.Err(); err != nil { + ui.Error(err.Error()) + } + wg.Done() + } + wg.Add(2) + go repeat(stdout) + go repeat(stderr) + + ui.Say(fmt.Sprintf("Executing Ansible: %s", strings.Join(cmd.Args, " "))) + cmd.Start() + wg.Wait() + err = cmd.Wait() + if err != nil { + return fmt.Errorf("Non-zero exit status: %s", err) + } + + 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 +} diff --git a/provisioner/ansible/provisioner_test.go b/provisioner/ansible/provisioner_test.go new file mode 100644 index 000000000..0b52e47c2 --- /dev/null +++ b/provisioner/ansible/provisioner_test.go @@ -0,0 +1,218 @@ +package ansible + +import ( + "crypto/rand" + "fmt" + "io" + "io/ioutil" + "os" + "testing" + + "github.com/mitchellh/packer/packer" +) + +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_Defaults(t *testing.T) { + var p Provisioner + config := testConfig() + + err := p.Prepare(config) + if err == nil { + t.Fatalf("should have error") + } + + hostkey_file, err := ioutil.TempFile("", "hostkey") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(hostkey_file.Name()) + + publickey_file, err := ioutil.TempFile("", "publickey") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(publickey_file.Name()) + + playbook_file, err := ioutil.TempFile("", "playbook") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(playbook_file.Name()) + + config["ssh_private_host_key_file"] = hostkey_file.Name() + config["ssh_authorized_key_file"] = publickey_file.Name() + config["playbook_file"] = playbook_file.Name() + err = p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestProvisionerPrepare_PlaybookFile(t *testing.T) { + var p Provisioner + config := testConfig() + + hostkey_file, err := ioutil.TempFile("", "hostkey") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(hostkey_file.Name()) + + publickey_file, err := ioutil.TempFile("", "publickey") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(publickey_file.Name()) + + config["ssh_private_host_key_file"] = hostkey_file.Name() + config["ssh_authorized_key_file"] = publickey_file.Name() + + 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) + } +} + +func TestProvisionerPrepare_HostKeyFile(t *testing.T) { + var p Provisioner + config := testConfig() + + publickey_file, err := ioutil.TempFile("", "publickey") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(publickey_file.Name()) + + playbook_file, err := ioutil.TempFile("", "playbook") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(playbook_file.Name()) + + filename := make([]byte, 10) + n, err := io.ReadFull(rand.Reader, filename) + if n != len(filename) || err != nil { + t.Fatal("could not create random file name") + } + + config["ssh_private_host_key_file"] = fmt.Sprintf("%x", filename) + config["ssh_authorized_key_file"] = publickey_file.Name() + config["playbook_file"] = playbook_file.Name() + + err = p.Prepare(config) + if err == nil { + t.Fatal("should error if ssh_private_host_key_file does not exist") + } + + hostkey_file, err := ioutil.TempFile("", "hostkey") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(hostkey_file.Name()) + + config["ssh_private_host_key_file"] = hostkey_file.Name() + err = p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestProvisionerPrepare_AuthorizedKeyFile(t *testing.T) { + var p Provisioner + config := testConfig() + + hostkey_file, err := ioutil.TempFile("", "hostkey") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(hostkey_file.Name()) + + playbook_file, err := ioutil.TempFile("", "playbook") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(playbook_file.Name()) + + config["ssh_private_host_key_file"] = hostkey_file.Name() + config["playbook_file"] = playbook_file.Name() + + err = p.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + publickey_file, err := ioutil.TempFile("", "publickey") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(publickey_file.Name()) + + config["ssh_authorized_key_file"] = publickey_file.Name() + err = p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestProvisionerPrepare_LocalPort(t *testing.T) { + var p Provisioner + config := testConfig() + + hostkey_file, err := ioutil.TempFile("", "hostkey") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(hostkey_file.Name()) + + publickey_file, err := ioutil.TempFile("", "publickey") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(publickey_file.Name()) + + playbook_file, err := ioutil.TempFile("", "playbook") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(playbook_file.Name()) + + config["ssh_private_host_key_file"] = hostkey_file.Name() + config["ssh_authorized_key_file"] = publickey_file.Name() + config["playbook_file"] = playbook_file.Name() + + config["local_port"] = "65537" + err = p.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + config["local_port"] = "22222" + err = p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } +} diff --git a/provisioner/shell/provisioner.go b/provisioner/shell/provisioner.go index 3c32b3fc2..37bbf421b 100644 --- a/provisioner/shell/provisioner.go +++ b/provisioner/shell/provisioner.go @@ -75,11 +75,6 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { err := config.Decode(&p.config, &config.DecodeOpts{ Interpolate: true, InterpolateContext: &p.config.ctx, - InterpolateFilter: &interpolate.RenderFilter{ - Exclude: []string{ - "execute_command", - }, - }, }, raws...) if err != nil { return err diff --git a/website/source/docs/provisioners/ansible.html.markdown b/website/source/docs/provisioners/ansible.html.markdown new file mode 100644 index 000000000..e91f55f7a --- /dev/null +++ b/website/source/docs/provisioners/ansible.html.markdown @@ -0,0 +1,66 @@ +--- +layout: "docs" +page_title: "Ansible Provisioner" +description: |- + The `ansible` Packer provisioner allows Ansible playbooks to be run to provision the machine. +--- + +# Ansible Provisioner + +Type: `ansible` + +The `ansible` Packer provisioner allows Ansible playbooks to be run to provision the machine. + +## Basic Example + +This is a fully functional template that will provision an image on +DigitalOcean. Replace the mock `api_token` value with your own. + +```json +{ + "provisioners": [ + { + "type": "ansible", + "playbook_file": "./playbook.yml", + "extra_arguments": ["--private-key", "./id_packer-ansible", "-v", "-c", "paramiko"], + "ssh_authorized_key_file": "./id_packer-ansible.pub", + "ssh_host_key_file": "./packer_host_private_key" + } + ], + + "builders": [ + { + "type": "digitalocean", + "api_token": "6a561151587389c7cf8faa2d83e94150a4202da0e2bad34dd2bf236018ffaeeb", + "image": "ubuntu-14-04-x64", + "region": "sfo1" + }, + ] +} +``` + +## Configuration Reference + +Required Parameters: + +- `playbook_file` - The playbook file to be run by Ansible. + +- `ssh_host_key_file` - The SSH key that will be used to run the SSH server to which Ansible connects. + +- `ssh_authorized_key_file` - The SSH public key of the Ansible `ssh_user`. + +Optional Parameters: + +- `local_port` (string) - The port on which to + attempt to listen for SSH connections. This value is a starting point. + The provisioner will attempt listen for SSH connections on the first + available of ten ports, starting at `local_port`. The default value is 2200. + +- `sftp_command` (string) - The command to run on the machine to handle the + SFTP protocol that Ansible will use to transfer files. The command should + read and write on stdin and stdout, respectively. Defaults to + `/usr/lib/sftp-server -e`. + +## Limitations + +The `ansible` provisioner does not support SCP to transfer files. From 23415b3fe364cbcc28b1579ed9e01ca317533047 Mon Sep 17 00:00:00 2001 From: "Billie H. Cleek" Date: Mon, 9 Mar 2015 22:21:15 -0700 Subject: [PATCH 2/6] correct plugin imports --- plugin/provisioner-ansible/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/provisioner-ansible/main.go b/plugin/provisioner-ansible/main.go index 789339591..9be2683e0 100644 --- a/plugin/provisioner-ansible/main.go +++ b/plugin/provisioner-ansible/main.go @@ -1,8 +1,8 @@ package main import ( - "github.com/bhcleek/packer-provisioner-ansible/provisioner/ansible" "github.com/mitchellh/packer/packer/plugin" + "github.com/mitchellh/packer/provisioner/ansible" ) func main() { From 5366393f9ffa7655643dff55fc1a8388bffc4c29 Mon Sep 17 00:00:00 2001 From: "Billie H. Cleek" Date: Mon, 9 Mar 2015 22:25:45 -0700 Subject: [PATCH 3/6] fix tests --- provisioner/ansible/provisioner_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/provisioner/ansible/provisioner_test.go b/provisioner/ansible/provisioner_test.go index 0b52e47c2..80074ebde 100644 --- a/provisioner/ansible/provisioner_test.go +++ b/provisioner/ansible/provisioner_test.go @@ -51,7 +51,7 @@ func TestProvisionerPrepare_Defaults(t *testing.T) { } defer os.Remove(playbook_file.Name()) - config["ssh_private_host_key_file"] = hostkey_file.Name() + config["ssh_host_key_file"] = hostkey_file.Name() config["ssh_authorized_key_file"] = publickey_file.Name() config["playbook_file"] = playbook_file.Name() err = p.Prepare(config) @@ -76,7 +76,7 @@ func TestProvisionerPrepare_PlaybookFile(t *testing.T) { } defer os.Remove(publickey_file.Name()) - config["ssh_private_host_key_file"] = hostkey_file.Name() + config["ssh_host_key_file"] = hostkey_file.Name() config["ssh_authorized_key_file"] = publickey_file.Name() err = p.Prepare(config) @@ -119,13 +119,13 @@ func TestProvisionerPrepare_HostKeyFile(t *testing.T) { t.Fatal("could not create random file name") } - config["ssh_private_host_key_file"] = fmt.Sprintf("%x", filename) + config["ssh_host_key_file"] = fmt.Sprintf("%x", filename) config["ssh_authorized_key_file"] = publickey_file.Name() config["playbook_file"] = playbook_file.Name() err = p.Prepare(config) if err == nil { - t.Fatal("should error if ssh_private_host_key_file does not exist") + t.Fatal("should error if ssh_host_key_file does not exist") } hostkey_file, err := ioutil.TempFile("", "hostkey") @@ -134,7 +134,7 @@ func TestProvisionerPrepare_HostKeyFile(t *testing.T) { } defer os.Remove(hostkey_file.Name()) - config["ssh_private_host_key_file"] = hostkey_file.Name() + config["ssh_host_key_file"] = hostkey_file.Name() err = p.Prepare(config) if err != nil { t.Fatalf("err: %s", err) @@ -157,7 +157,7 @@ func TestProvisionerPrepare_AuthorizedKeyFile(t *testing.T) { } defer os.Remove(playbook_file.Name()) - config["ssh_private_host_key_file"] = hostkey_file.Name() + config["ssh_host_key_file"] = hostkey_file.Name() config["playbook_file"] = playbook_file.Name() err = p.Prepare(config) @@ -200,7 +200,7 @@ func TestProvisionerPrepare_LocalPort(t *testing.T) { } defer os.Remove(playbook_file.Name()) - config["ssh_private_host_key_file"] = hostkey_file.Name() + config["ssh_host_key_file"] = hostkey_file.Name() config["ssh_authorized_key_file"] = publickey_file.Name() config["playbook_file"] = playbook_file.Name() From 2dc9e0af3ff60d84575c18ba2b8c887bcf96a587 Mon Sep 17 00:00:00 2001 From: Billie Cleek Date: Sat, 2 May 2015 17:50:07 -0700 Subject: [PATCH 4/6] fix `go vet` identified issues --- provisioner/ansible/adapter.go | 9 ++++----- provisioner/ansible/provisioner.go | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/provisioner/ansible/adapter.go b/provisioner/ansible/adapter.go index 62cf54c60..9458e4117 100644 --- a/provisioner/ansible/adapter.go +++ b/provisioner/ansible/adapter.go @@ -52,6 +52,7 @@ func (c *adapter) Serve() { conn, err := c.l.Accept() select { case <-c.done: + close(errc) return default: if err != nil { @@ -62,8 +63,6 @@ func (c *adapter) Serve() { }(conn) } } - - close(errc) } func (c *adapter) Handle(conn net.Conn, errc chan<- error) error { @@ -83,9 +82,9 @@ func (c *adapter) Handle(conn net.Conn, errc chan<- error) error { continue } - go func(errc chan<- error) { - errc <- c.handleSession(newChannel) - }(errc) + go func(errc chan<- error, ch ssh.NewChannel) { + errc <- c.handleSession(ch) + }(errc, newChannel) } return nil diff --git a/provisioner/ansible/provisioner.go b/provisioner/ansible/provisioner.go index 063c9334e..acfb961b6 100644 --- a/provisioner/ansible/provisioner.go +++ b/provisioner/ansible/provisioner.go @@ -115,8 +115,8 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { keyChecker := ssh.CertChecker{ UserKeyFallback: func(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) { - if conn.User() != "packer-ansible" { - ui.Say(fmt.Sprintf("%s is not a valid user")) + if user := conn.User(); user != "packer-ansible" { + ui.Say(fmt.Sprintf("%s is not a valid user", user)) return nil, errors.New("authentication failed") } From d73e75a7cf20616f1463309d80a872a45b1607a0 Mon Sep 17 00:00:00 2001 From: Billie Cleek Date: Sat, 19 Dec 2015 12:15:04 -0800 Subject: [PATCH 5/6] fix panic when connection is closed before packer send Shutdown --- provisioner/ansible/adapter.go | 1 + 1 file changed, 1 insertion(+) diff --git a/provisioner/ansible/adapter.go b/provisioner/ansible/adapter.go index 9458e4117..e64939390 100644 --- a/provisioner/ansible/adapter.go +++ b/provisioner/ansible/adapter.go @@ -57,6 +57,7 @@ func (c *adapter) Serve() { default: if err != nil { c.ui.Error(fmt.Sprintf("listen.Accept failed: %v", err)) + continue } go func(conn net.Conn) { errc <- c.Handle(conn, errc) From 77c48678d6a4af459b8adf2c567de652c5f6f9c4 Mon Sep 17 00:00:00 2001 From: Billie Cleek Date: Sat, 19 Dec 2015 13:05:59 -0800 Subject: [PATCH 6/6] eliminate possible race conditions Eliminate race-y use of the packer.Ui interface by wrapping it in a concurrency-safe implementation. --- provisioner/ansible/adapter.go | 27 ++---- provisioner/ansible/adapter_test.go | 142 ++++++++++++++++++++++++++++ provisioner/ansible/provisioner.go | 48 +++++++++- 3 files changed, 197 insertions(+), 20 deletions(-) create mode 100644 provisioner/ansible/adapter_test.go diff --git a/provisioner/ansible/adapter.go b/provisioner/ansible/adapter.go index e64939390..418aa3dc7 100644 --- a/provisioner/ansible/adapter.go +++ b/provisioner/ansible/adapter.go @@ -35,16 +35,6 @@ func newAdapter(done <-chan struct{}, l net.Listener, config *ssh.ServerConfig, func (c *adapter) Serve() { c.ui.Say(fmt.Sprintf("SSH proxy: serving on %s", c.l.Addr())) - errc := make(chan error, 1) - - go func(errc chan error) { - for err := range errc { - if err != nil { - c.ui.Error(err.Error()) - } - } - }(errc) - for { // Accept will return if either the underlying connection is closed or if a connection is made. // after returning, check to see if c.done can be received. If so, then Accept() returned because @@ -52,7 +42,6 @@ func (c *adapter) Serve() { conn, err := c.l.Accept() select { case <-c.done: - close(errc) return default: if err != nil { @@ -60,14 +49,16 @@ func (c *adapter) Serve() { continue } go func(conn net.Conn) { - errc <- c.Handle(conn, errc) + if err := c.Handle(conn, c.ui); err != nil { + c.ui.Error(err.Error()) + } }(conn) } } } -func (c *adapter) Handle(conn net.Conn, errc chan<- error) error { - c.ui.Say("SSH proxy: accepted connection") +func (c *adapter) Handle(conn net.Conn, ui packer.Ui) error { + c.ui.Message("SSH proxy: accepted connection") _, chans, reqs, err := ssh.NewServerConn(conn, c.config) if err != nil { return errors.New("failed to handshake") @@ -83,9 +74,11 @@ func (c *adapter) Handle(conn net.Conn, errc chan<- error) error { continue } - go func(errc chan<- error, ch ssh.NewChannel) { - errc <- c.handleSession(ch) - }(errc, newChannel) + go func(ch ssh.NewChannel) { + if err := c.handleSession(ch); err != nil { + c.ui.Error(err.Error()) + } + }(newChannel) } return nil diff --git a/provisioner/ansible/adapter_test.go b/provisioner/ansible/adapter_test.go new file mode 100644 index 000000000..dbe8174c6 --- /dev/null +++ b/provisioner/ansible/adapter_test.go @@ -0,0 +1,142 @@ +package ansible + +import ( + "errors" + "io" + "log" + "net" + "os" + "testing" + "time" + + "github.com/mitchellh/packer/packer" + + "golang.org/x/crypto/ssh" +) + +func TestAdapter_Serve(t *testing.T) { + + // done signals the adapter that the provisioner is done + done := make(chan struct{}) + + acceptC := make(chan struct{}) + l := listener{done: make(chan struct{}), acceptC: acceptC} + + config := &ssh.ServerConfig{} + + ui := new(ui) + + sut := newAdapter(done, &l, config, "", newUi(ui), communicator{}) + go func() { + i := 0 + for range acceptC { + i++ + if i == 4 { + close(done) + l.Close() + } + } + }() + + sut.Serve() +} + +type listener struct { + done chan struct{} + acceptC chan<- struct{} + i int +} + +func (l *listener) Accept() (net.Conn, error) { + log.Println("Accept() called") + l.acceptC <- struct{}{} + select { + case <-l.done: + log.Println("done, serving an error") + return nil, errors.New("listener is closed") + + case <-time.After(10 * time.Millisecond): + l.i++ + + if l.i%2 == 0 { + c1, c2 := net.Pipe() + + go func(c net.Conn) { + <-time.After(100 * time.Millisecond) + log.Println("closing c") + c.Close() + }(c1) + + return c2, nil + } + } + + return nil, errors.New("accept error") +} + +func (l *listener) Close() error { + close(l.done) + return nil +} + +func (l *listener) Addr() net.Addr { + return addr{} +} + +type addr struct{} + +func (a addr) Network() string { + return a.String() +} + +func (a addr) String() string { + return "test" +} + +type ui int + +func (u *ui) Ask(s string) (string, error) { + *u++ + return s, nil +} + +func (u *ui) Say(s string) { + *u++ + log.Println(s) +} + +func (u *ui) Message(s string) { + *u++ + log.Println(s) +} + +func (u *ui) Error(s string) { + *u++ + log.Println(s) +} + +func (u *ui) Machine(s1 string, s2 ...string) { + *u++ + log.Println(s1) + for _, s := range s2 { + log.Println(s) + } +} + +type communicator struct{} + +func (c communicator) Start(*packer.RemoteCmd) error { + return errors.New("communicator not supported") +} + +func (c communicator) Upload(string, io.Reader, *os.FileInfo) error { + return errors.New("communicator not supported") +} + +func (c communicator) UploadDir(dst string, src string, exclude []string) error { + return errors.New("communicator not supported") +} + +func (c communicator) Download(string, io.Writer) error { + return errors.New("communicator not supported") +} diff --git a/provisioner/ansible/provisioner.go b/provisioner/ansible/provisioner.go index acfb961b6..5eaa08c12 100644 --- a/provisioner/ansible/provisioner.go +++ b/provisioner/ansible/provisioner.go @@ -170,6 +170,7 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { return err } + ui = newUi(ui) p.adapter = newAdapter(p.done, localListener, config, p.config.SFTPCmd, ui, comm) defer func() { @@ -199,12 +200,11 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { }() } - if err := p.executeAnsible(ui, comm); err != nil { + if err := p.executeAnsible(ui); err != nil { return fmt.Errorf("Error executing Ansible: %s", err) } return nil - } func (p *Provisioner) Cancel() { @@ -217,7 +217,7 @@ func (p *Provisioner) Cancel() { os.Exit(0) } -func (p *Provisioner) executeAnsible(ui packer.Ui, comm packer.Communicator) error { +func (p *Provisioner) executeAnsible(ui packer.Ui) error { playbook, _ := filepath.Abs(p.config.PlaybookFile) inventory := p.config.inventoryFile @@ -275,3 +275,45 @@ func validateFileConfig(name string, config string, req bool) error { } return nil } + +// Ui provides concurrency-safe access to packer.Ui. +type Ui struct { + sem chan int + ui packer.Ui +} + +func newUi(ui packer.Ui) packer.Ui { + return &Ui{sem: make(chan int, 1), ui: ui} +} + +func (ui *Ui) Ask(s string) (string, error) { + ui.sem <- 1 + ret, err := ui.ui.Ask(s) + <-ui.sem + + return ret, err +} + +func (ui *Ui) Say(s string) { + ui.sem <- 1 + ui.ui.Say(s) + <-ui.sem +} + +func (ui *Ui) Message(s string) { + ui.sem <- 1 + ui.ui.Message(s) + <-ui.sem +} + +func (ui *Ui) Error(s string) { + ui.sem <- 1 + ui.ui.Error(s) + <-ui.sem +} + +func (ui *Ui) Machine(t string, args ...string) { + ui.sem <- 1 + ui.ui.Machine(t, args...) + <-ui.sem +}