github.com/ddnomad/packer@v1.3.2/provisioner/ansible/provisioner.go (about)

     1  package ansible
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"crypto/rand"
     7  	"crypto/rsa"
     8  	"crypto/x509"
     9  	"encoding/pem"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"io/ioutil"
    14  	"log"
    15  	"net"
    16  	"os"
    17  	"os/exec"
    18  	"os/user"
    19  	"path/filepath"
    20  	"regexp"
    21  	"strconv"
    22  	"strings"
    23  	"sync"
    24  	"unicode"
    25  
    26  	"golang.org/x/crypto/ssh"
    27  
    28  	"github.com/hashicorp/packer/common"
    29  	commonhelper "github.com/hashicorp/packer/helper/common"
    30  	"github.com/hashicorp/packer/helper/config"
    31  	"github.com/hashicorp/packer/packer"
    32  	"github.com/hashicorp/packer/template/interpolate"
    33  )
    34  
    35  type Config struct {
    36  	common.PackerConfig `mapstructure:",squash"`
    37  	ctx                 interpolate.Context
    38  
    39  	// The command to run ansible
    40  	Command string
    41  
    42  	// Extra options to pass to the ansible command
    43  	ExtraArguments []string `mapstructure:"extra_arguments"`
    44  
    45  	AnsibleEnvVars []string `mapstructure:"ansible_env_vars"`
    46  
    47  	// The main playbook file to execute.
    48  	PlaybookFile         string   `mapstructure:"playbook_file"`
    49  	Groups               []string `mapstructure:"groups"`
    50  	EmptyGroups          []string `mapstructure:"empty_groups"`
    51  	HostAlias            string   `mapstructure:"host_alias"`
    52  	User                 string   `mapstructure:"user"`
    53  	LocalPort            string   `mapstructure:"local_port"`
    54  	SSHHostKeyFile       string   `mapstructure:"ssh_host_key_file"`
    55  	SSHAuthorizedKeyFile string   `mapstructure:"ssh_authorized_key_file"`
    56  	SFTPCmd              string   `mapstructure:"sftp_command"`
    57  	SkipVersionCheck     bool     `mapstructure:"skip_version_check"`
    58  	UseSFTP              bool     `mapstructure:"use_sftp"`
    59  	InventoryDirectory   string   `mapstructure:"inventory_directory"`
    60  	InventoryFile        string   `mapstructure:"inventory_file"`
    61  }
    62  
    63  type Provisioner struct {
    64  	config            Config
    65  	adapter           *adapter
    66  	done              chan struct{}
    67  	ansibleVersion    string
    68  	ansibleMajVersion uint
    69  }
    70  
    71  type PassthroughTemplate struct {
    72  	WinRMPassword string
    73  }
    74  
    75  func (p *Provisioner) Prepare(raws ...interface{}) error {
    76  	p.done = make(chan struct{})
    77  
    78  	// Create passthrough for winrm password so we can fill it in once we know
    79  	// it
    80  	p.config.ctx.Data = &PassthroughTemplate{
    81  		WinRMPassword: `{{.WinRMPassword}}`,
    82  	}
    83  
    84  	err := config.Decode(&p.config, &config.DecodeOpts{
    85  		Interpolate:        true,
    86  		InterpolateContext: &p.config.ctx,
    87  		InterpolateFilter: &interpolate.RenderFilter{
    88  			Exclude: []string{},
    89  		},
    90  	}, raws...)
    91  	if err != nil {
    92  		return err
    93  	}
    94  
    95  	// Defaults
    96  	if p.config.Command == "" {
    97  		p.config.Command = "ansible-playbook"
    98  	}
    99  
   100  	if p.config.HostAlias == "" {
   101  		p.config.HostAlias = "default"
   102  	}
   103  
   104  	var errs *packer.MultiError
   105  	err = validateFileConfig(p.config.PlaybookFile, "playbook_file", true)
   106  	if err != nil {
   107  		errs = packer.MultiErrorAppend(errs, err)
   108  	}
   109  
   110  	// Check that the authorized key file exists
   111  	if len(p.config.SSHAuthorizedKeyFile) > 0 {
   112  		err = validateFileConfig(p.config.SSHAuthorizedKeyFile, "ssh_authorized_key_file", true)
   113  		if err != nil {
   114  			log.Println(p.config.SSHAuthorizedKeyFile, "does not exist")
   115  			errs = packer.MultiErrorAppend(errs, err)
   116  		}
   117  	}
   118  	if len(p.config.SSHHostKeyFile) > 0 {
   119  		err = validateFileConfig(p.config.SSHHostKeyFile, "ssh_host_key_file", true)
   120  		if err != nil {
   121  			log.Println(p.config.SSHHostKeyFile, "does not exist")
   122  			errs = packer.MultiErrorAppend(errs, err)
   123  		}
   124  	} else {
   125  		p.config.AnsibleEnvVars = append(p.config.AnsibleEnvVars, "ANSIBLE_HOST_KEY_CHECKING=False")
   126  	}
   127  
   128  	if !p.config.UseSFTP {
   129  		p.config.AnsibleEnvVars = append(p.config.AnsibleEnvVars, "ANSIBLE_SCP_IF_SSH=True")
   130  	}
   131  
   132  	if len(p.config.LocalPort) > 0 {
   133  		if _, err := strconv.ParseUint(p.config.LocalPort, 10, 16); err != nil {
   134  			errs = packer.MultiErrorAppend(errs, fmt.Errorf("local_port: %s must be a valid port", p.config.LocalPort))
   135  		}
   136  	} else {
   137  		p.config.LocalPort = "0"
   138  	}
   139  
   140  	if len(p.config.InventoryDirectory) > 0 {
   141  		err = validateInventoryDirectoryConfig(p.config.InventoryDirectory)
   142  		if err != nil {
   143  			log.Println(p.config.InventoryDirectory, "does not exist")
   144  			errs = packer.MultiErrorAppend(errs, err)
   145  		}
   146  	}
   147  
   148  	if !p.config.SkipVersionCheck {
   149  		err = p.getVersion()
   150  		if err != nil {
   151  			errs = packer.MultiErrorAppend(errs, err)
   152  		}
   153  	}
   154  
   155  	if p.config.User == "" {
   156  		usr, err := user.Current()
   157  		if err != nil {
   158  			errs = packer.MultiErrorAppend(errs, err)
   159  		} else {
   160  			p.config.User = usr.Username
   161  		}
   162  	}
   163  	if p.config.User == "" {
   164  		errs = packer.MultiErrorAppend(errs, fmt.Errorf("user: could not determine current user from environment."))
   165  	}
   166  
   167  	if errs != nil && len(errs.Errors) > 0 {
   168  		return errs
   169  	}
   170  	return nil
   171  }
   172  
   173  func (p *Provisioner) getVersion() error {
   174  	out, err := exec.Command(p.config.Command, "--version").Output()
   175  	if err != nil {
   176  		return fmt.Errorf(
   177  			"Error running \"%s --version\": %s", p.config.Command, err.Error())
   178  	}
   179  
   180  	versionRe := regexp.MustCompile(`\w (\d+\.\d+[.\d+]*)`)
   181  	matches := versionRe.FindStringSubmatch(string(out))
   182  	if matches == nil {
   183  		return fmt.Errorf(
   184  			"Could not find %s version in output:\n%s", p.config.Command, string(out))
   185  	}
   186  
   187  	version := matches[1]
   188  	log.Printf("%s version: %s", p.config.Command, version)
   189  	p.ansibleVersion = version
   190  
   191  	majVer, err := strconv.ParseUint(strings.Split(version, ".")[0], 10, 0)
   192  	if err != nil {
   193  		return fmt.Errorf("Could not parse major version from \"%s\".", version)
   194  	}
   195  	p.ansibleMajVersion = uint(majVer)
   196  
   197  	return nil
   198  }
   199  
   200  func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
   201  	ui.Say("Provisioning with Ansible...")
   202  	// Interpolate env vars to check for .WinRMPassword
   203  	p.config.ctx.Data = &PassthroughTemplate{
   204  		WinRMPassword: getWinRMPassword(p.config.PackerBuildName),
   205  	}
   206  	for i, envVar := range p.config.AnsibleEnvVars {
   207  		envVar, err := interpolate.Render(envVar, &p.config.ctx)
   208  		if err != nil {
   209  			return fmt.Errorf("Could not interpolate ansible env vars: %s", err)
   210  		}
   211  		p.config.AnsibleEnvVars[i] = envVar
   212  	}
   213  	// Interpolate extra vars to check for .WinRMPassword
   214  	for i, arg := range p.config.ExtraArguments {
   215  		arg, err := interpolate.Render(arg, &p.config.ctx)
   216  		if err != nil {
   217  			return fmt.Errorf("Could not interpolate ansible env vars: %s", err)
   218  		}
   219  		p.config.ExtraArguments[i] = arg
   220  	}
   221  
   222  	k, err := newUserKey(p.config.SSHAuthorizedKeyFile)
   223  	if err != nil {
   224  		return err
   225  	}
   226  
   227  	hostSigner, err := newSigner(p.config.SSHHostKeyFile)
   228  	// Remove the private key file
   229  	if len(k.privKeyFile) > 0 {
   230  		defer os.Remove(k.privKeyFile)
   231  	}
   232  
   233  	keyChecker := ssh.CertChecker{
   234  		UserKeyFallback: func(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
   235  			if user := conn.User(); user != p.config.User {
   236  				return nil, errors.New(fmt.Sprintf("authentication failed: %s is not a valid user", user))
   237  			}
   238  
   239  			if !bytes.Equal(k.Marshal(), pubKey.Marshal()) {
   240  				return nil, errors.New("authentication failed: unauthorized key")
   241  			}
   242  
   243  			return nil, nil
   244  		},
   245  	}
   246  
   247  	config := &ssh.ServerConfig{
   248  		AuthLogCallback: func(conn ssh.ConnMetadata, method string, err error) {
   249  			log.Printf("authentication attempt from %s to %s as %s using %s", conn.RemoteAddr(), conn.LocalAddr(), conn.User(), method)
   250  		},
   251  		PublicKeyCallback: keyChecker.Authenticate,
   252  		//NoClientAuth:      true,
   253  	}
   254  
   255  	config.AddHostKey(hostSigner)
   256  
   257  	localListener, err := func() (net.Listener, error) {
   258  		port, err := strconv.ParseUint(p.config.LocalPort, 10, 16)
   259  		if err != nil {
   260  			return nil, err
   261  		}
   262  
   263  		tries := 1
   264  		if port != 0 {
   265  			tries = 10
   266  		}
   267  		for i := 0; i < tries; i++ {
   268  			l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
   269  			port++
   270  			if err != nil {
   271  				ui.Say(err.Error())
   272  				continue
   273  			}
   274  			_, p.config.LocalPort, err = net.SplitHostPort(l.Addr().String())
   275  			if err != nil {
   276  				ui.Say(err.Error())
   277  				continue
   278  			}
   279  			return l, nil
   280  		}
   281  		return nil, errors.New("Error setting up SSH proxy connection")
   282  	}()
   283  
   284  	if err != nil {
   285  		return err
   286  	}
   287  
   288  	ui = newUi(ui)
   289  	p.adapter = newAdapter(p.done, localListener, config, p.config.SFTPCmd, ui, comm)
   290  
   291  	defer func() {
   292  		log.Print("shutting down the SSH proxy")
   293  		close(p.done)
   294  		p.adapter.Shutdown()
   295  	}()
   296  
   297  	go p.adapter.Serve()
   298  
   299  	if len(p.config.InventoryFile) == 0 {
   300  		tf, err := ioutil.TempFile(p.config.InventoryDirectory, "packer-provisioner-ansible")
   301  		if err != nil {
   302  			return fmt.Errorf("Error preparing inventory file: %s", err)
   303  		}
   304  		defer os.Remove(tf.Name())
   305  
   306  		host := fmt.Sprintf("%s ansible_host=127.0.0.1 ansible_user=%s ansible_port=%s\n",
   307  			p.config.HostAlias, p.config.User, p.config.LocalPort)
   308  		if p.ansibleMajVersion < 2 {
   309  			host = fmt.Sprintf("%s ansible_ssh_host=127.0.0.1 ansible_ssh_user=%s ansible_ssh_port=%s\n",
   310  				p.config.HostAlias, p.config.User, p.config.LocalPort)
   311  		}
   312  
   313  		w := bufio.NewWriter(tf)
   314  		w.WriteString(host)
   315  		for _, group := range p.config.Groups {
   316  			fmt.Fprintf(w, "[%s]\n%s", group, host)
   317  		}
   318  
   319  		for _, group := range p.config.EmptyGroups {
   320  			fmt.Fprintf(w, "[%s]\n", group)
   321  		}
   322  
   323  		if err := w.Flush(); err != nil {
   324  			tf.Close()
   325  			return fmt.Errorf("Error preparing inventory file: %s", err)
   326  		}
   327  		tf.Close()
   328  		p.config.InventoryFile = tf.Name()
   329  		defer func() {
   330  			p.config.InventoryFile = ""
   331  		}()
   332  	}
   333  
   334  	if err := p.executeAnsible(ui, comm, k.privKeyFile); err != nil {
   335  		return fmt.Errorf("Error executing Ansible: %s", err)
   336  	}
   337  
   338  	return nil
   339  }
   340  
   341  func (p *Provisioner) Cancel() {
   342  	if p.done != nil {
   343  		close(p.done)
   344  	}
   345  	if p.adapter != nil {
   346  		p.adapter.Shutdown()
   347  	}
   348  	os.Exit(0)
   349  }
   350  
   351  func (p *Provisioner) executeAnsible(ui packer.Ui, comm packer.Communicator, privKeyFile string) error {
   352  	playbook, _ := filepath.Abs(p.config.PlaybookFile)
   353  	inventory := p.config.InventoryFile
   354  	if len(p.config.InventoryDirectory) > 0 {
   355  		inventory = p.config.InventoryDirectory
   356  	}
   357  	var envvars []string
   358  
   359  	args := []string{"--extra-vars", fmt.Sprintf("packer_build_name=%s packer_builder_type=%s",
   360  		p.config.PackerBuildName, p.config.PackerBuilderType),
   361  		"-i", inventory, playbook}
   362  	if len(privKeyFile) > 0 {
   363  		// Changed this from using --private-key to supplying -e ansible_ssh_private_key_file as the latter
   364  		// is treated as a highest priority variable, and thus prevents overriding by dynamic variables
   365  		// as seen in #5852
   366  		// args = append(args, "--private-key", privKeyFile)
   367  		args = append(args, "-e", fmt.Sprintf("ansible_ssh_private_key_file=%s", privKeyFile))
   368  	}
   369  
   370  	// expose packer_http_addr extra variable
   371  	httpAddr := common.GetHTTPAddr()
   372  	if httpAddr != "" {
   373  		args = append(args, "--extra-vars", fmt.Sprintf("packer_http_addr=%s", httpAddr))
   374  	}
   375  
   376  	args = append(args, p.config.ExtraArguments...)
   377  	if len(p.config.AnsibleEnvVars) > 0 {
   378  		envvars = append(envvars, p.config.AnsibleEnvVars...)
   379  	}
   380  
   381  	cmd := exec.Command(p.config.Command, args...)
   382  
   383  	cmd.Env = os.Environ()
   384  	if len(envvars) > 0 {
   385  		cmd.Env = append(cmd.Env, envvars...)
   386  	}
   387  
   388  	stdout, err := cmd.StdoutPipe()
   389  	if err != nil {
   390  		return err
   391  	}
   392  	stderr, err := cmd.StderrPipe()
   393  	if err != nil {
   394  		return err
   395  	}
   396  
   397  	wg := sync.WaitGroup{}
   398  	repeat := func(r io.ReadCloser) {
   399  		reader := bufio.NewReader(r)
   400  		for {
   401  			line, err := reader.ReadString('\n')
   402  			if line != "" {
   403  				line = strings.TrimRightFunc(line, unicode.IsSpace)
   404  				ui.Message(line)
   405  			}
   406  			if err != nil {
   407  				if err == io.EOF {
   408  					break
   409  				} else {
   410  					ui.Error(err.Error())
   411  					break
   412  				}
   413  			}
   414  		}
   415  		wg.Done()
   416  	}
   417  	wg.Add(2)
   418  	go repeat(stdout)
   419  	go repeat(stderr)
   420  
   421  	// remove winrm password from command, if it's been added
   422  	flattenedCmd := strings.Join(cmd.Args, " ")
   423  	sanitized := flattenedCmd
   424  	if len(getWinRMPassword(p.config.PackerBuildName)) > 0 {
   425  		sanitized = strings.Replace(sanitized,
   426  			getWinRMPassword(p.config.PackerBuildName), "*****", -1)
   427  	}
   428  	ui.Say(fmt.Sprintf("Executing Ansible: %s", sanitized))
   429  
   430  	if err := cmd.Start(); err != nil {
   431  		return err
   432  	}
   433  	wg.Wait()
   434  	err = cmd.Wait()
   435  	if err != nil {
   436  		return fmt.Errorf("Non-zero exit status: %s", err)
   437  	}
   438  
   439  	return nil
   440  }
   441  
   442  func validateFileConfig(name string, config string, req bool) error {
   443  	if req {
   444  		if name == "" {
   445  			return fmt.Errorf("%s must be specified.", config)
   446  		}
   447  	}
   448  	info, err := os.Stat(name)
   449  	if err != nil {
   450  		return fmt.Errorf("%s: %s is invalid: %s", config, name, err)
   451  	} else if info.IsDir() {
   452  		return fmt.Errorf("%s: %s must point to a file", config, name)
   453  	}
   454  	return nil
   455  }
   456  
   457  func validateInventoryDirectoryConfig(name string) error {
   458  	info, err := os.Stat(name)
   459  	if err != nil {
   460  		return fmt.Errorf("inventory_directory: %s is invalid: %s", name, err)
   461  	} else if !info.IsDir() {
   462  		return fmt.Errorf("inventory_directory: %s must point to a directory", name)
   463  	}
   464  	return nil
   465  }
   466  
   467  type userKey struct {
   468  	ssh.PublicKey
   469  	privKeyFile string
   470  }
   471  
   472  func newUserKey(pubKeyFile string) (*userKey, error) {
   473  	userKey := new(userKey)
   474  	if len(pubKeyFile) > 0 {
   475  		pubKeyBytes, err := ioutil.ReadFile(pubKeyFile)
   476  		if err != nil {
   477  			return nil, errors.New("Failed to read public key")
   478  		}
   479  		userKey.PublicKey, _, _, _, err = ssh.ParseAuthorizedKey(pubKeyBytes)
   480  		if err != nil {
   481  			return nil, errors.New("Failed to parse authorized key")
   482  		}
   483  
   484  		return userKey, nil
   485  	}
   486  
   487  	key, err := rsa.GenerateKey(rand.Reader, 2048)
   488  	if err != nil {
   489  		return nil, errors.New("Failed to generate key pair")
   490  	}
   491  	userKey.PublicKey, err = ssh.NewPublicKey(key.Public())
   492  	if err != nil {
   493  		return nil, errors.New("Failed to extract public key from generated key pair")
   494  	}
   495  
   496  	// To support Ansible calling back to us we need to write
   497  	// this file down
   498  	privateKeyDer := x509.MarshalPKCS1PrivateKey(key)
   499  	privateKeyBlock := pem.Block{
   500  		Type:    "RSA PRIVATE KEY",
   501  		Headers: nil,
   502  		Bytes:   privateKeyDer,
   503  	}
   504  	tf, err := ioutil.TempFile("", "ansible-key")
   505  	if err != nil {
   506  		return nil, errors.New("failed to create temp file for generated key")
   507  	}
   508  	_, err = tf.Write(pem.EncodeToMemory(&privateKeyBlock))
   509  	if err != nil {
   510  		return nil, errors.New("failed to write private key to temp file")
   511  	}
   512  
   513  	err = tf.Close()
   514  	if err != nil {
   515  		return nil, errors.New("failed to close private key temp file")
   516  	}
   517  	userKey.privKeyFile = tf.Name()
   518  
   519  	return userKey, nil
   520  }
   521  
   522  type signer struct {
   523  	ssh.Signer
   524  }
   525  
   526  func newSigner(privKeyFile string) (*signer, error) {
   527  	signer := new(signer)
   528  
   529  	if len(privKeyFile) > 0 {
   530  		privateBytes, err := ioutil.ReadFile(privKeyFile)
   531  		if err != nil {
   532  			return nil, errors.New("Failed to load private host key")
   533  		}
   534  
   535  		signer.Signer, err = ssh.ParsePrivateKey(privateBytes)
   536  		if err != nil {
   537  			return nil, errors.New("Failed to parse private host key")
   538  		}
   539  
   540  		return signer, nil
   541  	}
   542  
   543  	key, err := rsa.GenerateKey(rand.Reader, 2048)
   544  	if err != nil {
   545  		return nil, errors.New("Failed to generate server key pair")
   546  	}
   547  
   548  	signer.Signer, err = ssh.NewSignerFromKey(key)
   549  	if err != nil {
   550  		return nil, errors.New("Failed to extract private key from generated key pair")
   551  	}
   552  
   553  	return signer, nil
   554  }
   555  
   556  func getWinRMPassword(buildName string) string {
   557  	winRMPass, _ := commonhelper.RetrieveSharedState("winrm_password", buildName)
   558  	packer.LogSecretFilter.Set(winRMPass)
   559  	return winRMPass
   560  }
   561  
   562  // Ui provides concurrency-safe access to packer.Ui.
   563  type Ui struct {
   564  	sem chan int
   565  	ui  packer.Ui
   566  }
   567  
   568  func newUi(ui packer.Ui) packer.Ui {
   569  	return &Ui{sem: make(chan int, 1), ui: ui}
   570  }
   571  
   572  func (ui *Ui) Ask(s string) (string, error) {
   573  	ui.sem <- 1
   574  	ret, err := ui.ui.Ask(s)
   575  	<-ui.sem
   576  
   577  	return ret, err
   578  }
   579  
   580  func (ui *Ui) Say(s string) {
   581  	ui.sem <- 1
   582  	ui.ui.Say(s)
   583  	<-ui.sem
   584  }
   585  
   586  func (ui *Ui) Message(s string) {
   587  	ui.sem <- 1
   588  	ui.ui.Message(s)
   589  	<-ui.sem
   590  }
   591  
   592  func (ui *Ui) Error(s string) {
   593  	ui.sem <- 1
   594  	ui.ui.Error(s)
   595  	<-ui.sem
   596  }
   597  
   598  func (ui *Ui) Machine(t string, args ...string) {
   599  	ui.sem <- 1
   600  	ui.ui.Machine(t, args...)
   601  	<-ui.sem
   602  }
   603  
   604  func (ui *Ui) ProgressBar() packer.ProgressBar {
   605  	return new(packer.NoopProgressBar)
   606  }