github.com/mmcquillan/packer@v1.1.1-0.20171009221028-c85cf0483a5d/provisioner/puppet-masterless/provisioner.go (about)

     1  // Package puppetmasterless implements a provisioner for Packer that executes
     2  // Puppet on the remote machine, configured to apply a local manifest
     3  // versus connecting to a Puppet master.
     4  package puppetmasterless
     5  
     6  import (
     7  	"fmt"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/hashicorp/packer/common"
    13  	"github.com/hashicorp/packer/helper/config"
    14  	"github.com/hashicorp/packer/packer"
    15  	"github.com/hashicorp/packer/provisioner"
    16  	"github.com/hashicorp/packer/template/interpolate"
    17  )
    18  
    19  type Config struct {
    20  	common.PackerConfig `mapstructure:",squash"`
    21  	ctx                 interpolate.Context
    22  
    23  	// The command used to execute Puppet.
    24  	ExecuteCommand string `mapstructure:"execute_command"`
    25  
    26  	// Additional arguments to pass when executing Puppet
    27  	ExtraArguments []string `mapstructure:"extra_arguments"`
    28  
    29  	// Additional facts to set when executing Puppet
    30  	Facter map[string]string
    31  
    32  	// Path to a hiera configuration file to upload and use.
    33  	HieraConfigPath string `mapstructure:"hiera_config_path"`
    34  
    35  	// An array of local paths of modules to upload.
    36  	ModulePaths []string `mapstructure:"module_paths"`
    37  
    38  	// The main manifest file to apply to kick off the entire thing.
    39  	ManifestFile string `mapstructure:"manifest_file"`
    40  
    41  	// A directory of manifest files that will be uploaded to the remote
    42  	// machine.
    43  	ManifestDir string `mapstructure:"manifest_dir"`
    44  
    45  	// If true, `sudo` will NOT be used to execute Puppet.
    46  	PreventSudo bool `mapstructure:"prevent_sudo"`
    47  
    48  	// The directory where files will be uploaded. Packer requires write
    49  	// permissions in this directory.
    50  	StagingDir string `mapstructure:"staging_directory"`
    51  
    52  	// If true, staging directory is removed after executing puppet.
    53  	CleanStagingDir bool `mapstructure:"clean_staging_directory"`
    54  
    55  	// The directory from which the command will be executed.
    56  	// Packer requires the directory to exist when running puppet.
    57  	WorkingDir string `mapstructure:"working_directory"`
    58  
    59  	// The directory that contains the puppet binary.
    60  	// E.g. if it can't be found on the standard path.
    61  	PuppetBinDir string `mapstructure:"puppet_bin_dir"`
    62  
    63  	// If true, packer will ignore all exit-codes from a puppet run
    64  	IgnoreExitCodes bool `mapstructure:"ignore_exit_codes"`
    65  
    66  	// The Guest OS Type (unix or windows)
    67  	GuestOSType string `mapstructure:"guest_os_type"`
    68  }
    69  
    70  type guestOSTypeConfig struct {
    71  	stagingDir       string
    72  	executeCommand   string
    73  	facterVarsFmt    string
    74  	facterVarsJoiner string
    75  	modulePathJoiner string
    76  }
    77  
    78  var guestOSTypeConfigs = map[string]guestOSTypeConfig{
    79  	provisioner.UnixOSType: {
    80  		stagingDir: "/tmp/packer-puppet-masterless",
    81  		executeCommand: "cd {{.WorkingDir}} && " +
    82  			`{{if ne .FacterVars ""}}{{.FacterVars}} {{end}}` +
    83  			"{{if .Sudo}}sudo -E {{end}}" +
    84  			`{{if ne .PuppetBinDir ""}}{{.PuppetBinDir}}/{{end}}` +
    85  			`puppet apply --verbose --modulepath='{{.ModulePath}}' ` +
    86  			`{{if ne .HieraConfigPath ""}}--hiera_config='{{.HieraConfigPath}}' {{end}}` +
    87  			`{{if ne .ManifestDir ""}}--manifestdir='{{.ManifestDir}}' {{end}}` +
    88  			"--detailed-exitcodes " +
    89  			`{{if ne .ExtraArguments ""}}{{.ExtraArguments}} {{end}}` +
    90  			"{{.ManifestFile}}",
    91  		facterVarsFmt:    "FACTER_%s='%s'",
    92  		facterVarsJoiner: " ",
    93  		modulePathJoiner: ":",
    94  	},
    95  	provisioner.WindowsOSType: {
    96  		stagingDir: "C:/Windows/Temp/packer-puppet-masterless",
    97  		executeCommand: "cd {{.WorkingDir}} && " +
    98  			"{{.FacterVars}} && " +
    99  			`{{if ne .PuppetBinDir ""}}{{.PuppetBinDir}}/{{end}}` +
   100  			`puppet apply --verbose --modulepath='{{.ModulePath}}' ` +
   101  			`{{if ne .HieraConfigPath ""}}--hiera_config='{{.HieraConfigPath}}' {{end}}` +
   102  			`{{if ne .ManifestDir ""}}--manifestdir='{{.ManifestDir}}' {{end}}` +
   103  			"--detailed-exitcodes " +
   104  			`{{if ne .ExtraArguments ""}}{{.ExtraArguments}} {{end}}` +
   105  			"{{.ManifestFile}}",
   106  		facterVarsFmt:    `SET "FACTER_%s=%s"`,
   107  		facterVarsJoiner: " & ",
   108  		modulePathJoiner: ";",
   109  	},
   110  }
   111  
   112  type Provisioner struct {
   113  	config            Config
   114  	guestOSTypeConfig guestOSTypeConfig
   115  	guestCommands     *provisioner.GuestCommands
   116  }
   117  
   118  type ExecuteTemplate struct {
   119  	WorkingDir      string
   120  	FacterVars      string
   121  	HieraConfigPath string
   122  	ModulePath      string
   123  	ManifestFile    string
   124  	ManifestDir     string
   125  	PuppetBinDir    string
   126  	Sudo            bool
   127  	ExtraArguments  string
   128  }
   129  
   130  func (p *Provisioner) Prepare(raws ...interface{}) error {
   131  	err := config.Decode(&p.config, &config.DecodeOpts{
   132  		Interpolate:        true,
   133  		InterpolateContext: &p.config.ctx,
   134  		InterpolateFilter: &interpolate.RenderFilter{
   135  			Exclude: []string{
   136  				"execute_command",
   137  			},
   138  		},
   139  	}, raws...)
   140  	if err != nil {
   141  		return err
   142  	}
   143  
   144  	// Set some defaults
   145  	if p.config.GuestOSType == "" {
   146  		p.config.GuestOSType = provisioner.DefaultOSType
   147  	}
   148  	p.config.GuestOSType = strings.ToLower(p.config.GuestOSType)
   149  
   150  	var ok bool
   151  	p.guestOSTypeConfig, ok = guestOSTypeConfigs[p.config.GuestOSType]
   152  	if !ok {
   153  		return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType)
   154  	}
   155  
   156  	p.guestCommands, err = provisioner.NewGuestCommands(p.config.GuestOSType, !p.config.PreventSudo)
   157  	if err != nil {
   158  		return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType)
   159  	}
   160  
   161  	if p.config.ExecuteCommand == "" {
   162  		p.config.ExecuteCommand = p.guestOSTypeConfig.executeCommand
   163  	}
   164  
   165  	if p.config.ExecuteCommand == "" {
   166  		p.config.ExecuteCommand = p.guestOSTypeConfig.executeCommand
   167  	}
   168  
   169  	if p.config.StagingDir == "" {
   170  		p.config.StagingDir = p.guestOSTypeConfig.stagingDir
   171  	}
   172  
   173  	if p.config.WorkingDir == "" {
   174  		p.config.WorkingDir = p.config.StagingDir
   175  	}
   176  
   177  	if p.config.Facter == nil {
   178  		p.config.Facter = make(map[string]string)
   179  	}
   180  	p.config.Facter["packer_build_name"] = p.config.PackerBuildName
   181  	p.config.Facter["packer_builder_type"] = p.config.PackerBuilderType
   182  
   183  	// Validation
   184  	var errs *packer.MultiError
   185  	if p.config.HieraConfigPath != "" {
   186  		info, err := os.Stat(p.config.HieraConfigPath)
   187  		if err != nil {
   188  			errs = packer.MultiErrorAppend(errs,
   189  				fmt.Errorf("hiera_config_path is invalid: %s", err))
   190  		} else if info.IsDir() {
   191  			errs = packer.MultiErrorAppend(errs,
   192  				fmt.Errorf("hiera_config_path must point to a file"))
   193  		}
   194  	}
   195  
   196  	if p.config.ManifestDir != "" {
   197  		info, err := os.Stat(p.config.ManifestDir)
   198  		if err != nil {
   199  			errs = packer.MultiErrorAppend(errs,
   200  				fmt.Errorf("manifest_dir is invalid: %s", err))
   201  		} else if !info.IsDir() {
   202  			errs = packer.MultiErrorAppend(errs,
   203  				fmt.Errorf("manifest_dir must point to a directory"))
   204  		}
   205  	}
   206  
   207  	if p.config.ManifestFile == "" {
   208  		errs = packer.MultiErrorAppend(errs,
   209  			fmt.Errorf("A manifest_file must be specified."))
   210  	} else {
   211  		_, err := os.Stat(p.config.ManifestFile)
   212  		if err != nil {
   213  			errs = packer.MultiErrorAppend(errs,
   214  				fmt.Errorf("manifest_file is invalid: %s", err))
   215  		}
   216  	}
   217  
   218  	for i, path := range p.config.ModulePaths {
   219  		info, err := os.Stat(path)
   220  		if err != nil {
   221  			errs = packer.MultiErrorAppend(errs,
   222  				fmt.Errorf("module_path[%d] is invalid: %s", i, err))
   223  		} else if !info.IsDir() {
   224  			errs = packer.MultiErrorAppend(errs,
   225  				fmt.Errorf("module_path[%d] must point to a directory", i))
   226  		}
   227  	}
   228  
   229  	if errs != nil && len(errs.Errors) > 0 {
   230  		return errs
   231  	}
   232  
   233  	return nil
   234  }
   235  
   236  func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
   237  	ui.Say("Provisioning with Puppet...")
   238  	ui.Message("Creating Puppet staging directory...")
   239  	if err := p.createDir(ui, comm, p.config.StagingDir); err != nil {
   240  		return fmt.Errorf("Error creating staging directory: %s", err)
   241  	}
   242  
   243  	// Upload hiera config if set
   244  	remoteHieraConfigPath := ""
   245  	if p.config.HieraConfigPath != "" {
   246  		var err error
   247  		remoteHieraConfigPath, err = p.uploadHieraConfig(ui, comm)
   248  		if err != nil {
   249  			return fmt.Errorf("Error uploading hiera config: %s", err)
   250  		}
   251  	}
   252  
   253  	// Upload manifest dir if set
   254  	remoteManifestDir := ""
   255  	if p.config.ManifestDir != "" {
   256  		ui.Message(fmt.Sprintf(
   257  			"Uploading manifest directory from: %s", p.config.ManifestDir))
   258  		remoteManifestDir = fmt.Sprintf("%s/manifests", p.config.StagingDir)
   259  		err := p.uploadDirectory(ui, comm, remoteManifestDir, p.config.ManifestDir)
   260  		if err != nil {
   261  			return fmt.Errorf("Error uploading manifest dir: %s", err)
   262  		}
   263  	}
   264  
   265  	// Upload all modules
   266  	modulePaths := make([]string, 0, len(p.config.ModulePaths))
   267  	for i, path := range p.config.ModulePaths {
   268  		ui.Message(fmt.Sprintf("Uploading local modules from: %s", path))
   269  		targetPath := fmt.Sprintf("%s/module-%d", p.config.StagingDir, i)
   270  		if err := p.uploadDirectory(ui, comm, targetPath, path); err != nil {
   271  			return fmt.Errorf("Error uploading modules: %s", err)
   272  		}
   273  
   274  		modulePaths = append(modulePaths, targetPath)
   275  	}
   276  
   277  	// Upload manifests
   278  	remoteManifestFile, err := p.uploadManifests(ui, comm)
   279  	if err != nil {
   280  		return fmt.Errorf("Error uploading manifests: %s", err)
   281  	}
   282  
   283  	// Compile the facter variables
   284  	facterVars := make([]string, 0, len(p.config.Facter))
   285  	for k, v := range p.config.Facter {
   286  		facterVars = append(facterVars, fmt.Sprintf(p.guestOSTypeConfig.facterVarsFmt, k, v))
   287  	}
   288  
   289  	// Execute Puppet
   290  	p.config.ctx.Data = &ExecuteTemplate{
   291  		FacterVars:      strings.Join(facterVars, p.guestOSTypeConfig.facterVarsJoiner),
   292  		HieraConfigPath: remoteHieraConfigPath,
   293  		ManifestDir:     remoteManifestDir,
   294  		ManifestFile:    remoteManifestFile,
   295  		ModulePath:      strings.Join(modulePaths, p.guestOSTypeConfig.modulePathJoiner),
   296  		PuppetBinDir:    p.config.PuppetBinDir,
   297  		Sudo:            !p.config.PreventSudo,
   298  		WorkingDir:      p.config.WorkingDir,
   299  		ExtraArguments:  strings.Join(p.config.ExtraArguments, " "),
   300  	}
   301  	command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx)
   302  	if err != nil {
   303  		return err
   304  	}
   305  
   306  	cmd := &packer.RemoteCmd{
   307  		Command: command,
   308  	}
   309  
   310  	ui.Message(fmt.Sprintf("Running Puppet: %s", command))
   311  	if err := cmd.StartWithUi(comm, ui); err != nil {
   312  		return fmt.Errorf("Got an error starting command: %s", err)
   313  	}
   314  
   315  	if cmd.ExitStatus != 0 && cmd.ExitStatus != 2 && !p.config.IgnoreExitCodes {
   316  		return fmt.Errorf("Puppet exited with a non-zero exit status: %d", cmd.ExitStatus)
   317  	}
   318  
   319  	if p.config.CleanStagingDir {
   320  		if err := p.removeDir(ui, comm, p.config.StagingDir); err != nil {
   321  			return fmt.Errorf("Error removing staging directory: %s", err)
   322  		}
   323  	}
   324  
   325  	return nil
   326  }
   327  
   328  func (p *Provisioner) Cancel() {
   329  	// Just hard quit. It isn't a big deal if what we're doing keeps
   330  	// running on the other side.
   331  	os.Exit(0)
   332  }
   333  
   334  func (p *Provisioner) uploadHieraConfig(ui packer.Ui, comm packer.Communicator) (string, error) {
   335  	ui.Message("Uploading hiera configuration...")
   336  	f, err := os.Open(p.config.HieraConfigPath)
   337  	if err != nil {
   338  		return "", err
   339  	}
   340  	defer f.Close()
   341  
   342  	path := fmt.Sprintf("%s/hiera.yaml", p.config.StagingDir)
   343  	if err := comm.Upload(path, f, nil); err != nil {
   344  		return "", err
   345  	}
   346  
   347  	return path, nil
   348  }
   349  
   350  func (p *Provisioner) uploadManifests(ui packer.Ui, comm packer.Communicator) (string, error) {
   351  	// Create the remote manifests directory...
   352  	ui.Message("Uploading manifests...")
   353  	remoteManifestsPath := fmt.Sprintf("%s/manifests", p.config.StagingDir)
   354  	if err := p.createDir(ui, comm, remoteManifestsPath); err != nil {
   355  		return "", fmt.Errorf("Error creating manifests directory: %s", err)
   356  	}
   357  
   358  	// NOTE! manifest_file may either be a directory or a file, as puppet apply
   359  	// now accepts either one.
   360  
   361  	fi, err := os.Stat(p.config.ManifestFile)
   362  	if err != nil {
   363  		return "", fmt.Errorf("Error inspecting manifest file: %s", err)
   364  	}
   365  
   366  	if fi.IsDir() {
   367  		// If manifest_file is a directory we'll upload the whole thing
   368  		ui.Message(fmt.Sprintf(
   369  			"Uploading manifest directory from: %s", p.config.ManifestFile))
   370  
   371  		remoteManifestDir := fmt.Sprintf("%s/manifests", p.config.StagingDir)
   372  		err := p.uploadDirectory(ui, comm, remoteManifestDir, p.config.ManifestFile)
   373  		if err != nil {
   374  			return "", fmt.Errorf("Error uploading manifest dir: %s", err)
   375  		}
   376  		return remoteManifestDir, nil
   377  	}
   378  	// Otherwise manifest_file is a file and we'll upload it
   379  	ui.Message(fmt.Sprintf(
   380  		"Uploading manifest file from: %s", p.config.ManifestFile))
   381  
   382  	f, err := os.Open(p.config.ManifestFile)
   383  	if err != nil {
   384  		return "", err
   385  	}
   386  	defer f.Close()
   387  
   388  	manifestFilename := filepath.Base(p.config.ManifestFile)
   389  	remoteManifestFile := fmt.Sprintf("%s/%s", remoteManifestsPath, manifestFilename)
   390  	if err := comm.Upload(remoteManifestFile, f, nil); err != nil {
   391  		return "", err
   392  	}
   393  	return remoteManifestFile, nil
   394  }
   395  
   396  func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error {
   397  	ui.Message(fmt.Sprintf("Creating directory: %s", dir))
   398  
   399  	cmd := &packer.RemoteCmd{Command: p.guestCommands.CreateDir(dir)}
   400  
   401  	if err := cmd.StartWithUi(comm, ui); err != nil {
   402  		return err
   403  	}
   404  
   405  	if cmd.ExitStatus != 0 {
   406  		return fmt.Errorf("Non-zero exit status.")
   407  	}
   408  
   409  	// Chmod the directory to 0777 just so that we can access it as our user
   410  	cmd = &packer.RemoteCmd{Command: p.guestCommands.Chmod(dir, "0777")}
   411  	if err := cmd.StartWithUi(comm, ui); err != nil {
   412  		return err
   413  	}
   414  	if cmd.ExitStatus != 0 {
   415  		return fmt.Errorf("Non-zero exit status. See output above for more info.")
   416  	}
   417  
   418  	return nil
   419  }
   420  
   421  func (p *Provisioner) removeDir(ui packer.Ui, comm packer.Communicator, dir string) error {
   422  	cmd := &packer.RemoteCmd{
   423  		Command: fmt.Sprintf("rm -fr '%s'", dir),
   424  	}
   425  
   426  	if err := cmd.StartWithUi(comm, ui); err != nil {
   427  		return err
   428  	}
   429  
   430  	if cmd.ExitStatus != 0 {
   431  		return fmt.Errorf("Non-zero exit status.")
   432  	}
   433  
   434  	return nil
   435  }
   436  
   437  func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, dst string, src string) error {
   438  	if err := p.createDir(ui, comm, dst); err != nil {
   439  		return err
   440  	}
   441  
   442  	// Make sure there is a trailing "/" so that the directory isn't
   443  	// created on the other side.
   444  	if src[len(src)-1] != '/' {
   445  		src = src + "/"
   446  	}
   447  
   448  	return comm.UploadDir(dst, src, nil)
   449  }