diff --git a/command/plugin.go b/command/plugin.go index cf213b3ef..eeeafa154 100644 --- a/command/plugin.go +++ b/command/plugin.go @@ -16,6 +16,7 @@ import ( amazonchrootbuilder "github.com/mitchellh/packer/builder/amazon/chroot" amazonebsbuilder "github.com/mitchellh/packer/builder/amazon/ebs" amazoninstancebuilder "github.com/mitchellh/packer/builder/amazon/instance" + ansibleprovisioner "github.com/mitchellh/packer/provisioner/ansible" ansiblelocalprovisioner "github.com/mitchellh/packer/provisioner/ansible-local" artificepostprocessor "github.com/mitchellh/packer/post-processor/artifice" atlaspostprocessor "github.com/mitchellh/packer/post-processor/atlas" @@ -79,6 +80,7 @@ var Builders = map[string]packer.Builder{ var Provisioners = map[string]packer.Provisioner{ + "ansible": new(ansibleprovisioner.Provisioner), "ansible-local": new(ansiblelocalprovisioner.Provisioner), "chef-client": new(chefclientprovisioner.Provisioner), "chef-solo": new(chefsoloprovisioner.Provisioner), diff --git a/provisioner/ansible/provisioner.go b/provisioner/ansible/provisioner.go index 6efb255c2..f98a6c094 100644 --- a/provisioner/ansible/provisioner.go +++ b/provisioner/ansible/provisioner.go @@ -3,6 +3,10 @@ package ansible import ( "bufio" "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" "errors" "fmt" "io" @@ -74,12 +78,15 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { 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 authorized key file exists ( this should really be called the public key ) + // Check for either file ( if you specify either file you must specify both files ) + if len(p.config.SSHAuthorizedKeyFile) > 0 { + err = validateFileConfig(p.config.SSHAuthorizedKeyFile, "ssh_authorized_key_file", true) + if err != nil { + log.Println(p.config.SSHAuthorizedKeyFile, "does not exist") + 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 { @@ -102,17 +109,107 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { return nil } +type Keys struct { + //! This is the public key that we will allow to authenticate + public ssh.PublicKey + + //! This is the name of the file of the private key that coorlates + //the the public key + filename string + + //! This is the servers private key ( Set in case the public key + //is autogenerated ) + private ssh.Signer + + //! This is the flag to say the server key was generated + generated bool +} + 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") + keyFactory := func(pubKeyFile string, privKeyFile string) (*Keys, error) { + var public ssh.PublicKey + var private ssh.Signer + var filename string = "" + var generated bool = false + + if len(pubKeyFile) > 0 { + pubKeyBytes, err := ioutil.ReadFile(pubKeyFile) + if err != nil { + return nil, errors.New("Failed to read public key") + } + public, _, _, _, err = ssh.ParseAuthorizedKey(pubKeyBytes) + if err != nil { + return nil, errors.New("Failed to parse authorized key") + } + } else { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, errors.New("Failed to generate key pair") + } + public, err = ssh.NewPublicKey(key.Public()) + if err != nil { + return nil, errors.New("Failed to extract public key from generated key pair") + } + + // To support Ansible calling back to us we need to write + // this file down + privateKeyDer := x509.MarshalPKCS1PrivateKey(key) + privateKeyBlock := pem.Block{ + Type: "RSA PRIVATE KEY", + Headers: nil, + Bytes: privateKeyDer, + } + tf, err := ioutil.TempFile("", "ansible-key") + if err != nil { + return nil, errors.New("failed to create temp file for generated key") + } + _, err = tf.Write(pem.EncodeToMemory(&privateKeyBlock)) + if err != nil { + return nil, errors.New("failed to write private key to temp file") + } + + err = tf.Close() + if err != nil { + return nil, errors.New("failed to close private key temp file") + } + filename = tf.Name() + } + + if len(privKeyFile) > 0 { + privateBytes, err := ioutil.ReadFile(privKeyFile) + if err != nil { + return nil, errors.New("Failed to load private host key") + } + + private, err = ssh.ParsePrivateKey(privateBytes) + if err != nil { + return nil, errors.New("Failed to parse private host key") + } + } else { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, errors.New("Failed to generate server key pair") + } + + private, err = ssh.NewSignerFromKey(key) + if err != nil { + return nil, errors.New("Failed to extract private key from generated key pair") + } + generated = true + } + return &Keys { public, filename, private, generated },nil } - public, _, _, _, err := ssh.ParseAuthorizedKey(pubKeyBytes) + k, err := keyFactory(p.config.SSHAuthorizedKeyFile, p.config.SSHHostKeyFile) if err != nil { - return errors.New("Failed to parse authorized key") + return err + } + + // Remove the private key file + if len(k.filename) > 0 { + defer os.Remove(k.filename) } keyChecker := ssh.CertChecker{ @@ -122,7 +219,7 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { return nil, errors.New("authentication failed") } - if !bytes.Equal(public.Marshal(), pubKey.Marshal()) { + if !bytes.Equal(k.public.Marshal(), pubKey.Marshal()) { ui.Say("unauthorized key") return nil, errors.New("authentication failed") } @@ -130,6 +227,7 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { 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)) @@ -138,17 +236,7 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { //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) + config.AddHostKey(k.private) localListener, err := func() (net.Listener, error) { port, err := strconv.ParseUint(p.config.LocalPort, 10, 16) @@ -211,7 +299,7 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { }() } - if err := p.executeAnsible(ui); err != nil { + if err := p.executeAnsible(ui, comm, k.filename, k.generated); err != nil { return fmt.Errorf("Error executing Ansible: %s", err) } @@ -229,15 +317,24 @@ func (p *Provisioner) Cancel() { os.Exit(0) } -func (p *Provisioner) executeAnsible(ui packer.Ui) error { +func (p *Provisioner) executeAnsible(ui packer.Ui, comm packer.Communicator, authToken string, generated bool) error { playbook, _ := filepath.Abs(p.config.PlaybookFile) inventory := p.config.inventoryFile - args := []string{playbook, "-i", inventory} + args := []string{playbook, "-i", inventory } + if len(authToken) > 0 { + args = append(args,"--private-key",authToken) + } args = append(args, p.config.ExtraArguments...) - + cmd := exec.Command(p.config.Command, args...) + // If we have autogenerated the key files turn off host key checking + if generated { + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env,"ANSIBLE_HOST_KEY_CHECKING=False") + } + stdout, err := cmd.StdoutPipe() if err != nil { return err diff --git a/website/source/docs/provisioners/ansible.html.markdown b/website/source/docs/provisioners/ansible.html.markdown index 67b14e383..9a8a54444 100644 --- a/website/source/docs/provisioners/ansible.html.markdown +++ b/website/source/docs/provisioners/ansible.html.markdown @@ -45,12 +45,21 @@ 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: +- `ssh_host_key_file` (string) - The SSH key that will be used to run + the SSH server on the host machine to forward commands to the target + machine. Ansible connects to this server and will validate the + identity of the server using the system known_hosts. The default behaviour is + to generate and use a one time key, and disable + host_key_verification in ansible to allow it to connect to the + server + +- `ssh_authorized_key_file` (string) - The SSH public key of the + Ansible `ssh_user`. The default behaviour is to generate and use a + one time key. If this file is generated the coorisponding private + key will be passed via the `--private-key` option to Ansible. + - `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 @@ -58,11 +67,13 @@ Optional Parameters: listen on a system-chosen port. -- `sftp_command` (string) - The command to run on the machine to handle the +- `sftp_command` (string) - The command to run on the provisioned 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`. +- `extra_arguments` (string) - Extra arguments to pass to Ansible + ## Limitations The `ansible` provisioner does not support SCP to transfer files.