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