github.com/pgray/terraform@v0.5.4-0.20170822184730-b6a464c5214d/builtin/provisioners/salt-masterless/resource_provisioner.go (about)

     1  // This package implements a provisioner for Terraform that executes a
     2  // saltstack state within the remote machine
     3  //
     4  // Adapted from gitub.com/hashicorp/packer/provisioner/salt-masterless
     5  
     6  package saltmasterless
     7  
     8  import (
     9  	"bytes"
    10  	"context"
    11  	"errors"
    12  	"fmt"
    13  	"os"
    14  	"path/filepath"
    15  
    16  	"github.com/hashicorp/terraform/communicator"
    17  	"github.com/hashicorp/terraform/communicator/remote"
    18  	"github.com/hashicorp/terraform/helper/schema"
    19  	"github.com/hashicorp/terraform/terraform"
    20  )
    21  
    22  type provisionFn func(terraform.UIOutput, communicator.Communicator) error
    23  
    24  type provisioner struct {
    25  	SkipBootstrap     bool
    26  	BootstrapArgs     string
    27  	LocalStateTree    string
    28  	DisableSudo       bool
    29  	CustomState       string
    30  	MinionConfig      string
    31  	LocalPillarRoots  string
    32  	RemoteStateTree   string
    33  	RemotePillarRoots string
    34  	TempConfigDir     string
    35  	NoExitOnFailure   bool
    36  	LogLevel          string
    37  	SaltCallArgs      string
    38  	CmdArgs           string
    39  }
    40  
    41  const DefaultStateTreeDir = "/srv/salt"
    42  const DefaultPillarRootDir = "/srv/pillar"
    43  
    44  // Provisioner returns a salt-masterless provisioner
    45  func Provisioner() terraform.ResourceProvisioner {
    46  	return &schema.Provisioner{
    47  		Schema: map[string]*schema.Schema{
    48  			"local_state_tree": &schema.Schema{
    49  				Type:     schema.TypeString,
    50  				Required: true,
    51  			},
    52  			"local_pillar_roots": &schema.Schema{
    53  				Type:     schema.TypeString,
    54  				Optional: true,
    55  			},
    56  			"remote_state_tree": &schema.Schema{
    57  				Type:     schema.TypeString,
    58  				Optional: true,
    59  			},
    60  			"remote_pillar_roots": &schema.Schema{
    61  				Type:     schema.TypeString,
    62  				Optional: true,
    63  			},
    64  			"temp_config_dir": &schema.Schema{
    65  				Type:     schema.TypeString,
    66  				Optional: true,
    67  				Default:  "/tmp/salt",
    68  			},
    69  			"skip_bootstrap": &schema.Schema{
    70  				Type:     schema.TypeBool,
    71  				Optional: true,
    72  			},
    73  			"no_exit_on_failure": &schema.Schema{
    74  				Type:     schema.TypeBool,
    75  				Optional: true,
    76  			},
    77  			"bootstrap_args": &schema.Schema{
    78  				Type:     schema.TypeString,
    79  				Optional: true,
    80  			},
    81  			"disable_sudo": &schema.Schema{
    82  				Type:     schema.TypeBool,
    83  				Optional: true,
    84  			},
    85  			"custom_state": &schema.Schema{
    86  				Type:     schema.TypeString,
    87  				Optional: true,
    88  			},
    89  			"minion_config": &schema.Schema{
    90  				Type:     schema.TypeString,
    91  				Optional: true,
    92  			},
    93  			"cmd_args": &schema.Schema{
    94  				Type:     schema.TypeString,
    95  				Optional: true,
    96  			},
    97  			"salt_call_args": &schema.Schema{
    98  				Type:     schema.TypeString,
    99  				Optional: true,
   100  			},
   101  			"log_level": &schema.Schema{
   102  				Type:     schema.TypeString,
   103  				Optional: true,
   104  			},
   105  		},
   106  
   107  		ApplyFunc:    applyFn,
   108  		ValidateFunc: validateFn,
   109  	}
   110  }
   111  
   112  // Apply executes the file provisioner
   113  func applyFn(ctx context.Context) error {
   114  	// Decode the raw config for this provisioner
   115  	var err error
   116  
   117  	o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput)
   118  	d := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
   119  
   120  	p, err := decodeConfig(d)
   121  	if err != nil {
   122  		return err
   123  	}
   124  
   125  	// Get a new communicator
   126  	comm, err := communicator.New(d.State())
   127  	if err != nil {
   128  		return err
   129  	}
   130  
   131  	var src, dst string
   132  
   133  	o.Output("Provisioning with Salt...")
   134  	if !p.SkipBootstrap {
   135  		cmd := &remote.Cmd{
   136  			// Fallback on wget if curl failed for any reason (such as not being installed)
   137  			Command: fmt.Sprintf("curl -L https://bootstrap.saltstack.com -o /tmp/install_salt.sh || wget -O /tmp/install_salt.sh https://bootstrap.saltstack.com"),
   138  		}
   139  		o.Output(fmt.Sprintf("Downloading saltstack bootstrap to /tmp/install_salt.sh"))
   140  		if err = comm.Start(cmd); err != nil {
   141  			return fmt.Errorf("Unable to download Salt: %s", err)
   142  		}
   143  		cmd = &remote.Cmd{
   144  			Command: fmt.Sprintf("%s /tmp/install_salt.sh %s", p.sudo("sh"), p.BootstrapArgs),
   145  		}
   146  		o.Output(fmt.Sprintf("Installing Salt with command %s", cmd.Command))
   147  		if err = comm.Start(cmd); err != nil {
   148  			return fmt.Errorf("Unable to install Salt: %s", err)
   149  		}
   150  	}
   151  
   152  	o.Output(fmt.Sprintf("Creating remote temporary directory: %s", p.TempConfigDir))
   153  	if err := p.createDir(o, comm, p.TempConfigDir); err != nil {
   154  		return fmt.Errorf("Error creating remote temporary directory: %s", err)
   155  	}
   156  
   157  	if p.MinionConfig != "" {
   158  		o.Output(fmt.Sprintf("Uploading minion config: %s", p.MinionConfig))
   159  		src = p.MinionConfig
   160  		dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "minion"))
   161  		if err = p.uploadFile(o, comm, dst, src); err != nil {
   162  			return fmt.Errorf("Error uploading local minion config file to remote: %s", err)
   163  		}
   164  
   165  		// move minion config into /etc/salt
   166  		o.Output(fmt.Sprintf("Make sure directory %s exists", "/etc/salt"))
   167  		if err := p.createDir(o, comm, "/etc/salt"); err != nil {
   168  			return fmt.Errorf("Error creating remote salt configuration directory: %s", err)
   169  		}
   170  		src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "minion"))
   171  		dst = "/etc/salt/minion"
   172  		if err = p.moveFile(o, comm, dst, src); err != nil {
   173  			return fmt.Errorf("Unable to move %s/minion to /etc/salt/minion: %s", p.TempConfigDir, err)
   174  		}
   175  	}
   176  
   177  	o.Output(fmt.Sprintf("Uploading local state tree: %s", p.LocalStateTree))
   178  	src = p.LocalStateTree
   179  	dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "states"))
   180  	if err = p.uploadDir(o, comm, dst, src, []string{".git"}); err != nil {
   181  		return fmt.Errorf("Error uploading local state tree to remote: %s", err)
   182  	}
   183  
   184  	// move state tree from temporary directory
   185  	src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "states"))
   186  	dst = p.RemoteStateTree
   187  	if err = p.removeDir(o, comm, dst); err != nil {
   188  		return fmt.Errorf("Unable to clear salt tree: %s", err)
   189  	}
   190  	if err = p.moveFile(o, comm, dst, src); err != nil {
   191  		return fmt.Errorf("Unable to move %s/states to %s: %s", p.TempConfigDir, dst, err)
   192  	}
   193  
   194  	if p.LocalPillarRoots != "" {
   195  		o.Output(fmt.Sprintf("Uploading local pillar roots: %s", p.LocalPillarRoots))
   196  		src = p.LocalPillarRoots
   197  		dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "pillar"))
   198  		if err = p.uploadDir(o, comm, dst, src, []string{".git"}); err != nil {
   199  			return fmt.Errorf("Error uploading local pillar roots to remote: %s", err)
   200  		}
   201  
   202  		// move pillar root from temporary directory
   203  		src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "pillar"))
   204  		dst = p.RemotePillarRoots
   205  
   206  		if err = p.removeDir(o, comm, dst); err != nil {
   207  			return fmt.Errorf("Unable to clear pillar root: %s", err)
   208  		}
   209  		if err = p.moveFile(o, comm, dst, src); err != nil {
   210  			return fmt.Errorf("Unable to move %s/pillar to %s: %s", p.TempConfigDir, dst, err)
   211  		}
   212  	}
   213  
   214  	o.Output(fmt.Sprintf("Running: salt-call --local %s", p.CmdArgs))
   215  	cmd := &remote.Cmd{Command: p.sudo(fmt.Sprintf("salt-call --local %s", p.CmdArgs))}
   216  	if err = comm.Start(cmd); err != nil || cmd.ExitStatus != 0 {
   217  		if err == nil {
   218  			err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus)
   219  		}
   220  
   221  		return fmt.Errorf("Error executing salt-call: %s", err)
   222  	}
   223  
   224  	return nil
   225  }
   226  
   227  // Prepends sudo to supplied command if config says to
   228  func (p *provisioner) sudo(cmd string) string {
   229  	if p.DisableSudo {
   230  		return cmd
   231  	}
   232  
   233  	return "sudo " + cmd
   234  }
   235  
   236  func validateDirConfig(path string, name string, required bool) error {
   237  	if required == true && path == "" {
   238  		return fmt.Errorf("%s cannot be empty", name)
   239  	} else if required == false && path == "" {
   240  		return nil
   241  	}
   242  	info, err := os.Stat(path)
   243  	if err != nil {
   244  		return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err)
   245  	} else if !info.IsDir() {
   246  		return fmt.Errorf("%s: path '%s' must point to a directory", name, path)
   247  	}
   248  	return nil
   249  }
   250  
   251  func validateFileConfig(path string, name string, required bool) error {
   252  	if required == true && path == "" {
   253  		return fmt.Errorf("%s cannot be empty", name)
   254  	} else if required == false && path == "" {
   255  		return nil
   256  	}
   257  	info, err := os.Stat(path)
   258  	if err != nil {
   259  		return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err)
   260  	} else if info.IsDir() {
   261  		return fmt.Errorf("%s: path '%s' must point to a file", name, path)
   262  	}
   263  	return nil
   264  }
   265  
   266  func (p *provisioner) uploadFile(o terraform.UIOutput, comm communicator.Communicator, dst, src string) error {
   267  	f, err := os.Open(src)
   268  	if err != nil {
   269  		return fmt.Errorf("Error opening: %s", err)
   270  	}
   271  	defer f.Close()
   272  
   273  	if err = comm.Upload(dst, f); err != nil {
   274  		return fmt.Errorf("Error uploading %s: %s", src, err)
   275  	}
   276  	return nil
   277  }
   278  
   279  func (p *provisioner) moveFile(o terraform.UIOutput, comm communicator.Communicator, dst, src string) error {
   280  	o.Output(fmt.Sprintf("Moving %s to %s", src, dst))
   281  	cmd := &remote.Cmd{Command: fmt.Sprintf(p.sudo("mv %s %s"), src, dst)}
   282  	if err := comm.Start(cmd); err != nil || cmd.ExitStatus != 0 {
   283  		if err == nil {
   284  			err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus)
   285  		}
   286  
   287  		return fmt.Errorf("Unable to move %s to %s: %s", src, dst, err)
   288  	}
   289  	return nil
   290  }
   291  
   292  func (p *provisioner) createDir(o terraform.UIOutput, comm communicator.Communicator, dir string) error {
   293  	o.Output(fmt.Sprintf("Creating directory: %s", dir))
   294  	cmd := &remote.Cmd{
   295  		Command: fmt.Sprintf("mkdir -p '%s'", dir),
   296  	}
   297  	if err := comm.Start(cmd); err != nil {
   298  		return err
   299  	}
   300  	if cmd.ExitStatus != 0 {
   301  		return fmt.Errorf("Non-zero exit status.")
   302  	}
   303  	return nil
   304  }
   305  
   306  func (p *provisioner) removeDir(o terraform.UIOutput, comm communicator.Communicator, dir string) error {
   307  	o.Output(fmt.Sprintf("Removing directory: %s", dir))
   308  	cmd := &remote.Cmd{
   309  		Command: fmt.Sprintf("rm -rf '%s'", dir),
   310  	}
   311  	if err := comm.Start(cmd); err != nil {
   312  		return err
   313  	}
   314  	if cmd.ExitStatus != 0 {
   315  		return fmt.Errorf("Non-zero exit status.")
   316  	}
   317  	return nil
   318  }
   319  
   320  func (p *provisioner) uploadDir(o terraform.UIOutput, comm communicator.Communicator, dst, src string, ignore []string) error {
   321  	if err := p.createDir(o, comm, dst); err != nil {
   322  		return err
   323  	}
   324  
   325  	// Make sure there is a trailing "/" so that the directory isn't
   326  	// created on the other side.
   327  	if src[len(src)-1] != '/' {
   328  		src = src + "/"
   329  	}
   330  	return comm.UploadDir(dst, src)
   331  }
   332  
   333  // Validate checks if the required arguments are configured
   334  func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) {
   335  	// require a salt state tree
   336  	localStateTreeTmp, ok := c.Get("local_state_tree")
   337  	var localStateTree string
   338  	if !ok {
   339  		es = append(es,
   340  			errors.New("Required local_state_tree is not set"))
   341  	} else {
   342  		localStateTree = localStateTreeTmp.(string)
   343  	}
   344  	err := validateDirConfig(localStateTree, "local_state_tree", true)
   345  	if err != nil {
   346  		es = append(es, err)
   347  	}
   348  
   349  	var localPillarRoots string
   350  	localPillarRootsTmp, ok := c.Get("local_pillar_roots")
   351  	if !ok {
   352  		localPillarRoots = ""
   353  	} else {
   354  		localPillarRoots = localPillarRootsTmp.(string)
   355  	}
   356  
   357  	err = validateDirConfig(localPillarRoots, "local_pillar_roots", false)
   358  	if err != nil {
   359  		es = append(es, err)
   360  	}
   361  
   362  	var minionConfig string
   363  	minionConfigTmp, ok := c.Get("minion_config")
   364  	if !ok {
   365  		minionConfig = ""
   366  	} else {
   367  		minionConfig = minionConfigTmp.(string)
   368  	}
   369  	err = validateFileConfig(minionConfig, "minion_config", false)
   370  	if err != nil {
   371  		es = append(es, err)
   372  	}
   373  
   374  	var remoteStateTree string
   375  	remoteStateTreeTmp, ok := c.Get("remote_state_tree")
   376  	if !ok {
   377  		remoteStateTree = ""
   378  	} else {
   379  		remoteStateTree = remoteStateTreeTmp.(string)
   380  	}
   381  
   382  	var remotePillarRoots string
   383  	remotePillarRootsTmp, ok := c.Get("remote_pillar_roots")
   384  	if !ok {
   385  		remotePillarRoots = ""
   386  	} else {
   387  		remotePillarRoots = remotePillarRootsTmp.(string)
   388  	}
   389  
   390  	if minionConfig != "" && (remoteStateTree != "" || remotePillarRoots != "") {
   391  		es = append(es,
   392  			errors.New("remote_state_tree and remote_pillar_roots only apply when minion_config is not used"))
   393  	}
   394  
   395  	if len(es) > 0 {
   396  		return ws, es
   397  	}
   398  
   399  	return ws, es
   400  }
   401  
   402  func decodeConfig(d *schema.ResourceData) (*provisioner, error) {
   403  	p := &provisioner{
   404  		LocalStateTree:    d.Get("local_state_tree").(string),
   405  		LogLevel:          d.Get("log_level").(string),
   406  		SaltCallArgs:      d.Get("salt_call_args").(string),
   407  		CmdArgs:           d.Get("cmd_args").(string),
   408  		MinionConfig:      d.Get("minion_config").(string),
   409  		CustomState:       d.Get("custom_state").(string),
   410  		DisableSudo:       d.Get("disable_sudo").(bool),
   411  		BootstrapArgs:     d.Get("bootstrap_args").(string),
   412  		NoExitOnFailure:   d.Get("no_exit_on_failure").(bool),
   413  		SkipBootstrap:     d.Get("skip_bootstrap").(bool),
   414  		TempConfigDir:     d.Get("temp_config_dir").(string),
   415  		RemotePillarRoots: d.Get("remote_pillar_roots").(string),
   416  		RemoteStateTree:   d.Get("remote_state_tree").(string),
   417  		LocalPillarRoots:  d.Get("local_pillar_roots").(string),
   418  	}
   419  
   420  	// build the command line args to pass onto salt
   421  	var cmdArgs bytes.Buffer
   422  
   423  	if p.CustomState == "" {
   424  		cmdArgs.WriteString(" state.highstate")
   425  	} else {
   426  		cmdArgs.WriteString(" state.sls ")
   427  		cmdArgs.WriteString(p.CustomState)
   428  	}
   429  
   430  	if p.MinionConfig == "" {
   431  		// pass --file-root and --pillar-root if no minion_config is supplied
   432  		if p.RemoteStateTree != "" {
   433  			cmdArgs.WriteString(" --file-root=")
   434  			cmdArgs.WriteString(p.RemoteStateTree)
   435  		} else {
   436  			cmdArgs.WriteString(" --file-root=")
   437  			cmdArgs.WriteString(DefaultStateTreeDir)
   438  		}
   439  		if p.RemotePillarRoots != "" {
   440  			cmdArgs.WriteString(" --pillar-root=")
   441  			cmdArgs.WriteString(p.RemotePillarRoots)
   442  		} else {
   443  			cmdArgs.WriteString(" --pillar-root=")
   444  			cmdArgs.WriteString(DefaultPillarRootDir)
   445  		}
   446  	}
   447  
   448  	if !p.NoExitOnFailure {
   449  		cmdArgs.WriteString(" --retcode-passthrough")
   450  	}
   451  
   452  	if p.LogLevel == "" {
   453  		cmdArgs.WriteString(" -l info")
   454  	} else {
   455  		cmdArgs.WriteString(" -l ")
   456  		cmdArgs.WriteString(p.LogLevel)
   457  	}
   458  
   459  	if p.SaltCallArgs != "" {
   460  		cmdArgs.WriteString(" ")
   461  		cmdArgs.WriteString(p.SaltCallArgs)
   462  	}
   463  
   464  	p.CmdArgs = cmdArgs.String()
   465  
   466  	return p, nil
   467  }