Merge branch 'ansible-provisioner' of https://github.com/bhcleek/packer into f-ansible
This commit is contained in:
commit
15f99a4aee
|
@ -0,0 +1,15 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/packer/packer/plugin"
|
||||
"github.com/mitchellh/packer/provisioner/ansible"
|
||||
)
|
||||
|
||||
func main() {
|
||||
server, err := plugin.Server()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
server.RegisterProvisioner(new(ansible.Provisioner))
|
||||
server.Serve()
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
package main
|
|
@ -0,0 +1,276 @@
|
|||
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()))
|
||||
|
||||
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))
|
||||
continue
|
||||
}
|
||||
go func(conn net.Conn) {
|
||||
if err := c.Handle(conn, c.ui); err != nil {
|
||||
c.ui.Error(err.Error())
|
||||
}
|
||||
}(conn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// 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(ch ssh.NewChannel) {
|
||||
if err := c.handleSession(ch); err != nil {
|
||||
c.ui.Error(err.Error())
|
||||
}
|
||||
}(newChannel)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -0,0 +1,319 @@
|
|||
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 user := conn.User(); user != "packer-ansible" {
|
||||
ui.Say(fmt.Sprintf("%s is not a valid user", 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
|
||||
}
|
||||
|
||||
ui = newUi(ui)
|
||||
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); 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) 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
|
@ -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_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_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_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_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_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_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_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)
|
||||
}
|
||||
}
|
|
@ -78,11 +78,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
|
||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue