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