github.phpd.cn/hashicorp/packer@v1.3.2/provisioner/salt-masterless/provisioner.go (about)

     1  // This package implements a provisioner for Packer that executes a
     2  // saltstack state within the remote machine
     3  package saltmasterless
     4  
     5  import (
     6  	"bytes"
     7  	"errors"
     8  	"fmt"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  
    13  	"github.com/hashicorp/packer/common"
    14  	"github.com/hashicorp/packer/helper/config"
    15  	"github.com/hashicorp/packer/packer"
    16  	"github.com/hashicorp/packer/provisioner"
    17  	"github.com/hashicorp/packer/template/interpolate"
    18  )
    19  
    20  type Config struct {
    21  	common.PackerConfig `mapstructure:",squash"`
    22  
    23  	// If true, run the salt-bootstrap script
    24  	SkipBootstrap bool   `mapstructure:"skip_bootstrap"`
    25  	BootstrapArgs string `mapstructure:"bootstrap_args"`
    26  
    27  	DisableSudo bool `mapstructure:"disable_sudo"`
    28  
    29  	// Custom state to run instead of highstate
    30  	CustomState string `mapstructure:"custom_state"`
    31  
    32  	// Local path to the minion config
    33  	MinionConfig string `mapstructure:"minion_config"`
    34  
    35  	// Local path to the minion grains
    36  	GrainsFile string `mapstructure:"grains_file"`
    37  
    38  	// Local path to the salt state tree
    39  	LocalStateTree string `mapstructure:"local_state_tree"`
    40  
    41  	// Local path to the salt pillar roots
    42  	LocalPillarRoots string `mapstructure:"local_pillar_roots"`
    43  
    44  	// Remote path to the salt state tree
    45  	RemoteStateTree string `mapstructure:"remote_state_tree"`
    46  
    47  	// Remote path to the salt pillar roots
    48  	RemotePillarRoots string `mapstructure:"remote_pillar_roots"`
    49  
    50  	// Where files will be copied before moving to the /srv/salt directory
    51  	TempConfigDir string `mapstructure:"temp_config_dir"`
    52  
    53  	// Don't exit packer if salt-call returns an error code
    54  	NoExitOnFailure bool `mapstructure:"no_exit_on_failure"`
    55  
    56  	// Set the logging level for the salt-call run
    57  	LogLevel string `mapstructure:"log_level"`
    58  
    59  	// Arguments to pass to salt-call
    60  	SaltCallArgs string `mapstructure:"salt_call_args"`
    61  
    62  	// Directory containing salt-call
    63  	SaltBinDir string `mapstructure:"salt_bin_dir"`
    64  
    65  	// Command line args passed onto salt-call
    66  	CmdArgs string ""
    67  
    68  	// The Guest OS Type (unix or windows)
    69  	GuestOSType string `mapstructure:"guest_os_type"`
    70  
    71  	ctx interpolate.Context
    72  }
    73  
    74  type Provisioner struct {
    75  	config            Config
    76  	guestOSTypeConfig guestOSTypeConfig
    77  	guestCommands     *provisioner.GuestCommands
    78  }
    79  
    80  type guestOSTypeConfig struct {
    81  	tempDir           string
    82  	stateRoot         string
    83  	pillarRoot        string
    84  	configDir         string
    85  	bootstrapFetchCmd string
    86  	bootstrapRunCmd   string
    87  }
    88  
    89  var guestOSTypeConfigs = map[string]guestOSTypeConfig{
    90  	provisioner.UnixOSType: {
    91  		configDir:         "/etc/salt",
    92  		tempDir:           "/tmp/salt",
    93  		stateRoot:         "/srv/salt",
    94  		pillarRoot:        "/srv/pillar",
    95  		bootstrapFetchCmd: "curl -L https://bootstrap.saltstack.com -o /tmp/install_salt.sh || wget -O /tmp/install_salt.sh https://bootstrap.saltstack.com",
    96  		bootstrapRunCmd:   "sh /tmp/install_salt.sh",
    97  	},
    98  	provisioner.WindowsOSType: {
    99  		configDir:         "C:/salt/conf",
   100  		tempDir:           "C:/Windows/Temp/salt/",
   101  		stateRoot:         "C:/salt/state",
   102  		pillarRoot:        "C:/salt/pillar/",
   103  		bootstrapFetchCmd: "powershell Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/saltstack/salt-bootstrap/stable/bootstrap-salt.ps1' -OutFile 'C:/Windows/Temp/bootstrap-salt.ps1'",
   104  		bootstrapRunCmd:   "Powershell C:/Windows/Temp/bootstrap-salt.ps1",
   105  	},
   106  }
   107  
   108  func (p *Provisioner) Prepare(raws ...interface{}) error {
   109  	err := config.Decode(&p.config, &config.DecodeOpts{
   110  		Interpolate:        true,
   111  		InterpolateContext: &p.config.ctx,
   112  		InterpolateFilter: &interpolate.RenderFilter{
   113  			Exclude: []string{},
   114  		},
   115  	}, raws...)
   116  	if err != nil {
   117  		return err
   118  	}
   119  
   120  	if p.config.GuestOSType == "" {
   121  		p.config.GuestOSType = provisioner.DefaultOSType
   122  	} else {
   123  		p.config.GuestOSType = strings.ToLower(p.config.GuestOSType)
   124  	}
   125  
   126  	var ok bool
   127  	p.guestOSTypeConfig, ok = guestOSTypeConfigs[p.config.GuestOSType]
   128  	if !ok {
   129  		return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType)
   130  	}
   131  
   132  	p.guestCommands, err = provisioner.NewGuestCommands(p.config.GuestOSType, false)
   133  	if err != nil {
   134  		return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType)
   135  	}
   136  
   137  	if p.config.TempConfigDir == "" {
   138  		p.config.TempConfigDir = p.guestOSTypeConfig.tempDir
   139  	}
   140  
   141  	var errs *packer.MultiError
   142  
   143  	// require a salt state tree
   144  	err = validateDirConfig(p.config.LocalStateTree, "local_state_tree", true)
   145  	if err != nil {
   146  		errs = packer.MultiErrorAppend(errs, err)
   147  	}
   148  
   149  	err = validateDirConfig(p.config.LocalPillarRoots, "local_pillar_roots", false)
   150  	if err != nil {
   151  		errs = packer.MultiErrorAppend(errs, err)
   152  	}
   153  
   154  	err = validateFileConfig(p.config.MinionConfig, "minion_config", false)
   155  	if err != nil {
   156  		errs = packer.MultiErrorAppend(errs, err)
   157  	}
   158  
   159  	if p.config.MinionConfig != "" && (p.config.RemoteStateTree != "" || p.config.RemotePillarRoots != "") {
   160  		errs = packer.MultiErrorAppend(errs,
   161  			errors.New("remote_state_tree and remote_pillar_roots only apply when minion_config is not used"))
   162  	}
   163  
   164  	err = validateFileConfig(p.config.GrainsFile, "grains_file", false)
   165  	if err != nil {
   166  		errs = packer.MultiErrorAppend(errs, err)
   167  	}
   168  
   169  	// build the command line args to pass onto salt
   170  	var cmd_args bytes.Buffer
   171  
   172  	if p.config.CustomState == "" {
   173  		cmd_args.WriteString(" state.highstate")
   174  	} else {
   175  		cmd_args.WriteString(" state.sls ")
   176  		cmd_args.WriteString(p.config.CustomState)
   177  	}
   178  
   179  	if p.config.MinionConfig == "" {
   180  		// pass --file-root and --pillar-root if no minion_config is supplied
   181  		if p.config.RemoteStateTree != "" {
   182  			cmd_args.WriteString(" --file-root=")
   183  			cmd_args.WriteString(p.config.RemoteStateTree)
   184  		} else {
   185  			cmd_args.WriteString(" --file-root=")
   186  			cmd_args.WriteString(p.guestOSTypeConfig.stateRoot)
   187  		}
   188  		if p.config.RemotePillarRoots != "" {
   189  			cmd_args.WriteString(" --pillar-root=")
   190  			cmd_args.WriteString(p.config.RemotePillarRoots)
   191  		} else {
   192  			cmd_args.WriteString(" --pillar-root=")
   193  			cmd_args.WriteString(p.guestOSTypeConfig.pillarRoot)
   194  		}
   195  	}
   196  
   197  	if !p.config.NoExitOnFailure {
   198  		cmd_args.WriteString(" --retcode-passthrough")
   199  	}
   200  
   201  	if p.config.LogLevel == "" {
   202  		cmd_args.WriteString(" -l info")
   203  	} else {
   204  		cmd_args.WriteString(" -l ")
   205  		cmd_args.WriteString(p.config.LogLevel)
   206  	}
   207  
   208  	if p.config.SaltCallArgs != "" {
   209  		cmd_args.WriteString(" ")
   210  		cmd_args.WriteString(p.config.SaltCallArgs)
   211  	}
   212  
   213  	p.config.CmdArgs = cmd_args.String()
   214  
   215  	if errs != nil && len(errs.Errors) > 0 {
   216  		return errs
   217  	}
   218  
   219  	return nil
   220  }
   221  
   222  func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
   223  	var err error
   224  	var src, dst string
   225  
   226  	ui.Say("Provisioning with Salt...")
   227  	if !p.config.SkipBootstrap {
   228  		cmd := &packer.RemoteCmd{
   229  			// Fallback on wget if curl failed for any reason (such as not being installed)
   230  			Command: fmt.Sprintf(p.guestOSTypeConfig.bootstrapFetchCmd),
   231  		}
   232  		ui.Message(fmt.Sprintf("Downloading saltstack bootstrap to /tmp/install_salt.sh"))
   233  		if err = cmd.StartWithUi(comm, ui); err != nil {
   234  			return fmt.Errorf("Unable to download Salt: %s", err)
   235  		}
   236  		cmd = &packer.RemoteCmd{
   237  			Command: fmt.Sprintf("%s %s", p.sudo(p.guestOSTypeConfig.bootstrapRunCmd), p.config.BootstrapArgs),
   238  		}
   239  		ui.Message(fmt.Sprintf("Installing Salt with command %s", cmd.Command))
   240  		if err = cmd.StartWithUi(comm, ui); err != nil {
   241  			return fmt.Errorf("Unable to install Salt: %s", err)
   242  		}
   243  	}
   244  
   245  	ui.Message(fmt.Sprintf("Creating remote temporary directory: %s", p.config.TempConfigDir))
   246  	if err := p.createDir(ui, comm, p.config.TempConfigDir); err != nil {
   247  		return fmt.Errorf("Error creating remote temporary directory: %s", err)
   248  	}
   249  
   250  	if p.config.MinionConfig != "" {
   251  		ui.Message(fmt.Sprintf("Uploading minion config: %s", p.config.MinionConfig))
   252  		src = p.config.MinionConfig
   253  		dst = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "minion"))
   254  		if err = p.uploadFile(ui, comm, dst, src); err != nil {
   255  			return fmt.Errorf("Error uploading local minion config file to remote: %s", err)
   256  		}
   257  
   258  		// move minion config into /etc/salt
   259  		ui.Message(fmt.Sprintf("Make sure directory %s exists", p.guestOSTypeConfig.configDir))
   260  		if err := p.createDir(ui, comm, p.guestOSTypeConfig.configDir); err != nil {
   261  			return fmt.Errorf("Error creating remote salt configuration directory: %s", err)
   262  		}
   263  		src = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "minion"))
   264  		dst = filepath.ToSlash(filepath.Join(p.guestOSTypeConfig.configDir, "minion"))
   265  		if err = p.moveFile(ui, comm, dst, src); err != nil {
   266  			return fmt.Errorf("Unable to move %s/minion to %s/minion: %s", p.config.TempConfigDir, p.guestOSTypeConfig.configDir, err)
   267  		}
   268  	}
   269  
   270  	if p.config.GrainsFile != "" {
   271  		ui.Message(fmt.Sprintf("Uploading grains file: %s", p.config.GrainsFile))
   272  		src = p.config.GrainsFile
   273  		dst = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "grains"))
   274  		if err = p.uploadFile(ui, comm, dst, src); err != nil {
   275  			return fmt.Errorf("Error uploading local grains file to remote: %s", err)
   276  		}
   277  
   278  		// move grains file into /etc/salt
   279  		ui.Message(fmt.Sprintf("Make sure directory %s exists", p.guestOSTypeConfig.configDir))
   280  		if err := p.createDir(ui, comm, p.guestOSTypeConfig.configDir); err != nil {
   281  			return fmt.Errorf("Error creating remote salt configuration directory: %s", err)
   282  		}
   283  		src = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "grains"))
   284  		dst = filepath.ToSlash(filepath.Join(p.guestOSTypeConfig.configDir, "grains"))
   285  		if err = p.moveFile(ui, comm, dst, src); err != nil {
   286  			return fmt.Errorf("Unable to move %s/grains to %s/grains: %s", p.config.TempConfigDir, p.guestOSTypeConfig.configDir, err)
   287  		}
   288  	}
   289  
   290  	ui.Message(fmt.Sprintf("Uploading local state tree: %s", p.config.LocalStateTree))
   291  	src = p.config.LocalStateTree
   292  	dst = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "states"))
   293  	if err = p.uploadDir(ui, comm, dst, src, []string{".git"}); err != nil {
   294  		return fmt.Errorf("Error uploading local state tree to remote: %s", err)
   295  	}
   296  
   297  	// move state tree from temporary directory
   298  	src = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "states"))
   299  	if p.config.RemoteStateTree != "" {
   300  		dst = p.config.RemoteStateTree
   301  	} else {
   302  		dst = p.guestOSTypeConfig.stateRoot
   303  	}
   304  
   305  	if err = p.statPath(ui, comm, dst); err != nil {
   306  		if err = p.removeDir(ui, comm, dst); err != nil {
   307  			return fmt.Errorf("Unable to clear salt tree: %s", err)
   308  		}
   309  	}
   310  
   311  	if err = p.moveFile(ui, comm, dst, src); err != nil {
   312  		return fmt.Errorf("Unable to move %s/states to %s: %s", p.config.TempConfigDir, dst, err)
   313  	}
   314  
   315  	if p.config.LocalPillarRoots != "" {
   316  		ui.Message(fmt.Sprintf("Uploading local pillar roots: %s", p.config.LocalPillarRoots))
   317  		src = p.config.LocalPillarRoots
   318  		dst = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "pillar"))
   319  		if err = p.uploadDir(ui, comm, dst, src, []string{".git"}); err != nil {
   320  			return fmt.Errorf("Error uploading local pillar roots to remote: %s", err)
   321  		}
   322  
   323  		// move pillar root from temporary directory
   324  		src = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "pillar"))
   325  		if p.config.RemotePillarRoots != "" {
   326  			dst = p.config.RemotePillarRoots
   327  		} else {
   328  			dst = p.guestOSTypeConfig.pillarRoot
   329  		}
   330  
   331  		if err = p.statPath(ui, comm, dst); err != nil {
   332  			if err = p.removeDir(ui, comm, dst); err != nil {
   333  				return fmt.Errorf("Unable to clear pillar root: %s", err)
   334  			}
   335  		}
   336  
   337  		if err = p.moveFile(ui, comm, dst, src); err != nil {
   338  			return fmt.Errorf("Unable to move %s/pillar to %s: %s", p.config.TempConfigDir, dst, err)
   339  		}
   340  	}
   341  
   342  	ui.Message(fmt.Sprintf("Running: salt-call --local %s", p.config.CmdArgs))
   343  	cmd := &packer.RemoteCmd{Command: p.sudo(fmt.Sprintf("%s --local %s", filepath.Join(p.config.SaltBinDir, "salt-call"), p.config.CmdArgs))}
   344  	if err = cmd.StartWithUi(comm, ui); err != nil || cmd.ExitStatus != 0 {
   345  		if err == nil {
   346  			err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus)
   347  		}
   348  
   349  		return fmt.Errorf("Error executing salt-call: %s", err)
   350  	}
   351  
   352  	return nil
   353  }
   354  
   355  func (p *Provisioner) Cancel() {
   356  	// Just hard quit. It isn't a big deal if what we're doing keeps
   357  	// running on the other side.
   358  	os.Exit(0)
   359  }
   360  
   361  // Prepends sudo to supplied command if config says to
   362  func (p *Provisioner) sudo(cmd string) string {
   363  	if p.config.DisableSudo || (p.config.GuestOSType == provisioner.WindowsOSType) {
   364  		return cmd
   365  	}
   366  
   367  	return "sudo " + cmd
   368  }
   369  
   370  func validateDirConfig(path string, name string, required bool) error {
   371  	if required && path == "" {
   372  		return fmt.Errorf("%s cannot be empty", name)
   373  	} else if required == false && path == "" {
   374  		return nil
   375  	}
   376  	info, err := os.Stat(path)
   377  	if err != nil {
   378  		return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err)
   379  	} else if !info.IsDir() {
   380  		return fmt.Errorf("%s: path '%s' must point to a directory", name, path)
   381  	}
   382  	return nil
   383  }
   384  
   385  func validateFileConfig(path string, name string, required bool) error {
   386  	if required == true && path == "" {
   387  		return fmt.Errorf("%s cannot be empty", name)
   388  	} else if required == false && path == "" {
   389  		return nil
   390  	}
   391  	info, err := os.Stat(path)
   392  	if err != nil {
   393  		return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err)
   394  	} else if info.IsDir() {
   395  		return fmt.Errorf("%s: path '%s' must point to a file", name, path)
   396  	}
   397  	return nil
   398  }
   399  
   400  func (p *Provisioner) uploadFile(ui packer.Ui, comm packer.Communicator, dst, src string) error {
   401  	f, err := os.Open(src)
   402  	if err != nil {
   403  		return fmt.Errorf("Error opening: %s", err)
   404  	}
   405  	defer f.Close()
   406  
   407  	if err = comm.Upload(dst, f, nil); err != nil {
   408  		return fmt.Errorf("Error uploading %s: %s", src, err)
   409  	}
   410  	return nil
   411  }
   412  
   413  func (p *Provisioner) moveFile(ui packer.Ui, comm packer.Communicator, dst string, src string) error {
   414  	ui.Message(fmt.Sprintf("Moving %s to %s", src, dst))
   415  	cmd := &packer.RemoteCmd{
   416  		Command: p.sudo(p.guestCommands.MovePath(src, dst)),
   417  	}
   418  	if err := cmd.StartWithUi(comm, ui); err != nil || cmd.ExitStatus != 0 {
   419  		if err == nil {
   420  			err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus)
   421  		}
   422  
   423  		return fmt.Errorf("Unable to move %s to %s: %s", src, dst, err)
   424  	}
   425  	return nil
   426  }
   427  
   428  func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error {
   429  	ui.Message(fmt.Sprintf("Creating directory: %s", dir))
   430  	cmd := &packer.RemoteCmd{
   431  		Command: p.guestCommands.CreateDir(dir),
   432  	}
   433  	if err := cmd.StartWithUi(comm, ui); err != nil {
   434  		return err
   435  	}
   436  	if cmd.ExitStatus != 0 {
   437  		return fmt.Errorf("Non-zero exit status.")
   438  	}
   439  	return nil
   440  }
   441  
   442  func (p *Provisioner) statPath(ui packer.Ui, comm packer.Communicator, path string) error {
   443  	ui.Message(fmt.Sprintf("Verifying Path: %s", path))
   444  	cmd := &packer.RemoteCmd{
   445  		Command: p.guestCommands.StatPath(path),
   446  	}
   447  	if err := cmd.StartWithUi(comm, ui); err != nil {
   448  		return err
   449  	}
   450  	if cmd.ExitStatus != 0 {
   451  		return fmt.Errorf("Non-zero exit status.")
   452  	}
   453  	return nil
   454  }
   455  
   456  func (p *Provisioner) removeDir(ui packer.Ui, comm packer.Communicator, dir string) error {
   457  	ui.Message(fmt.Sprintf("Removing directory: %s", dir))
   458  	cmd := &packer.RemoteCmd{
   459  		Command: p.guestCommands.RemoveDir(dir),
   460  	}
   461  	if err := cmd.StartWithUi(comm, ui); err != nil {
   462  		return err
   463  	}
   464  	if cmd.ExitStatus != 0 {
   465  		return fmt.Errorf("Non-zero exit status.")
   466  	}
   467  	return nil
   468  }
   469  
   470  func (p *Provisioner) uploadDir(ui packer.Ui, comm packer.Communicator, dst, src string, ignore []string) error {
   471  	if err := p.createDir(ui, comm, dst); err != nil {
   472  		return err
   473  	}
   474  
   475  	// Make sure there is a trailing "/" so that the directory isn't
   476  	// created on the other side.
   477  	if src[len(src)-1] != '/' {
   478  		src = src + "/"
   479  	}
   480  	return comm.UploadDir(dst, src, ignore)
   481  }