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