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

     1  package ansiblelocal
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"github.com/hashicorp/packer/common"
    11  	"github.com/hashicorp/packer/common/uuid"
    12  	"github.com/hashicorp/packer/helper/config"
    13  	"github.com/hashicorp/packer/packer"
    14  	"github.com/hashicorp/packer/template/interpolate"
    15  )
    16  
    17  const DefaultStagingDir = "/tmp/packer-provisioner-ansible-local"
    18  
    19  type Config struct {
    20  	common.PackerConfig `mapstructure:",squash"`
    21  	ctx                 interpolate.Context
    22  
    23  	// The command to run ansible
    24  	Command string
    25  
    26  	// Extra options to pass to the ansible command
    27  	ExtraArguments []string `mapstructure:"extra_arguments"`
    28  
    29  	// Path to group_vars directory
    30  	GroupVars string `mapstructure:"group_vars"`
    31  
    32  	// Path to host_vars directory
    33  	HostVars string `mapstructure:"host_vars"`
    34  
    35  	// The playbook dir to upload.
    36  	PlaybookDir string `mapstructure:"playbook_dir"`
    37  
    38  	// The main playbook file to execute.
    39  	PlaybookFile string `mapstructure:"playbook_file"`
    40  
    41  	// The playbook files to execute.
    42  	PlaybookFiles []string `mapstructure:"playbook_files"`
    43  
    44  	// An array of local paths of playbook files to upload.
    45  	PlaybookPaths []string `mapstructure:"playbook_paths"`
    46  
    47  	// An array of local paths of roles to upload.
    48  	RolePaths []string `mapstructure:"role_paths"`
    49  
    50  	// The directory where files will be uploaded. Packer requires write
    51  	// permissions in this directory.
    52  	StagingDir string `mapstructure:"staging_directory"`
    53  
    54  	// If true, staging directory is removed after executing ansible.
    55  	CleanStagingDir bool `mapstructure:"clean_staging_directory"`
    56  
    57  	// The optional inventory file
    58  	InventoryFile string `mapstructure:"inventory_file"`
    59  
    60  	// The optional inventory groups
    61  	InventoryGroups []string `mapstructure:"inventory_groups"`
    62  
    63  	// The optional ansible-galaxy requirements file
    64  	GalaxyFile string `mapstructure:"galaxy_file"`
    65  
    66  	// The command to run ansible-galaxy
    67  	GalaxyCommand string
    68  }
    69  
    70  type Provisioner struct {
    71  	config Config
    72  
    73  	playbookFiles []string
    74  }
    75  
    76  func (p *Provisioner) Prepare(raws ...interface{}) error {
    77  	err := config.Decode(&p.config, &config.DecodeOpts{
    78  		Interpolate:        true,
    79  		InterpolateContext: &p.config.ctx,
    80  		InterpolateFilter: &interpolate.RenderFilter{
    81  			Exclude: []string{},
    82  		},
    83  	}, raws...)
    84  	if err != nil {
    85  		return err
    86  	}
    87  
    88  	// Reset the state.
    89  	p.playbookFiles = make([]string, 0, len(p.config.PlaybookFiles))
    90  
    91  	// Defaults
    92  	if p.config.Command == "" {
    93  		p.config.Command = "ANSIBLE_FORCE_COLOR=1 PYTHONUNBUFFERED=1 ansible-playbook"
    94  	}
    95  	if p.config.GalaxyCommand == "" {
    96  		p.config.GalaxyCommand = "ansible-galaxy"
    97  	}
    98  
    99  	if p.config.StagingDir == "" {
   100  		p.config.StagingDir = filepath.ToSlash(filepath.Join(DefaultStagingDir, uuid.TimeOrderedUUID()))
   101  	}
   102  
   103  	// Validation
   104  	var errs *packer.MultiError
   105  
   106  	// Check that either playbook_file or playbook_files is specified
   107  	if len(p.config.PlaybookFiles) != 0 && p.config.PlaybookFile != "" {
   108  		errs = packer.MultiErrorAppend(errs, fmt.Errorf("Either playbook_file or playbook_files can be specified, not both"))
   109  	}
   110  	if len(p.config.PlaybookFiles) == 0 && p.config.PlaybookFile == "" {
   111  		errs = packer.MultiErrorAppend(errs, fmt.Errorf("Either playbook_file or playbook_files must be specified"))
   112  	}
   113  	if p.config.PlaybookFile != "" {
   114  		err = validateFileConfig(p.config.PlaybookFile, "playbook_file", true)
   115  		if err != nil {
   116  			errs = packer.MultiErrorAppend(errs, err)
   117  		}
   118  	}
   119  
   120  	for _, playbookFile := range p.config.PlaybookFiles {
   121  		if err := validateFileConfig(playbookFile, "playbook_files", true); err != nil {
   122  			errs = packer.MultiErrorAppend(errs, err)
   123  		} else {
   124  			playbookFile, err := filepath.Abs(playbookFile)
   125  			if err != nil {
   126  				errs = packer.MultiErrorAppend(errs, err)
   127  			} else {
   128  				p.playbookFiles = append(p.playbookFiles, playbookFile)
   129  			}
   130  		}
   131  	}
   132  
   133  	// Check that the inventory file exists, if configured
   134  	if len(p.config.InventoryFile) > 0 {
   135  		err = validateFileConfig(p.config.InventoryFile, "inventory_file", true)
   136  		if err != nil {
   137  			errs = packer.MultiErrorAppend(errs, err)
   138  		}
   139  	}
   140  
   141  	// Check that the galaxy file exists, if configured
   142  	if len(p.config.GalaxyFile) > 0 {
   143  		err = validateFileConfig(p.config.GalaxyFile, "galaxy_file", true)
   144  		if err != nil {
   145  			errs = packer.MultiErrorAppend(errs, err)
   146  		}
   147  	}
   148  
   149  	// Check that the playbook_dir directory exists, if configured
   150  	if len(p.config.PlaybookDir) > 0 {
   151  		if err := validateDirConfig(p.config.PlaybookDir, "playbook_dir"); err != nil {
   152  			errs = packer.MultiErrorAppend(errs, err)
   153  		}
   154  	}
   155  
   156  	// Check that the group_vars directory exists, if configured
   157  	if len(p.config.GroupVars) > 0 {
   158  		if err := validateDirConfig(p.config.GroupVars, "group_vars"); err != nil {
   159  			errs = packer.MultiErrorAppend(errs, err)
   160  		}
   161  	}
   162  
   163  	// Check that the host_vars directory exists, if configured
   164  	if len(p.config.HostVars) > 0 {
   165  		if err := validateDirConfig(p.config.HostVars, "host_vars"); err != nil {
   166  			errs = packer.MultiErrorAppend(errs, err)
   167  		}
   168  	}
   169  
   170  	for _, path := range p.config.PlaybookPaths {
   171  		err := validateDirConfig(path, "playbook_paths")
   172  		if err != nil {
   173  			errs = packer.MultiErrorAppend(errs, err)
   174  		}
   175  	}
   176  	for _, path := range p.config.RolePaths {
   177  		if err := validateDirConfig(path, "role_paths"); err != nil {
   178  			errs = packer.MultiErrorAppend(errs, err)
   179  		}
   180  	}
   181  
   182  	if errs != nil && len(errs.Errors) > 0 {
   183  		return errs
   184  	}
   185  	return nil
   186  }
   187  
   188  func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
   189  	ui.Say("Provisioning with Ansible...")
   190  
   191  	if len(p.config.PlaybookDir) > 0 {
   192  		ui.Message("Uploading Playbook directory to Ansible staging directory...")
   193  		if err := p.uploadDir(ui, comm, p.config.StagingDir, p.config.PlaybookDir); err != nil {
   194  			return fmt.Errorf("Error uploading playbook_dir directory: %s", err)
   195  		}
   196  	} else {
   197  		ui.Message("Creating Ansible staging directory...")
   198  		if err := p.createDir(ui, comm, p.config.StagingDir); err != nil {
   199  			return fmt.Errorf("Error creating staging directory: %s", err)
   200  		}
   201  	}
   202  
   203  	if p.config.PlaybookFile != "" {
   204  		ui.Message("Uploading main Playbook file...")
   205  		src := p.config.PlaybookFile
   206  		dst := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(src)))
   207  		if err := p.uploadFile(ui, comm, dst, src); err != nil {
   208  			return fmt.Errorf("Error uploading main playbook: %s", err)
   209  		}
   210  	} else if err := p.provisionPlaybookFiles(ui, comm); err != nil {
   211  		return err
   212  	}
   213  
   214  	if len(p.config.InventoryFile) == 0 {
   215  		tf, err := ioutil.TempFile("", "packer-provisioner-ansible-local")
   216  		if err != nil {
   217  			return fmt.Errorf("Error preparing inventory file: %s", err)
   218  		}
   219  		defer os.Remove(tf.Name())
   220  		if len(p.config.InventoryGroups) != 0 {
   221  			content := ""
   222  			for _, group := range p.config.InventoryGroups {
   223  				content += fmt.Sprintf("[%s]\n127.0.0.1\n", group)
   224  			}
   225  			_, err = tf.Write([]byte(content))
   226  		} else {
   227  			_, err = tf.Write([]byte("127.0.0.1"))
   228  		}
   229  		if err != nil {
   230  			tf.Close()
   231  			return fmt.Errorf("Error preparing inventory file: %s", err)
   232  		}
   233  		tf.Close()
   234  		p.config.InventoryFile = tf.Name()
   235  		defer func() {
   236  			p.config.InventoryFile = ""
   237  		}()
   238  	}
   239  
   240  	if len(p.config.GalaxyFile) > 0 {
   241  		ui.Message("Uploading galaxy file...")
   242  		src := p.config.GalaxyFile
   243  		dst := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(src)))
   244  		if err := p.uploadFile(ui, comm, dst, src); err != nil {
   245  			return fmt.Errorf("Error uploading galaxy file: %s", err)
   246  		}
   247  	}
   248  
   249  	ui.Message("Uploading inventory file...")
   250  	src := p.config.InventoryFile
   251  	dst := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(src)))
   252  	if err := p.uploadFile(ui, comm, dst, src); err != nil {
   253  		return fmt.Errorf("Error uploading inventory file: %s", err)
   254  	}
   255  
   256  	if len(p.config.GroupVars) > 0 {
   257  		ui.Message("Uploading group_vars directory...")
   258  		src := p.config.GroupVars
   259  		dst := filepath.ToSlash(filepath.Join(p.config.StagingDir, "group_vars"))
   260  		if err := p.uploadDir(ui, comm, dst, src); err != nil {
   261  			return fmt.Errorf("Error uploading group_vars directory: %s", err)
   262  		}
   263  	}
   264  
   265  	if len(p.config.HostVars) > 0 {
   266  		ui.Message("Uploading host_vars directory...")
   267  		src := p.config.HostVars
   268  		dst := filepath.ToSlash(filepath.Join(p.config.StagingDir, "host_vars"))
   269  		if err := p.uploadDir(ui, comm, dst, src); err != nil {
   270  			return fmt.Errorf("Error uploading host_vars directory: %s", err)
   271  		}
   272  	}
   273  
   274  	if len(p.config.RolePaths) > 0 {
   275  		ui.Message("Uploading role directories...")
   276  		for _, src := range p.config.RolePaths {
   277  			dst := filepath.ToSlash(filepath.Join(p.config.StagingDir, "roles", filepath.Base(src)))
   278  			if err := p.uploadDir(ui, comm, dst, src); err != nil {
   279  				return fmt.Errorf("Error uploading roles: %s", err)
   280  			}
   281  		}
   282  	}
   283  
   284  	if len(p.config.PlaybookPaths) > 0 {
   285  		ui.Message("Uploading additional Playbooks...")
   286  		playbookDir := filepath.ToSlash(filepath.Join(p.config.StagingDir, "playbooks"))
   287  		if err := p.createDir(ui, comm, playbookDir); err != nil {
   288  			return fmt.Errorf("Error creating playbooks directory: %s", err)
   289  		}
   290  		for _, src := range p.config.PlaybookPaths {
   291  			dst := filepath.ToSlash(filepath.Join(playbookDir, filepath.Base(src)))
   292  			if err := p.uploadDir(ui, comm, dst, src); err != nil {
   293  				return fmt.Errorf("Error uploading playbooks: %s", err)
   294  			}
   295  		}
   296  	}
   297  
   298  	if err := p.executeAnsible(ui, comm); err != nil {
   299  		return fmt.Errorf("Error executing Ansible: %s", err)
   300  	}
   301  
   302  	if p.config.CleanStagingDir {
   303  		ui.Message("Removing staging directory...")
   304  		if err := p.removeDir(ui, comm, p.config.StagingDir); err != nil {
   305  			return fmt.Errorf("Error removing staging directory: %s", err)
   306  		}
   307  	}
   308  	return nil
   309  }
   310  
   311  func (p *Provisioner) Cancel() {
   312  	// Just hard quit. It isn't a big deal if what we're doing keeps
   313  	// running on the other side.
   314  	os.Exit(0)
   315  }
   316  
   317  func (p *Provisioner) provisionPlaybookFiles(ui packer.Ui, comm packer.Communicator) error {
   318  	var playbookDir string
   319  	if p.config.PlaybookDir != "" {
   320  		var err error
   321  		playbookDir, err = filepath.Abs(p.config.PlaybookDir)
   322  		if err != nil {
   323  			return err
   324  		}
   325  	}
   326  	for index, playbookFile := range p.playbookFiles {
   327  		if playbookDir != "" && strings.HasPrefix(playbookFile, playbookDir) {
   328  			p.playbookFiles[index] = strings.TrimPrefix(playbookFile, playbookDir)
   329  			continue
   330  		}
   331  		if err := p.provisionPlaybookFile(ui, comm, playbookFile); err != nil {
   332  			return err
   333  		}
   334  	}
   335  	return nil
   336  }
   337  
   338  func (p *Provisioner) provisionPlaybookFile(ui packer.Ui, comm packer.Communicator, playbookFile string) error {
   339  	ui.Message(fmt.Sprintf("Uploading playbook file: %s", playbookFile))
   340  
   341  	remoteDir := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Dir(playbookFile)))
   342  	remotePlaybookFile := filepath.ToSlash(filepath.Join(p.config.StagingDir, playbookFile))
   343  
   344  	if err := p.createDir(ui, comm, remoteDir); err != nil {
   345  		return fmt.Errorf("Error uploading playbook file: %s [%s]", playbookFile, err)
   346  	}
   347  
   348  	if err := p.uploadFile(ui, comm, remotePlaybookFile, playbookFile); err != nil {
   349  		return fmt.Errorf("Error uploading playbook: %s [%s]", playbookFile, err)
   350  	}
   351  
   352  	return nil
   353  }
   354  
   355  func (p *Provisioner) executeGalaxy(ui packer.Ui, comm packer.Communicator) error {
   356  	rolesDir := filepath.ToSlash(filepath.Join(p.config.StagingDir, "roles"))
   357  	galaxyFile := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(p.config.GalaxyFile)))
   358  
   359  	// ansible-galaxy install -r requirements.yml -p roles/
   360  	command := fmt.Sprintf("cd %s && %s install -r %s -p %s",
   361  		p.config.StagingDir, p.config.GalaxyCommand, galaxyFile, rolesDir)
   362  	ui.Message(fmt.Sprintf("Executing Ansible Galaxy: %s", command))
   363  	cmd := &packer.RemoteCmd{
   364  		Command: command,
   365  	}
   366  	if err := cmd.StartWithUi(comm, ui); err != nil {
   367  		return err
   368  	}
   369  	if cmd.ExitStatus != 0 {
   370  		// ansible-galaxy version 2.0.0.2 doesn't return exit codes on error..
   371  		return fmt.Errorf("Non-zero exit status: %d", cmd.ExitStatus)
   372  	}
   373  	return nil
   374  }
   375  
   376  func (p *Provisioner) executeAnsible(ui packer.Ui, comm packer.Communicator) error {
   377  	inventory := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(p.config.InventoryFile)))
   378  
   379  	extraArgs := fmt.Sprintf(" --extra-vars \"packer_build_name=%s packer_builder_type=%s packer_http_addr=%s\" ",
   380  		p.config.PackerBuildName, p.config.PackerBuilderType, common.GetHTTPAddr())
   381  	if len(p.config.ExtraArguments) > 0 {
   382  		extraArgs = extraArgs + strings.Join(p.config.ExtraArguments, " ")
   383  	}
   384  
   385  	// Fetch external dependencies
   386  	if len(p.config.GalaxyFile) > 0 {
   387  		if err := p.executeGalaxy(ui, comm); err != nil {
   388  			return fmt.Errorf("Error executing Ansible Galaxy: %s", err)
   389  		}
   390  	}
   391  
   392  	if p.config.PlaybookFile != "" {
   393  		playbookFile := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(p.config.PlaybookFile)))
   394  		if err := p.executeAnsiblePlaybook(ui, comm, playbookFile, extraArgs, inventory); err != nil {
   395  			return err
   396  		}
   397  	}
   398  
   399  	for _, playbookFile := range p.playbookFiles {
   400  		playbookFile = filepath.ToSlash(filepath.Join(p.config.StagingDir, playbookFile))
   401  		if err := p.executeAnsiblePlaybook(ui, comm, playbookFile, extraArgs, inventory); err != nil {
   402  			return err
   403  		}
   404  	}
   405  	return nil
   406  }
   407  
   408  func (p *Provisioner) executeAnsiblePlaybook(
   409  	ui packer.Ui, comm packer.Communicator, playbookFile, extraArgs, inventory string,
   410  ) error {
   411  	command := fmt.Sprintf("cd %s && %s %s%s -c local -i %s",
   412  		p.config.StagingDir, p.config.Command, playbookFile, extraArgs, inventory,
   413  	)
   414  	ui.Message(fmt.Sprintf("Executing Ansible: %s", command))
   415  	cmd := &packer.RemoteCmd{
   416  		Command: command,
   417  	}
   418  	if err := cmd.StartWithUi(comm, ui); err != nil {
   419  		return err
   420  	}
   421  	if cmd.ExitStatus != 0 {
   422  		if cmd.ExitStatus == 127 {
   423  			return fmt.Errorf("%s could not be found. Verify that it is available on the\n"+
   424  				"PATH after connecting to the machine.",
   425  				p.config.Command)
   426  		}
   427  
   428  		return fmt.Errorf("Non-zero exit status: %d", cmd.ExitStatus)
   429  	}
   430  	return nil
   431  }
   432  
   433  func validateDirConfig(path string, config string) error {
   434  	info, err := os.Stat(path)
   435  	if err != nil {
   436  		return fmt.Errorf("%s: %s is invalid: %s", config, path, err)
   437  	} else if !info.IsDir() {
   438  		return fmt.Errorf("%s: %s must point to a directory", config, path)
   439  	}
   440  	return nil
   441  }
   442  
   443  func validateFileConfig(name string, config string, req bool) error {
   444  	if req {
   445  		if name == "" {
   446  			return fmt.Errorf("%s must be specified.", config)
   447  		}
   448  	}
   449  	info, err := os.Stat(name)
   450  	if err != nil {
   451  		return fmt.Errorf("%s: %s is invalid: %s", config, name, err)
   452  	} else if info.IsDir() {
   453  		return fmt.Errorf("%s: %s must point to a file", config, name)
   454  	}
   455  	return nil
   456  }
   457  
   458  func (p *Provisioner) uploadFile(ui packer.Ui, comm packer.Communicator, dst, src string) error {
   459  	f, err := os.Open(src)
   460  	if err != nil {
   461  		return fmt.Errorf("Error opening: %s", err)
   462  	}
   463  	defer f.Close()
   464  
   465  	if err = comm.Upload(dst, f, nil); err != nil {
   466  		return fmt.Errorf("Error uploading %s: %s", src, err)
   467  	}
   468  	return nil
   469  }
   470  
   471  func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error {
   472  	cmd := &packer.RemoteCmd{
   473  		Command: fmt.Sprintf("mkdir -p '%s'", dir),
   474  	}
   475  
   476  	ui.Message(fmt.Sprintf("Creating directory: %s", dir))
   477  	if err := cmd.StartWithUi(comm, ui); err != nil {
   478  		return err
   479  	}
   480  
   481  	if cmd.ExitStatus != 0 {
   482  		return fmt.Errorf("Non-zero exit status. See output above for more information.")
   483  	}
   484  	return nil
   485  }
   486  
   487  func (p *Provisioner) removeDir(ui packer.Ui, comm packer.Communicator, dir string) error {
   488  	cmd := &packer.RemoteCmd{
   489  		Command: fmt.Sprintf("rm -rf '%s'", dir),
   490  	}
   491  
   492  	ui.Message(fmt.Sprintf("Removing directory: %s", dir))
   493  	if err := cmd.StartWithUi(comm, ui); err != nil {
   494  		return err
   495  	}
   496  
   497  	if cmd.ExitStatus != 0 {
   498  		return fmt.Errorf("Non-zero exit status. See output above for more information.")
   499  	}
   500  	return nil
   501  }
   502  
   503  func (p *Provisioner) uploadDir(ui packer.Ui, comm packer.Communicator, dst, src string) error {
   504  	if err := p.createDir(ui, comm, dst); err != nil {
   505  		return err
   506  	}
   507  
   508  	// Make sure there is a trailing "/" so that the directory isn't
   509  	// created on the other side.
   510  	if src[len(src)-1] != '/' {
   511  		src = src + "/"
   512  	}
   513  	return comm.UploadDir(dst, src, nil)
   514  }