github.com/mmcquillan/packer@v1.1.1-0.20171009221028-c85cf0483a5d/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  	// An array of local paths of playbook files to upload.
    42  	PlaybookPaths []string `mapstructure:"playbook_paths"`
    43  
    44  	// An array of local paths of roles to upload.
    45  	RolePaths []string `mapstructure:"role_paths"`
    46  
    47  	// The directory where files will be uploaded. Packer requires write
    48  	// permissions in this directory.
    49  	StagingDir string `mapstructure:"staging_directory"`
    50  
    51  	// The optional inventory file
    52  	InventoryFile string `mapstructure:"inventory_file"`
    53  
    54  	// The optional inventory groups
    55  	InventoryGroups []string `mapstructure:"inventory_groups"`
    56  
    57  	// The optional ansible-galaxy requirements file
    58  	GalaxyFile string `mapstructure:"galaxy_file"`
    59  
    60  	// The command to run ansible-galaxy
    61  	GalaxyCommand string
    62  }
    63  
    64  type Provisioner struct {
    65  	config Config
    66  }
    67  
    68  func (p *Provisioner) Prepare(raws ...interface{}) error {
    69  	err := config.Decode(&p.config, &config.DecodeOpts{
    70  		Interpolate:        true,
    71  		InterpolateContext: &p.config.ctx,
    72  		InterpolateFilter: &interpolate.RenderFilter{
    73  			Exclude: []string{},
    74  		},
    75  	}, raws...)
    76  	if err != nil {
    77  		return err
    78  	}
    79  
    80  	// Defaults
    81  	if p.config.Command == "" {
    82  		p.config.Command = "ANSIBLE_FORCE_COLOR=1 PYTHONUNBUFFERED=1 ansible-playbook"
    83  	}
    84  	if p.config.GalaxyCommand == "" {
    85  		p.config.GalaxyCommand = "ansible-galaxy"
    86  	}
    87  
    88  	if p.config.StagingDir == "" {
    89  		p.config.StagingDir = filepath.ToSlash(filepath.Join(DefaultStagingDir, uuid.TimeOrderedUUID()))
    90  	}
    91  
    92  	// Validation
    93  	var errs *packer.MultiError
    94  	err = validateFileConfig(p.config.PlaybookFile, "playbook_file", true)
    95  	if err != nil {
    96  		errs = packer.MultiErrorAppend(errs, err)
    97  	}
    98  
    99  	// Check that the inventory file exists, if configured
   100  	if len(p.config.InventoryFile) > 0 {
   101  		err = validateFileConfig(p.config.InventoryFile, "inventory_file", true)
   102  		if err != nil {
   103  			errs = packer.MultiErrorAppend(errs, err)
   104  		}
   105  	}
   106  
   107  	// Check that the galaxy file exists, if configured
   108  	if len(p.config.GalaxyFile) > 0 {
   109  		err = validateFileConfig(p.config.GalaxyFile, "galaxy_file", true)
   110  		if err != nil {
   111  			errs = packer.MultiErrorAppend(errs, err)
   112  		}
   113  	}
   114  
   115  	// Check that the playbook_dir directory exists, if configured
   116  	if len(p.config.PlaybookDir) > 0 {
   117  		if err := validateDirConfig(p.config.PlaybookDir, "playbook_dir"); err != nil {
   118  			errs = packer.MultiErrorAppend(errs, err)
   119  		}
   120  	}
   121  
   122  	// Check that the group_vars directory exists, if configured
   123  	if len(p.config.GroupVars) > 0 {
   124  		if err := validateDirConfig(p.config.GroupVars, "group_vars"); err != nil {
   125  			errs = packer.MultiErrorAppend(errs, err)
   126  		}
   127  	}
   128  
   129  	// Check that the host_vars directory exists, if configured
   130  	if len(p.config.HostVars) > 0 {
   131  		if err := validateDirConfig(p.config.HostVars, "host_vars"); err != nil {
   132  			errs = packer.MultiErrorAppend(errs, err)
   133  		}
   134  	}
   135  
   136  	for _, path := range p.config.PlaybookPaths {
   137  		err := validateDirConfig(path, "playbook_paths")
   138  		if err != nil {
   139  			errs = packer.MultiErrorAppend(errs, err)
   140  		}
   141  	}
   142  	for _, path := range p.config.RolePaths {
   143  		if err := validateDirConfig(path, "role_paths"); err != nil {
   144  			errs = packer.MultiErrorAppend(errs, err)
   145  		}
   146  	}
   147  
   148  	if errs != nil && len(errs.Errors) > 0 {
   149  		return errs
   150  	}
   151  	return nil
   152  }
   153  
   154  func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
   155  	ui.Say("Provisioning with Ansible...")
   156  
   157  	if len(p.config.PlaybookDir) > 0 {
   158  		ui.Message("Uploading Playbook directory to Ansible staging directory...")
   159  		if err := p.uploadDir(ui, comm, p.config.StagingDir, p.config.PlaybookDir); err != nil {
   160  			return fmt.Errorf("Error uploading playbook_dir directory: %s", err)
   161  		}
   162  	} else {
   163  		ui.Message("Creating Ansible staging directory...")
   164  		if err := p.createDir(ui, comm, p.config.StagingDir); err != nil {
   165  			return fmt.Errorf("Error creating staging directory: %s", err)
   166  		}
   167  	}
   168  
   169  	ui.Message("Uploading main Playbook file...")
   170  	src := p.config.PlaybookFile
   171  	dst := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(src)))
   172  	if err := p.uploadFile(ui, comm, dst, src); err != nil {
   173  		return fmt.Errorf("Error uploading main playbook: %s", err)
   174  	}
   175  
   176  	if len(p.config.InventoryFile) == 0 {
   177  		tf, err := ioutil.TempFile("", "packer-provisioner-ansible-local")
   178  		if err != nil {
   179  			return fmt.Errorf("Error preparing inventory file: %s", err)
   180  		}
   181  		defer os.Remove(tf.Name())
   182  		if len(p.config.InventoryGroups) != 0 {
   183  			content := ""
   184  			for _, group := range p.config.InventoryGroups {
   185  				content += fmt.Sprintf("[%s]\n127.0.0.1\n", group)
   186  			}
   187  			_, err = tf.Write([]byte(content))
   188  		} else {
   189  			_, err = tf.Write([]byte("127.0.0.1"))
   190  		}
   191  		if err != nil {
   192  			tf.Close()
   193  			return fmt.Errorf("Error preparing inventory file: %s", err)
   194  		}
   195  		tf.Close()
   196  		p.config.InventoryFile = tf.Name()
   197  		defer func() {
   198  			p.config.InventoryFile = ""
   199  		}()
   200  	}
   201  
   202  	if len(p.config.GalaxyFile) > 0 {
   203  		ui.Message("Uploading galaxy file...")
   204  		src = p.config.GalaxyFile
   205  		dst = filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(src)))
   206  		if err := p.uploadFile(ui, comm, dst, src); err != nil {
   207  			return fmt.Errorf("Error uploading galaxy file: %s", err)
   208  		}
   209  	}
   210  
   211  	ui.Message("Uploading inventory file...")
   212  	src = p.config.InventoryFile
   213  	dst = filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(src)))
   214  	if err := p.uploadFile(ui, comm, dst, src); err != nil {
   215  		return fmt.Errorf("Error uploading inventory file: %s", err)
   216  	}
   217  
   218  	if len(p.config.GroupVars) > 0 {
   219  		ui.Message("Uploading group_vars directory...")
   220  		src := p.config.GroupVars
   221  		dst := filepath.ToSlash(filepath.Join(p.config.StagingDir, "group_vars"))
   222  		if err := p.uploadDir(ui, comm, dst, src); err != nil {
   223  			return fmt.Errorf("Error uploading group_vars directory: %s", err)
   224  		}
   225  	}
   226  
   227  	if len(p.config.HostVars) > 0 {
   228  		ui.Message("Uploading host_vars directory...")
   229  		src := p.config.HostVars
   230  		dst := filepath.ToSlash(filepath.Join(p.config.StagingDir, "host_vars"))
   231  		if err := p.uploadDir(ui, comm, dst, src); err != nil {
   232  			return fmt.Errorf("Error uploading host_vars directory: %s", err)
   233  		}
   234  	}
   235  
   236  	if len(p.config.RolePaths) > 0 {
   237  		ui.Message("Uploading role directories...")
   238  		for _, src := range p.config.RolePaths {
   239  			dst := filepath.ToSlash(filepath.Join(p.config.StagingDir, "roles", filepath.Base(src)))
   240  			if err := p.uploadDir(ui, comm, dst, src); err != nil {
   241  				return fmt.Errorf("Error uploading roles: %s", err)
   242  			}
   243  		}
   244  	}
   245  
   246  	if len(p.config.PlaybookPaths) > 0 {
   247  		ui.Message("Uploading additional Playbooks...")
   248  		playbookDir := filepath.ToSlash(filepath.Join(p.config.StagingDir, "playbooks"))
   249  		if err := p.createDir(ui, comm, playbookDir); err != nil {
   250  			return fmt.Errorf("Error creating playbooks directory: %s", err)
   251  		}
   252  		for _, src := range p.config.PlaybookPaths {
   253  			dst := filepath.ToSlash(filepath.Join(playbookDir, filepath.Base(src)))
   254  			if err := p.uploadDir(ui, comm, dst, src); err != nil {
   255  				return fmt.Errorf("Error uploading playbooks: %s", err)
   256  			}
   257  		}
   258  	}
   259  
   260  	if err := p.executeAnsible(ui, comm); err != nil {
   261  		return fmt.Errorf("Error executing Ansible: %s", err)
   262  	}
   263  	return nil
   264  }
   265  
   266  func (p *Provisioner) Cancel() {
   267  	// Just hard quit. It isn't a big deal if what we're doing keeps
   268  	// running on the other side.
   269  	os.Exit(0)
   270  }
   271  
   272  func (p *Provisioner) executeGalaxy(ui packer.Ui, comm packer.Communicator) error {
   273  	rolesDir := filepath.ToSlash(filepath.Join(p.config.StagingDir, "roles"))
   274  	galaxyFile := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(p.config.GalaxyFile)))
   275  
   276  	// ansible-galaxy install -r requirements.yml -p roles/
   277  	command := fmt.Sprintf("cd %s && %s install -r %s -p %s",
   278  		p.config.StagingDir, p.config.GalaxyCommand, galaxyFile, rolesDir)
   279  	ui.Message(fmt.Sprintf("Executing Ansible Galaxy: %s", command))
   280  	cmd := &packer.RemoteCmd{
   281  		Command: command,
   282  	}
   283  	if err := cmd.StartWithUi(comm, ui); err != nil {
   284  		return err
   285  	}
   286  	if cmd.ExitStatus != 0 {
   287  		// ansible-galaxy version 2.0.0.2 doesn't return exit codes on error..
   288  		return fmt.Errorf("Non-zero exit status: %d", cmd.ExitStatus)
   289  	}
   290  	return nil
   291  }
   292  
   293  func (p *Provisioner) executeAnsible(ui packer.Ui, comm packer.Communicator) error {
   294  	playbook := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(p.config.PlaybookFile)))
   295  	inventory := filepath.ToSlash(filepath.Join(p.config.StagingDir, filepath.Base(p.config.InventoryFile)))
   296  
   297  	extraArgs := fmt.Sprintf(" --extra-vars \"packer_build_name=%s packer_builder_type=%s packer_http_addr=%s\" ",
   298  		p.config.PackerBuildName, p.config.PackerBuilderType, common.GetHTTPAddr())
   299  	if len(p.config.ExtraArguments) > 0 {
   300  		extraArgs = extraArgs + strings.Join(p.config.ExtraArguments, " ")
   301  	}
   302  
   303  	// Fetch external dependencies
   304  	if len(p.config.GalaxyFile) > 0 {
   305  		if err := p.executeGalaxy(ui, comm); err != nil {
   306  			return fmt.Errorf("Error executing Ansible Galaxy: %s", err)
   307  		}
   308  	}
   309  
   310  	command := fmt.Sprintf("cd %s && %s %s%s -c local -i %s",
   311  		p.config.StagingDir, p.config.Command, playbook, extraArgs, inventory)
   312  	ui.Message(fmt.Sprintf("Executing Ansible: %s", command))
   313  	cmd := &packer.RemoteCmd{
   314  		Command: command,
   315  	}
   316  	if err := cmd.StartWithUi(comm, ui); err != nil {
   317  		return err
   318  	}
   319  	if cmd.ExitStatus != 0 {
   320  		if cmd.ExitStatus == 127 {
   321  			return fmt.Errorf("%s could not be found. Verify that it is available on the\n"+
   322  				"PATH after connecting to the machine.",
   323  				p.config.Command)
   324  		}
   325  
   326  		return fmt.Errorf("Non-zero exit status: %d", cmd.ExitStatus)
   327  	}
   328  	return nil
   329  }
   330  
   331  func validateDirConfig(path string, config string) error {
   332  	info, err := os.Stat(path)
   333  	if err != nil {
   334  		return fmt.Errorf("%s: %s is invalid: %s", config, path, err)
   335  	} else if !info.IsDir() {
   336  		return fmt.Errorf("%s: %s must point to a directory", config, path)
   337  	}
   338  	return nil
   339  }
   340  
   341  func validateFileConfig(name string, config string, req bool) error {
   342  	if req {
   343  		if name == "" {
   344  			return fmt.Errorf("%s must be specified.", config)
   345  		}
   346  	}
   347  	info, err := os.Stat(name)
   348  	if err != nil {
   349  		return fmt.Errorf("%s: %s is invalid: %s", config, name, err)
   350  	} else if info.IsDir() {
   351  		return fmt.Errorf("%s: %s must point to a file", config, name)
   352  	}
   353  	return nil
   354  }
   355  
   356  func (p *Provisioner) uploadFile(ui packer.Ui, comm packer.Communicator, dst, src string) error {
   357  	f, err := os.Open(src)
   358  	if err != nil {
   359  		return fmt.Errorf("Error opening: %s", err)
   360  	}
   361  	defer f.Close()
   362  
   363  	if err = comm.Upload(dst, f, nil); err != nil {
   364  		return fmt.Errorf("Error uploading %s: %s", src, err)
   365  	}
   366  	return nil
   367  }
   368  
   369  func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error {
   370  	ui.Message(fmt.Sprintf("Creating directory: %s", dir))
   371  	cmd := &packer.RemoteCmd{
   372  		Command: fmt.Sprintf("mkdir -p '%s'", dir),
   373  	}
   374  	if err := cmd.StartWithUi(comm, ui); err != nil {
   375  		return err
   376  	}
   377  	if cmd.ExitStatus != 0 {
   378  		return fmt.Errorf("Non-zero exit status.")
   379  	}
   380  	return nil
   381  }
   382  
   383  func (p *Provisioner) uploadDir(ui packer.Ui, comm packer.Communicator, dst, src string) error {
   384  	if err := p.createDir(ui, comm, dst); err != nil {
   385  		return err
   386  	}
   387  
   388  	// Make sure there is a trailing "/" so that the directory isn't
   389  	// created on the other side.
   390  	if src[len(src)-1] != '/' {
   391  		src = src + "/"
   392  	}
   393  	return comm.UploadDir(dst, src, nil)
   394  }