github.com/hooklift/terraform@v0.11.0-beta1.0.20171117000744-6786c1361ffe/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_file": &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  	connState := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState)
   120  
   121  	p, err := decodeConfig(d)
   122  	if err != nil {
   123  		return err
   124  	}
   125  
   126  	// Get a new communicator
   127  	comm, err := communicator.New(connState)
   128  	if err != nil {
   129  		return err
   130  	}
   131  
   132  	var src, dst string
   133  
   134  	o.Output("Provisioning with Salt...")
   135  	if !p.SkipBootstrap {
   136  		cmd := &remote.Cmd{
   137  			// Fallback on wget if curl failed for any reason (such as not being installed)
   138  			Command: fmt.Sprintf("curl -L https://bootstrap.saltstack.com -o /tmp/install_salt.sh || wget -O /tmp/install_salt.sh https://bootstrap.saltstack.com"),
   139  		}
   140  		o.Output(fmt.Sprintf("Downloading saltstack bootstrap to /tmp/install_salt.sh"))
   141  		if err = comm.Start(cmd); err != nil {
   142  			return fmt.Errorf("Unable to download Salt: %s", err)
   143  		}
   144  		cmd = &remote.Cmd{
   145  			Command: fmt.Sprintf("%s /tmp/install_salt.sh %s", p.sudo("sh"), p.BootstrapArgs),
   146  		}
   147  		o.Output(fmt.Sprintf("Installing Salt with command %s", cmd.Command))
   148  		if err = comm.Start(cmd); err != nil {
   149  			return fmt.Errorf("Unable to install Salt: %s", err)
   150  		}
   151  	}
   152  
   153  	o.Output(fmt.Sprintf("Creating remote temporary directory: %s", p.TempConfigDir))
   154  	if err := p.createDir(o, comm, p.TempConfigDir); err != nil {
   155  		return fmt.Errorf("Error creating remote temporary directory: %s", err)
   156  	}
   157  
   158  	if p.MinionConfig != "" {
   159  		o.Output(fmt.Sprintf("Uploading minion config: %s", p.MinionConfig))
   160  		src = p.MinionConfig
   161  		dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "minion"))
   162  		if err = p.uploadFile(o, comm, dst, src); err != nil {
   163  			return fmt.Errorf("Error uploading local minion config file to remote: %s", err)
   164  		}
   165  
   166  		// move minion config into /etc/salt
   167  		o.Output(fmt.Sprintf("Make sure directory %s exists", "/etc/salt"))
   168  		if err := p.createDir(o, comm, "/etc/salt"); err != nil {
   169  			return fmt.Errorf("Error creating remote salt configuration directory: %s", err)
   170  		}
   171  		src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "minion"))
   172  		dst = "/etc/salt/minion"
   173  		if err = p.moveFile(o, comm, dst, src); err != nil {
   174  			return fmt.Errorf("Unable to move %s/minion to /etc/salt/minion: %s", p.TempConfigDir, err)
   175  		}
   176  	}
   177  
   178  	o.Output(fmt.Sprintf("Uploading local state tree: %s", p.LocalStateTree))
   179  	src = p.LocalStateTree
   180  	dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "states"))
   181  	if err = p.uploadDir(o, comm, dst, src, []string{".git"}); err != nil {
   182  		return fmt.Errorf("Error uploading local state tree to remote: %s", err)
   183  	}
   184  
   185  	// move state tree from temporary directory
   186  	src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "states"))
   187  	dst = p.RemoteStateTree
   188  	if err = p.removeDir(o, comm, dst); err != nil {
   189  		return fmt.Errorf("Unable to clear salt tree: %s", err)
   190  	}
   191  	if err = p.moveFile(o, comm, dst, src); err != nil {
   192  		return fmt.Errorf("Unable to move %s/states to %s: %s", p.TempConfigDir, dst, err)
   193  	}
   194  
   195  	if p.LocalPillarRoots != "" {
   196  		o.Output(fmt.Sprintf("Uploading local pillar roots: %s", p.LocalPillarRoots))
   197  		src = p.LocalPillarRoots
   198  		dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "pillar"))
   199  		if err = p.uploadDir(o, comm, dst, src, []string{".git"}); err != nil {
   200  			return fmt.Errorf("Error uploading local pillar roots to remote: %s", err)
   201  		}
   202  
   203  		// move pillar root from temporary directory
   204  		src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "pillar"))
   205  		dst = p.RemotePillarRoots
   206  
   207  		if err = p.removeDir(o, comm, dst); err != nil {
   208  			return fmt.Errorf("Unable to clear pillar root: %s", err)
   209  		}
   210  		if err = p.moveFile(o, comm, dst, src); err != nil {
   211  			return fmt.Errorf("Unable to move %s/pillar to %s: %s", p.TempConfigDir, dst, err)
   212  		}
   213  	}
   214  
   215  	o.Output(fmt.Sprintf("Running: salt-call --local %s", p.CmdArgs))
   216  	cmd := &remote.Cmd{Command: p.sudo(fmt.Sprintf("salt-call --local %s", p.CmdArgs))}
   217  	if err = comm.Start(cmd); err != nil || cmd.ExitStatus != 0 {
   218  		if err == nil {
   219  			err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus)
   220  		}
   221  
   222  		return fmt.Errorf("Error executing salt-call: %s", err)
   223  	}
   224  
   225  	return nil
   226  }
   227  
   228  // Prepends sudo to supplied command if config says to
   229  func (p *provisioner) sudo(cmd string) string {
   230  	if p.DisableSudo {
   231  		return cmd
   232  	}
   233  
   234  	return "sudo " + cmd
   235  }
   236  
   237  func validateDirConfig(path string, name string, required bool) error {
   238  	if required == true && path == "" {
   239  		return fmt.Errorf("%s cannot be empty", name)
   240  	} else if required == false && path == "" {
   241  		return nil
   242  	}
   243  	info, err := os.Stat(path)
   244  	if err != nil {
   245  		return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err)
   246  	} else if !info.IsDir() {
   247  		return fmt.Errorf("%s: path '%s' must point to a directory", name, path)
   248  	}
   249  	return nil
   250  }
   251  
   252  func validateFileConfig(path string, name string, required bool) error {
   253  	if required == true && path == "" {
   254  		return fmt.Errorf("%s cannot be empty", name)
   255  	} else if required == false && path == "" {
   256  		return nil
   257  	}
   258  	info, err := os.Stat(path)
   259  	if err != nil {
   260  		return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err)
   261  	} else if info.IsDir() {
   262  		return fmt.Errorf("%s: path '%s' must point to a file", name, path)
   263  	}
   264  	return nil
   265  }
   266  
   267  func (p *provisioner) uploadFile(o terraform.UIOutput, comm communicator.Communicator, dst, src string) error {
   268  	f, err := os.Open(src)
   269  	if err != nil {
   270  		return fmt.Errorf("Error opening: %s", err)
   271  	}
   272  	defer f.Close()
   273  
   274  	if err = comm.Upload(dst, f); err != nil {
   275  		return fmt.Errorf("Error uploading %s: %s", src, err)
   276  	}
   277  	return nil
   278  }
   279  
   280  func (p *provisioner) moveFile(o terraform.UIOutput, comm communicator.Communicator, dst, src string) error {
   281  	o.Output(fmt.Sprintf("Moving %s to %s", src, dst))
   282  	cmd := &remote.Cmd{Command: fmt.Sprintf(p.sudo("mv %s %s"), src, dst)}
   283  	if err := comm.Start(cmd); err != nil || cmd.ExitStatus != 0 {
   284  		if err == nil {
   285  			err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus)
   286  		}
   287  
   288  		return fmt.Errorf("Unable to move %s to %s: %s", src, dst, err)
   289  	}
   290  	return nil
   291  }
   292  
   293  func (p *provisioner) createDir(o terraform.UIOutput, comm communicator.Communicator, dir string) error {
   294  	o.Output(fmt.Sprintf("Creating directory: %s", dir))
   295  	cmd := &remote.Cmd{
   296  		Command: fmt.Sprintf("mkdir -p '%s'", dir),
   297  	}
   298  	if err := comm.Start(cmd); err != nil {
   299  		return err
   300  	}
   301  	if cmd.ExitStatus != 0 {
   302  		return fmt.Errorf("Non-zero exit status.")
   303  	}
   304  	return nil
   305  }
   306  
   307  func (p *provisioner) removeDir(o terraform.UIOutput, comm communicator.Communicator, dir string) error {
   308  	o.Output(fmt.Sprintf("Removing directory: %s", dir))
   309  	cmd := &remote.Cmd{
   310  		Command: fmt.Sprintf("rm -rf '%s'", dir),
   311  	}
   312  	if err := comm.Start(cmd); err != nil {
   313  		return err
   314  	}
   315  	if cmd.ExitStatus != 0 {
   316  		return fmt.Errorf("Non-zero exit status.")
   317  	}
   318  	return nil
   319  }
   320  
   321  func (p *provisioner) uploadDir(o terraform.UIOutput, comm communicator.Communicator, dst, src string, ignore []string) error {
   322  	if err := p.createDir(o, comm, dst); err != nil {
   323  		return err
   324  	}
   325  
   326  	// Make sure there is a trailing "/" so that the directory isn't
   327  	// created on the other side.
   328  	if src[len(src)-1] != '/' {
   329  		src = src + "/"
   330  	}
   331  	return comm.UploadDir(dst, src)
   332  }
   333  
   334  // Validate checks if the required arguments are configured
   335  func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) {
   336  	// require a salt state tree
   337  	localStateTreeTmp, ok := c.Get("local_state_tree")
   338  	var localStateTree string
   339  	if !ok {
   340  		es = append(es,
   341  			errors.New("Required local_state_tree is not set"))
   342  	} else {
   343  		localStateTree = localStateTreeTmp.(string)
   344  	}
   345  	err := validateDirConfig(localStateTree, "local_state_tree", true)
   346  	if err != nil {
   347  		es = append(es, err)
   348  	}
   349  
   350  	var localPillarRoots string
   351  	localPillarRootsTmp, ok := c.Get("local_pillar_roots")
   352  	if !ok {
   353  		localPillarRoots = ""
   354  	} else {
   355  		localPillarRoots = localPillarRootsTmp.(string)
   356  	}
   357  
   358  	err = validateDirConfig(localPillarRoots, "local_pillar_roots", false)
   359  	if err != nil {
   360  		es = append(es, err)
   361  	}
   362  
   363  	var minionConfig string
   364  	minionConfigTmp, ok := c.Get("minion_config_file")
   365  	if !ok {
   366  		minionConfig = ""
   367  	} else {
   368  		minionConfig = minionConfigTmp.(string)
   369  	}
   370  	err = validateFileConfig(minionConfig, "minion_config_file", false)
   371  	if err != nil {
   372  		es = append(es, err)
   373  	}
   374  
   375  	var remoteStateTree string
   376  	remoteStateTreeTmp, ok := c.Get("remote_state_tree")
   377  	if !ok {
   378  		remoteStateTree = ""
   379  	} else {
   380  		remoteStateTree = remoteStateTreeTmp.(string)
   381  	}
   382  
   383  	var remotePillarRoots string
   384  	remotePillarRootsTmp, ok := c.Get("remote_pillar_roots")
   385  	if !ok {
   386  		remotePillarRoots = ""
   387  	} else {
   388  		remotePillarRoots = remotePillarRootsTmp.(string)
   389  	}
   390  
   391  	if minionConfig != "" && (remoteStateTree != "" || remotePillarRoots != "") {
   392  		es = append(es,
   393  			errors.New("remote_state_tree and remote_pillar_roots only apply when minion_config_file is not used"))
   394  	}
   395  
   396  	if len(es) > 0 {
   397  		return ws, es
   398  	}
   399  
   400  	return ws, es
   401  }
   402  
   403  func decodeConfig(d *schema.ResourceData) (*provisioner, error) {
   404  	p := &provisioner{
   405  		LocalStateTree:    d.Get("local_state_tree").(string),
   406  		LogLevel:          d.Get("log_level").(string),
   407  		SaltCallArgs:      d.Get("salt_call_args").(string),
   408  		CmdArgs:           d.Get("cmd_args").(string),
   409  		MinionConfig:      d.Get("minion_config_file").(string),
   410  		CustomState:       d.Get("custom_state").(string),
   411  		DisableSudo:       d.Get("disable_sudo").(bool),
   412  		BootstrapArgs:     d.Get("bootstrap_args").(string),
   413  		NoExitOnFailure:   d.Get("no_exit_on_failure").(bool),
   414  		SkipBootstrap:     d.Get("skip_bootstrap").(bool),
   415  		TempConfigDir:     d.Get("temp_config_dir").(string),
   416  		RemotePillarRoots: d.Get("remote_pillar_roots").(string),
   417  		RemoteStateTree:   d.Get("remote_state_tree").(string),
   418  		LocalPillarRoots:  d.Get("local_pillar_roots").(string),
   419  	}
   420  
   421  	// build the command line args to pass onto salt
   422  	var cmdArgs bytes.Buffer
   423  
   424  	if p.CustomState == "" {
   425  		cmdArgs.WriteString(" state.highstate")
   426  	} else {
   427  		cmdArgs.WriteString(" state.sls ")
   428  		cmdArgs.WriteString(p.CustomState)
   429  	}
   430  
   431  	if p.MinionConfig == "" {
   432  		// pass --file-root and --pillar-root if no minion_config_file is supplied
   433  		if p.RemoteStateTree != "" {
   434  			cmdArgs.WriteString(" --file-root=")
   435  			cmdArgs.WriteString(p.RemoteStateTree)
   436  		} else {
   437  			cmdArgs.WriteString(" --file-root=")
   438  			cmdArgs.WriteString(DefaultStateTreeDir)
   439  		}
   440  		if p.RemotePillarRoots != "" {
   441  			cmdArgs.WriteString(" --pillar-root=")
   442  			cmdArgs.WriteString(p.RemotePillarRoots)
   443  		} else {
   444  			cmdArgs.WriteString(" --pillar-root=")
   445  			cmdArgs.WriteString(DefaultPillarRootDir)
   446  		}
   447  	}
   448  
   449  	if !p.NoExitOnFailure {
   450  		cmdArgs.WriteString(" --retcode-passthrough")
   451  	}
   452  
   453  	if p.LogLevel == "" {
   454  		cmdArgs.WriteString(" -l info")
   455  	} else {
   456  		cmdArgs.WriteString(" -l ")
   457  		cmdArgs.WriteString(p.LogLevel)
   458  	}
   459  
   460  	if p.SaltCallArgs != "" {
   461  		cmdArgs.WriteString(" ")
   462  		cmdArgs.WriteString(p.SaltCallArgs)
   463  	}
   464  
   465  	p.CmdArgs = cmdArgs.String()
   466  
   467  	return p, nil
   468  }