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