gopkg.in/hashicorp/packer.v1@v1.3.2/provisioner/puppet-server/provisioner.go (about)

     1  // Package puppetserver implements a provisioner for Packer that executes
     2  // Puppet on the remote machine connecting to a Puppet master.
     3  package puppetserver
     4  
     5  import (
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  
    11  	"github.com/hashicorp/packer/common"
    12  	"github.com/hashicorp/packer/helper/config"
    13  	"github.com/hashicorp/packer/packer"
    14  	"github.com/hashicorp/packer/provisioner"
    15  	"github.com/hashicorp/packer/template/interpolate"
    16  )
    17  
    18  type Config struct {
    19  	common.PackerConfig `mapstructure:",squash"`
    20  	ctx                 interpolate.Context
    21  
    22  	// If true, staging directory is removed after executing puppet.
    23  	CleanStagingDir bool `mapstructure:"clean_staging_directory"`
    24  
    25  	// A path to the client certificate
    26  	ClientCertPath string `mapstructure:"client_cert_path"`
    27  
    28  	// A path to a directory containing the client private keys
    29  	ClientPrivateKeyPath string `mapstructure:"client_private_key_path"`
    30  
    31  	// The command used to execute Puppet.
    32  	ExecuteCommand string `mapstructure:"execute_command"`
    33  
    34  	// Additional argument to pass when executing Puppet.
    35  	ExtraArguments []string `mapstructure:"extra_arguments"`
    36  
    37  	// Additional facts to set when executing Puppet
    38  	Facter map[string]string
    39  
    40  	// The Guest OS Type (unix or windows)
    41  	GuestOSType string `mapstructure:"guest_os_type"`
    42  
    43  	// If true, packer will ignore all exit-codes from a puppet run
    44  	IgnoreExitCodes bool `mapstructure:"ignore_exit_codes"`
    45  
    46  	// If true, `sudo` will NOT be used to execute Puppet.
    47  	PreventSudo bool `mapstructure:"prevent_sudo"`
    48  
    49  	// The directory that contains the puppet binary.
    50  	// E.g. if it can't be found on the standard path.
    51  	PuppetBinDir string `mapstructure:"puppet_bin_dir"`
    52  
    53  	// The hostname of the Puppet node.
    54  	PuppetNode string `mapstructure:"puppet_node"`
    55  
    56  	// The hostname of the Puppet server.
    57  	PuppetServer string `mapstructure:"puppet_server"`
    58  
    59  	// The directory where files will be uploaded. Packer requires write
    60  	// permissions in this directory.
    61  	StagingDir string `mapstructure:"staging_dir"`
    62  
    63  	// The directory from which the command will be executed.
    64  	// Packer requires the directory to exist when running puppet.
    65  	WorkingDir string `mapstructure:"working_directory"`
    66  }
    67  
    68  type guestOSTypeConfig struct {
    69  	executeCommand   string
    70  	facterVarsFmt    string
    71  	facterVarsJoiner string
    72  	stagingDir       string
    73  	tempDir          string
    74  }
    75  
    76  // FIXME assumes both Packer host and target are same OS
    77  var guestOSTypeConfigs = map[string]guestOSTypeConfig{
    78  	provisioner.UnixOSType: {
    79  		tempDir:    "/tmp",
    80  		stagingDir: "/tmp/packer-puppet-server",
    81  		executeCommand: "cd {{.WorkingDir}} && " +
    82  			`{{if ne .FacterVars ""}}{{.FacterVars}} {{end}}` +
    83  			"{{if .Sudo}}sudo -E {{end}}" +
    84  			`{{if ne .PuppetBinDir ""}}{{.PuppetBinDir}}/{{end}}` +
    85  			"puppet agent --onetime --no-daemonize --detailed-exitcodes " +
    86  			"{{if .Debug}}--debug {{end}}" +
    87  			`{{if ne .PuppetServer ""}}--server='{{.PuppetServer}}' {{end}}` +
    88  			`{{if ne .PuppetNode ""}}--certname='{{.PuppetNode}}' {{end}}` +
    89  			`{{if ne .ClientCertPath ""}}--certdir='{{.ClientCertPath}}' {{end}}` +
    90  			`{{if ne .ClientPrivateKeyPath ""}}--privatekeydir='{{.ClientPrivateKeyPath}}' {{end}}` +
    91  			`{{if ne .ExtraArguments ""}}{{.ExtraArguments}} {{end}}`,
    92  		facterVarsFmt:    "FACTER_%s='%s'",
    93  		facterVarsJoiner: " ",
    94  	},
    95  	provisioner.WindowsOSType: {
    96  		tempDir:    filepath.ToSlash(os.Getenv("TEMP")),
    97  		stagingDir: filepath.ToSlash(os.Getenv("SYSTEMROOT")) + "/Temp/packer-puppet-server",
    98  		executeCommand: "cd {{.WorkingDir}} && " +
    99  			`{{if ne .FacterVars ""}}{{.FacterVars}} && {{end}}` +
   100  			`{{if ne .PuppetBinDir ""}}{{.PuppetBinDir}}/{{end}}` +
   101  			"puppet agent --onetime --no-daemonize --detailed-exitcodes " +
   102  			"{{if .Debug}}--debug {{end}}" +
   103  			`{{if ne .PuppetServer ""}}--server='{{.PuppetServer}}' {{end}}` +
   104  			`{{if ne .PuppetNode ""}}--certname='{{.PuppetNode}}' {{end}}` +
   105  			`{{if ne .ClientCertPath ""}}--certdir='{{.ClientCertPath}}' {{end}}` +
   106  			`{{if ne .ClientPrivateKeyPath ""}}--privatekeydir='{{.ClientPrivateKeyPath}}' {{end}}` +
   107  			`{{if ne .ExtraArguments ""}}{{.ExtraArguments}} {{end}}`,
   108  		facterVarsFmt:    `SET "FACTER_%s=%s"`,
   109  		facterVarsJoiner: " & ",
   110  	},
   111  }
   112  
   113  type Provisioner struct {
   114  	config            Config
   115  	guestOSTypeConfig guestOSTypeConfig
   116  	guestCommands     *provisioner.GuestCommands
   117  }
   118  
   119  type ExecuteTemplate struct {
   120  	ClientCertPath       string
   121  	ClientPrivateKeyPath string
   122  	Debug                bool
   123  	ExtraArguments       string
   124  	FacterVars           string
   125  	PuppetNode           string
   126  	PuppetServer         string
   127  	PuppetBinDir         string
   128  	Sudo                 bool
   129  	WorkingDir           string
   130  }
   131  
   132  func (p *Provisioner) Prepare(raws ...interface{}) error {
   133  	err := config.Decode(&p.config, &config.DecodeOpts{
   134  		Interpolate:        true,
   135  		InterpolateContext: &p.config.ctx,
   136  		InterpolateFilter: &interpolate.RenderFilter{
   137  			Exclude: []string{
   138  				"execute_command",
   139  				"extra_arguments",
   140  			},
   141  		},
   142  	}, raws...)
   143  	if err != nil {
   144  		return err
   145  	}
   146  
   147  	if p.config.GuestOSType == "" {
   148  		p.config.GuestOSType = provisioner.DefaultOSType
   149  	}
   150  	p.config.GuestOSType = strings.ToLower(p.config.GuestOSType)
   151  
   152  	var ok bool
   153  	p.guestOSTypeConfig, ok = guestOSTypeConfigs[p.config.GuestOSType]
   154  	if !ok {
   155  		return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType)
   156  	}
   157  
   158  	p.guestCommands, err = provisioner.NewGuestCommands(p.config.GuestOSType, !p.config.PreventSudo)
   159  	if err != nil {
   160  		return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType)
   161  	}
   162  
   163  	if p.config.ExecuteCommand == "" {
   164  		p.config.ExecuteCommand = p.guestOSTypeConfig.executeCommand
   165  	}
   166  
   167  	if p.config.StagingDir == "" {
   168  		p.config.StagingDir = p.guestOSTypeConfig.stagingDir
   169  	}
   170  
   171  	if p.config.WorkingDir == "" {
   172  		p.config.WorkingDir = p.config.StagingDir
   173  	}
   174  
   175  	if p.config.Facter == nil {
   176  		p.config.Facter = make(map[string]string)
   177  	}
   178  	p.config.Facter["packer_build_name"] = p.config.PackerBuildName
   179  	p.config.Facter["packer_builder_type"] = p.config.PackerBuilderType
   180  
   181  	var errs *packer.MultiError
   182  	if p.config.ClientCertPath != "" {
   183  		info, err := os.Stat(p.config.ClientCertPath)
   184  		if err != nil {
   185  			errs = packer.MultiErrorAppend(errs,
   186  				fmt.Errorf("client_cert_dir is invalid: %s", err))
   187  		} else if !info.IsDir() {
   188  			errs = packer.MultiErrorAppend(errs,
   189  				fmt.Errorf("client_cert_dir must point to a directory"))
   190  		}
   191  	}
   192  
   193  	if p.config.ClientPrivateKeyPath != "" {
   194  		info, err := os.Stat(p.config.ClientPrivateKeyPath)
   195  		if err != nil {
   196  			errs = packer.MultiErrorAppend(errs,
   197  				fmt.Errorf("client_private_key_dir is invalid: %s", err))
   198  		} else if !info.IsDir() {
   199  			errs = packer.MultiErrorAppend(errs,
   200  				fmt.Errorf("client_private_key_dir must point to a directory"))
   201  		}
   202  	}
   203  
   204  	if errs != nil && len(errs.Errors) > 0 {
   205  		return errs
   206  	}
   207  
   208  	return nil
   209  }
   210  
   211  func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
   212  	ui.Say("Provisioning with Puppet...")
   213  	ui.Message("Creating Puppet staging directory...")
   214  	if err := p.createDir(ui, comm, p.config.StagingDir); err != nil {
   215  		return fmt.Errorf("Error creating staging directory: %s", err)
   216  	}
   217  
   218  	// Upload client cert dir if set
   219  	remoteClientCertPath := ""
   220  	if p.config.ClientCertPath != "" {
   221  		ui.Message(fmt.Sprintf(
   222  			"Uploading client cert from: %s", p.config.ClientCertPath))
   223  		remoteClientCertPath = fmt.Sprintf("%s/certs", p.config.StagingDir)
   224  		err := p.uploadDirectory(ui, comm, remoteClientCertPath, p.config.ClientCertPath)
   225  		if err != nil {
   226  			return fmt.Errorf("Error uploading client cert: %s", err)
   227  		}
   228  	}
   229  
   230  	// Upload client cert dir if set
   231  	remoteClientPrivateKeyPath := ""
   232  	if p.config.ClientPrivateKeyPath != "" {
   233  		ui.Message(fmt.Sprintf(
   234  			"Uploading client private keys from: %s", p.config.ClientPrivateKeyPath))
   235  		remoteClientPrivateKeyPath = fmt.Sprintf("%s/private_keys", p.config.StagingDir)
   236  		err := p.uploadDirectory(ui, comm, remoteClientPrivateKeyPath, p.config.ClientPrivateKeyPath)
   237  		if err != nil {
   238  			return fmt.Errorf("Error uploading client private keys: %s", err)
   239  		}
   240  	}
   241  
   242  	// Compile the facter variables
   243  	facterVars := make([]string, 0, len(p.config.Facter))
   244  	for k, v := range p.config.Facter {
   245  		facterVars = append(facterVars, fmt.Sprintf(p.guestOSTypeConfig.facterVarsFmt, k, v))
   246  	}
   247  
   248  	data := ExecuteTemplate{
   249  		ClientCertPath:       remoteClientCertPath,
   250  		ClientPrivateKeyPath: remoteClientPrivateKeyPath,
   251  		ExtraArguments:       "",
   252  		FacterVars:           strings.Join(facterVars, p.guestOSTypeConfig.facterVarsJoiner),
   253  		PuppetNode:           p.config.PuppetNode,
   254  		PuppetServer:         p.config.PuppetServer,
   255  		PuppetBinDir:         p.config.PuppetBinDir,
   256  		Sudo:                 !p.config.PreventSudo,
   257  		WorkingDir:           p.config.WorkingDir,
   258  	}
   259  
   260  	p.config.ctx.Data = &data
   261  	_ExtraArguments, err := interpolate.Render(strings.Join(p.config.ExtraArguments, " "), &p.config.ctx)
   262  	if err != nil {
   263  		return err
   264  	}
   265  	data.ExtraArguments = _ExtraArguments
   266  
   267  	command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx)
   268  	if err != nil {
   269  		return err
   270  	}
   271  
   272  	cmd := &packer.RemoteCmd{
   273  		Command: command,
   274  	}
   275  
   276  	ui.Message(fmt.Sprintf("Running Puppet: %s", command))
   277  	if err := cmd.StartWithUi(comm, ui); err != nil {
   278  		return err
   279  	}
   280  
   281  	if cmd.ExitStatus != 0 && cmd.ExitStatus != 2 && !p.config.IgnoreExitCodes {
   282  		return fmt.Errorf("Puppet exited with a non-zero exit status: %d", cmd.ExitStatus)
   283  	}
   284  
   285  	if p.config.CleanStagingDir {
   286  		if err := p.removeDir(ui, comm, p.config.StagingDir); err != nil {
   287  			return fmt.Errorf("Error removing staging directory: %s", err)
   288  		}
   289  	}
   290  
   291  	return nil
   292  }
   293  
   294  func (p *Provisioner) Cancel() {
   295  	// Just hard quit. It isn't a big deal if what we're doing keeps
   296  	// running on the other side.
   297  	os.Exit(0)
   298  }
   299  
   300  func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error {
   301  	ui.Message(fmt.Sprintf("Creating directory: %s", dir))
   302  
   303  	cmd := &packer.RemoteCmd{Command: p.guestCommands.CreateDir(dir)}
   304  	if err := cmd.StartWithUi(comm, ui); err != nil {
   305  		return err
   306  	}
   307  	if cmd.ExitStatus != 0 {
   308  		return fmt.Errorf("Non-zero exit status. See output above for more info.")
   309  	}
   310  
   311  	// Chmod the directory to 0777 just so that we can access it as our user
   312  	cmd = &packer.RemoteCmd{Command: p.guestCommands.Chmod(dir, "0777")}
   313  	if err := cmd.StartWithUi(comm, ui); err != nil {
   314  		return err
   315  	}
   316  	if cmd.ExitStatus != 0 {
   317  		return fmt.Errorf("Non-zero exit status. See output above for more info.")
   318  	}
   319  
   320  	return nil
   321  }
   322  
   323  func (p *Provisioner) removeDir(ui packer.Ui, comm packer.Communicator, dir string) error {
   324  	cmd := &packer.RemoteCmd{
   325  		Command: fmt.Sprintf("rm -fr '%s'", dir),
   326  	}
   327  
   328  	if err := cmd.StartWithUi(comm, ui); err != nil {
   329  		return err
   330  	}
   331  
   332  	if cmd.ExitStatus != 0 {
   333  		return fmt.Errorf("Non-zero exit status.")
   334  	}
   335  
   336  	return nil
   337  }
   338  
   339  func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, dst string, src string) error {
   340  	if err := p.createDir(ui, comm, dst); err != nil {
   341  		return err
   342  	}
   343  
   344  	// Make sure there is a trailing "/" so that the directory isn't
   345  	// created on the other side.
   346  	if src[len(src)-1] != '/' {
   347  		src = src + "/"
   348  	}
   349  
   350  	return comm.UploadDir(dst, src, nil)
   351  }