github.com/kimor79/packer@v0.8.7-0.20151221212622-d507b18eb4cf/provisioner/salt-masterless/provisioner.go (about)

     1  // This package implements a provisioner for Packer that executes a
     2  // saltstack highstate within the remote machine
     3  package saltmasterless
     4  
     5  import (
     6  	"errors"
     7  	"fmt"
     8  	"os"
     9  	"path/filepath"
    10  
    11  	"github.com/mitchellh/packer/common"
    12  	"github.com/mitchellh/packer/helper/config"
    13  	"github.com/mitchellh/packer/packer"
    14  	"github.com/mitchellh/packer/template/interpolate"
    15  )
    16  
    17  const DefaultTempConfigDir = "/tmp/salt"
    18  const DefaultStateTreeDir = "/srv/salt"
    19  const DefaultPillarRootDir = "/srv/pillar"
    20  
    21  type Config struct {
    22  	common.PackerConfig `mapstructure:",squash"`
    23  
    24  	// If true, run the salt-bootstrap script
    25  	SkipBootstrap bool   `mapstructure:"skip_bootstrap"`
    26  	BootstrapArgs string `mapstructure:"bootstrap_args"`
    27  
    28  	DisableSudo bool `mapstructure:"disable_sudo"`
    29  
    30  	// Local path to the minion config
    31  	MinionConfig string `mapstructure:"minion_config"`
    32  
    33  	// Local path to the salt state tree
    34  	LocalStateTree string `mapstructure:"local_state_tree"`
    35  
    36  	// Local path to the salt pillar roots
    37  	LocalPillarRoots string `mapstructure:"local_pillar_roots"`
    38  
    39  	// Remote path to the salt state tree
    40  	RemoteStateTree string `mapstructure:"remote_state_tree"`
    41  
    42  	// Remote path to the salt pillar roots
    43  	RemotePillarRoots string `mapstructure:"remote_pillar_roots"`
    44  
    45  	// Where files will be copied before moving to the /srv/salt directory
    46  	TempConfigDir string `mapstructure:"temp_config_dir"`
    47  
    48  	ctx interpolate.Context
    49  }
    50  
    51  type Provisioner struct {
    52  	config Config
    53  }
    54  
    55  func (p *Provisioner) Prepare(raws ...interface{}) error {
    56  	err := config.Decode(&p.config, &config.DecodeOpts{
    57  		Interpolate:        true,
    58  		InterpolateContext: &p.config.ctx,
    59  		InterpolateFilter: &interpolate.RenderFilter{
    60  			Exclude: []string{},
    61  		},
    62  	}, raws...)
    63  	if err != nil {
    64  		return err
    65  	}
    66  
    67  	if p.config.TempConfigDir == "" {
    68  		p.config.TempConfigDir = DefaultTempConfigDir
    69  	}
    70  
    71  	if p.config.RemoteStateTree == "" {
    72  		p.config.RemoteStateTree = DefaultStateTreeDir
    73  	}
    74  
    75  	if p.config.RemotePillarRoots == "" {
    76  		p.config.RemotePillarRoots = DefaultPillarRootDir
    77  	}
    78  
    79  	var errs *packer.MultiError
    80  
    81  	// require a salt state tree
    82  	if p.config.LocalStateTree == "" {
    83  		errs = packer.MultiErrorAppend(errs,
    84  			errors.New("local_state_tree must be supplied"))
    85  	} else {
    86  		if _, err := os.Stat(p.config.LocalStateTree); err != nil {
    87  			errs = packer.MultiErrorAppend(errs,
    88  				errors.New("local_state_tree must exist and be accessible"))
    89  		}
    90  	}
    91  
    92  	if p.config.LocalPillarRoots != "" {
    93  		if _, err := os.Stat(p.config.LocalPillarRoots); err != nil {
    94  			errs = packer.MultiErrorAppend(errs,
    95  				errors.New("local_pillar_roots must exist and be accessible"))
    96  		}
    97  	}
    98  
    99  	if p.config.MinionConfig != "" {
   100  		if _, err := os.Stat(p.config.MinionConfig); err != nil {
   101  			errs = packer.MultiErrorAppend(errs,
   102  				errors.New("minion_config must exist and be accessible"))
   103  		}
   104  	}
   105  
   106  	if errs != nil && len(errs.Errors) > 0 {
   107  		return errs
   108  	}
   109  
   110  	return nil
   111  }
   112  
   113  func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
   114  	var err error
   115  	var src, dst string
   116  
   117  	ui.Say("Provisioning with Salt...")
   118  	if !p.config.SkipBootstrap {
   119  		cmd := &packer.RemoteCmd{
   120  			Command: fmt.Sprintf("curl -L https://bootstrap.saltstack.com -o /tmp/install_salt.sh"),
   121  		}
   122  		ui.Message(fmt.Sprintf("Downloading saltstack bootstrap to /tmp/install_salt.sh"))
   123  		if err = cmd.StartWithUi(comm, ui); err != nil {
   124  			return fmt.Errorf("Unable to download Salt: %s", err)
   125  		}
   126  		cmd = &packer.RemoteCmd{
   127  			Command: fmt.Sprintf("%s /tmp/install_salt.sh %s", p.sudo("sh"), p.config.BootstrapArgs),
   128  		}
   129  		ui.Message(fmt.Sprintf("Installing Salt with command %s", cmd.Command))
   130  		if err = cmd.StartWithUi(comm, ui); err != nil {
   131  			return fmt.Errorf("Unable to install Salt: %s", err)
   132  		}
   133  	}
   134  
   135  	ui.Message(fmt.Sprintf("Creating remote temporary directory: %s", p.config.TempConfigDir))
   136  	if err := p.createDir(ui, comm, p.config.TempConfigDir); err != nil {
   137  		return fmt.Errorf("Error creating remote temporary directory: %s", err)
   138  	}
   139  
   140  	if p.config.MinionConfig != "" {
   141  		ui.Message(fmt.Sprintf("Uploading minion config: %s", p.config.MinionConfig))
   142  		src = p.config.MinionConfig
   143  		dst = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "minion"))
   144  		if err = p.uploadFile(ui, comm, dst, src); err != nil {
   145  			return fmt.Errorf("Error uploading local minion config file to remote: %s", err)
   146  		}
   147  
   148  		// move minion config into /etc/salt
   149  		ui.Message(fmt.Sprintf("Make sure directory %s exists", "/etc/salt"))
   150  		if err := p.createDir(ui, comm, "/etc/salt"); err != nil {
   151  			return fmt.Errorf("Error creating remote salt configuration directory: %s", err)
   152  		}
   153  		src = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "minion"))
   154  		dst = "/etc/salt/minion"
   155  		if err = p.moveFile(ui, comm, dst, src); err != nil {
   156  			return fmt.Errorf("Unable to move %s/minion to /etc/salt/minion: %s", p.config.TempConfigDir, err)
   157  		}
   158  	}
   159  
   160  	ui.Message(fmt.Sprintf("Uploading local state tree: %s", p.config.LocalStateTree))
   161  	src = p.config.LocalStateTree
   162  	dst = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "states"))
   163  	if err = p.uploadDir(ui, comm, dst, src, []string{".git"}); err != nil {
   164  		return fmt.Errorf("Error uploading local state tree to remote: %s", err)
   165  	}
   166  
   167  	// move state tree from temporary directory
   168  	src = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "states"))
   169  	dst = p.config.RemoteStateTree
   170  	if err = p.removeDir(ui, comm, dst); err != nil {
   171  		return fmt.Errorf("Unable to clear salt tree: %s", err)
   172  	}
   173  	if err = p.moveFile(ui, comm, dst, src); err != nil {
   174  		return fmt.Errorf("Unable to move %s/states to %s: %s", p.config.TempConfigDir, dst, err)
   175  	}
   176  
   177  	if p.config.LocalPillarRoots != "" {
   178  		ui.Message(fmt.Sprintf("Uploading local pillar roots: %s", p.config.LocalPillarRoots))
   179  		src = p.config.LocalPillarRoots
   180  		dst = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "pillar"))
   181  		if err = p.uploadDir(ui, comm, dst, src, []string{".git"}); err != nil {
   182  			return fmt.Errorf("Error uploading local pillar roots to remote: %s", err)
   183  		}
   184  
   185  		// move pillar root from temporary directory
   186  		src = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "pillar"))
   187  		dst = p.config.RemotePillarRoots
   188  		if err = p.removeDir(ui, comm, dst); err != nil {
   189  			return fmt.Errorf("Unable to clear pillat root: %s", err)
   190  		}
   191  		if err = p.moveFile(ui, comm, dst, src); err != nil {
   192  			return fmt.Errorf("Unable to move %s/pillar to %s: %s", p.config.TempConfigDir, dst, err)
   193  		}
   194  	}
   195  
   196  	ui.Message("Running highstate")
   197  	cmd := &packer.RemoteCmd{Command: fmt.Sprintf(p.sudo("salt-call --local state.highstate --file-root=%s --pillar-root=%s -l info --retcode-passthrough"), p.config.RemoteStateTree, p.config.RemotePillarRoots)}
   198  	if err = cmd.StartWithUi(comm, ui); err != nil || cmd.ExitStatus != 0 {
   199  		if err == nil {
   200  			err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus)
   201  		}
   202  
   203  		return fmt.Errorf("Error executing highstate: %s", err)
   204  	}
   205  
   206  	return nil
   207  }
   208  
   209  func (p *Provisioner) Cancel() {
   210  	// Just hard quit. It isn't a big deal if what we're doing keeps
   211  	// running on the other side.
   212  	os.Exit(0)
   213  }
   214  
   215  // Prepends sudo to supplied command if config says to
   216  func (p *Provisioner) sudo(cmd string) string {
   217  	if p.config.DisableSudo {
   218  		return cmd
   219  	}
   220  
   221  	return "sudo " + cmd
   222  }
   223  
   224  func (p *Provisioner) uploadFile(ui packer.Ui, comm packer.Communicator, dst, src string) error {
   225  	f, err := os.Open(src)
   226  	if err != nil {
   227  		return fmt.Errorf("Error opening: %s", err)
   228  	}
   229  	defer f.Close()
   230  
   231  	if err = comm.Upload(dst, f, nil); err != nil {
   232  		return fmt.Errorf("Error uploading %s: %s", src, err)
   233  	}
   234  	return nil
   235  }
   236  
   237  func (p *Provisioner) moveFile(ui packer.Ui, comm packer.Communicator, dst, src string) error {
   238  	ui.Message(fmt.Sprintf("Moving %s to %s", src, dst))
   239  	cmd := &packer.RemoteCmd{Command: fmt.Sprintf(p.sudo("mv %s %s"), src, dst)}
   240  	if err := cmd.StartWithUi(comm, ui); err != nil || cmd.ExitStatus != 0 {
   241  		if err == nil {
   242  			err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus)
   243  		}
   244  
   245  		return fmt.Errorf("Unable to move %s to %s: %s", src, dst, err)
   246  	}
   247  	return nil
   248  }
   249  
   250  func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error {
   251  	ui.Message(fmt.Sprintf("Creating directory: %s", dir))
   252  	cmd := &packer.RemoteCmd{
   253  		Command: fmt.Sprintf("mkdir -p '%s'", dir),
   254  	}
   255  	if err := cmd.StartWithUi(comm, ui); err != nil {
   256  		return err
   257  	}
   258  	if cmd.ExitStatus != 0 {
   259  		return fmt.Errorf("Non-zero exit status.")
   260  	}
   261  	return nil
   262  }
   263  
   264  func (p *Provisioner) removeDir(ui packer.Ui, comm packer.Communicator, dir string) error {
   265  	ui.Message(fmt.Sprintf("Removing directory: %s", dir))
   266  	cmd := &packer.RemoteCmd{
   267  		Command: fmt.Sprintf("rm -rf '%s'", dir),
   268  	}
   269  	if err := cmd.StartWithUi(comm, ui); err != nil {
   270  		return err
   271  	}
   272  	if cmd.ExitStatus != 0 {
   273  		return fmt.Errorf("Non-zero exit status.")
   274  	}
   275  	return nil
   276  }
   277  
   278  func (p *Provisioner) uploadDir(ui packer.Ui, comm packer.Communicator, dst, src string, ignore []string) error {
   279  	if err := p.createDir(ui, comm, dst); err != nil {
   280  		return err
   281  	}
   282  
   283  	// Make sure there is a trailing "/" so that the directory isn't
   284  	// created on the other side.
   285  	if src[len(src)-1] != '/' {
   286  		src = src + "/"
   287  	}
   288  	return comm.UploadDir(dst, src, ignore)
   289  }