github.com/tarrant/terraform@v0.3.8-0.20150402012457-f68c9eee638e/builtin/provisioners/remote-exec/resource_provisioner.go (about)

     1  package remoteexec
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"log"
     9  	"os"
    10  	"strings"
    11  	"time"
    12  
    13  	helper "github.com/hashicorp/terraform/helper/ssh"
    14  	"github.com/hashicorp/terraform/terraform"
    15  	"github.com/mitchellh/go-linereader"
    16  )
    17  
    18  const (
    19  	// DefaultShebang is added at the top of the script file
    20  	DefaultShebang = "#!/bin/sh"
    21  )
    22  
    23  type ResourceProvisioner struct{}
    24  
    25  func (p *ResourceProvisioner) Apply(
    26  	o terraform.UIOutput,
    27  	s *terraform.InstanceState,
    28  	c *terraform.ResourceConfig) error {
    29  	// Ensure the connection type is SSH
    30  	if err := helper.VerifySSH(s); err != nil {
    31  		return err
    32  	}
    33  
    34  	// Get the SSH configuration
    35  	conf, err := helper.ParseSSHConfig(s)
    36  	if err != nil {
    37  		return err
    38  	}
    39  
    40  	// Collect the scripts
    41  	scripts, err := p.collectScripts(c)
    42  	if err != nil {
    43  		return err
    44  	}
    45  	for _, s := range scripts {
    46  		defer s.Close()
    47  	}
    48  
    49  	// Copy and execute each script
    50  	if err := p.runScripts(o, conf, scripts); err != nil {
    51  		return err
    52  	}
    53  	return nil
    54  }
    55  
    56  func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) (ws []string, es []error) {
    57  	num := 0
    58  	for name := range c.Raw {
    59  		switch name {
    60  		case "scripts":
    61  			fallthrough
    62  		case "script":
    63  			fallthrough
    64  		case "inline":
    65  			num++
    66  		default:
    67  			es = append(es, fmt.Errorf("Unknown configuration '%s'", name))
    68  		}
    69  	}
    70  	if num != 1 {
    71  		es = append(es, fmt.Errorf("Must provide one of 'scripts', 'script' or 'inline' to remote-exec"))
    72  	}
    73  	return
    74  }
    75  
    76  // generateScript takes the configuration and creates a script to be executed
    77  // from the inline configs
    78  func (p *ResourceProvisioner) generateScript(c *terraform.ResourceConfig) (string, error) {
    79  	lines := []string{DefaultShebang}
    80  	command, ok := c.Config["inline"]
    81  	if ok {
    82  		switch cmd := command.(type) {
    83  		case string:
    84  			lines = append(lines, cmd)
    85  		case []string:
    86  			lines = append(lines, cmd...)
    87  		case []interface{}:
    88  			for _, l := range cmd {
    89  				lStr, ok := l.(string)
    90  				if ok {
    91  					lines = append(lines, lStr)
    92  				} else {
    93  					return "", fmt.Errorf("Unsupported 'inline' type! Must be string, or list of strings.")
    94  				}
    95  			}
    96  		default:
    97  			return "", fmt.Errorf("Unsupported 'inline' type! Must be string, or list of strings.")
    98  		}
    99  	}
   100  	lines = append(lines, "")
   101  	return strings.Join(lines, "\n"), nil
   102  }
   103  
   104  // collectScripts is used to collect all the scripts we need
   105  // to execute in preparation for copying them.
   106  func (p *ResourceProvisioner) collectScripts(c *terraform.ResourceConfig) ([]io.ReadCloser, error) {
   107  	// Check if inline
   108  	_, ok := c.Config["inline"]
   109  	if ok {
   110  		script, err := p.generateScript(c)
   111  		if err != nil {
   112  			return nil, err
   113  		}
   114  		rc := ioutil.NopCloser(bytes.NewReader([]byte(script)))
   115  		return []io.ReadCloser{rc}, nil
   116  	}
   117  
   118  	// Collect scripts
   119  	var scripts []string
   120  	s, ok := c.Config["script"]
   121  	if ok {
   122  		sStr, ok := s.(string)
   123  		if !ok {
   124  			return nil, fmt.Errorf("Unsupported 'script' type! Must be a string.")
   125  		}
   126  		scripts = append(scripts, sStr)
   127  	}
   128  
   129  	sl, ok := c.Config["scripts"]
   130  	if ok {
   131  		switch slt := sl.(type) {
   132  		case []string:
   133  			scripts = append(scripts, slt...)
   134  		case []interface{}:
   135  			for _, l := range slt {
   136  				lStr, ok := l.(string)
   137  				if ok {
   138  					scripts = append(scripts, lStr)
   139  				} else {
   140  					return nil, fmt.Errorf("Unsupported 'scripts' type! Must be list of strings.")
   141  				}
   142  			}
   143  		default:
   144  			return nil, fmt.Errorf("Unsupported 'scripts' type! Must be list of strings.")
   145  		}
   146  	}
   147  
   148  	// Open all the scripts
   149  	var fhs []io.ReadCloser
   150  	for _, s := range scripts {
   151  		fh, err := os.Open(s)
   152  		if err != nil {
   153  			for _, fh := range fhs {
   154  				fh.Close()
   155  			}
   156  			return nil, fmt.Errorf("Failed to open script '%s': %v", s, err)
   157  		}
   158  		fhs = append(fhs, fh)
   159  	}
   160  
   161  	// Done, return the file handles
   162  	return fhs, nil
   163  }
   164  
   165  // runScripts is used to copy and execute a set of scripts
   166  func (p *ResourceProvisioner) runScripts(
   167  	o terraform.UIOutput,
   168  	conf *helper.SSHConfig,
   169  	scripts []io.ReadCloser) error {
   170  	// Get the SSH client config
   171  	config, err := helper.PrepareConfig(conf)
   172  	if err != nil {
   173  		return err
   174  	}
   175  	defer config.CleanupConfig()
   176  
   177  	o.Output(fmt.Sprintf(
   178  		"Connecting to remote host via SSH...\n"+
   179  			"  Host: %s\n"+
   180  			"  User: %s\n"+
   181  			"  Password: %v\n"+
   182  			"  Private key: %v"+
   183  			"  SSH Agent: %v",
   184  		conf.Host, conf.User,
   185  		conf.Password != "",
   186  		conf.KeyFile != "",
   187  		conf.Agent,
   188  	))
   189  
   190  	// Wait and retry until we establish the SSH connection
   191  	var comm *helper.SSHCommunicator
   192  	err = retryFunc(conf.TimeoutVal, func() error {
   193  		host := fmt.Sprintf("%s:%d", conf.Host, conf.Port)
   194  		comm, err = helper.New(host, config)
   195  		if err != nil {
   196  			o.Output(fmt.Sprintf("Connection error, will retry: %s", err))
   197  		}
   198  
   199  		return err
   200  	})
   201  	if err != nil {
   202  		return err
   203  	}
   204  
   205  	o.Output("Connected! Executing scripts...")
   206  	for _, script := range scripts {
   207  		var cmd *helper.RemoteCmd
   208  		outR, outW := io.Pipe()
   209  		errR, errW := io.Pipe()
   210  		outDoneCh := make(chan struct{})
   211  		errDoneCh := make(chan struct{})
   212  		go p.copyOutput(o, outR, outDoneCh)
   213  		go p.copyOutput(o, errR, errDoneCh)
   214  
   215  		err := retryFunc(conf.TimeoutVal, func() error {
   216  			if err := comm.Upload(conf.ScriptPath, script); err != nil {
   217  				return fmt.Errorf("Failed to upload script: %v", err)
   218  			}
   219  			cmd = &helper.RemoteCmd{
   220  				Command: fmt.Sprintf("chmod 0777 %s", conf.ScriptPath),
   221  			}
   222  			if err := comm.Start(cmd); err != nil {
   223  				return fmt.Errorf(
   224  					"Error chmodding script file to 0777 in remote "+
   225  						"machine: %s", err)
   226  			}
   227  			cmd.Wait()
   228  
   229  			cmd = &helper.RemoteCmd{
   230  				Command: conf.ScriptPath,
   231  				Stdout:  outW,
   232  				Stderr:  errW,
   233  			}
   234  			if err := comm.Start(cmd); err != nil {
   235  				return fmt.Errorf("Error starting script: %v", err)
   236  			}
   237  			return nil
   238  		})
   239  		if err == nil {
   240  			cmd.Wait()
   241  			if cmd.ExitStatus != 0 {
   242  				err = fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus)
   243  			}
   244  		}
   245  
   246  		// Wait for output to clean up
   247  		outW.Close()
   248  		errW.Close()
   249  		<-outDoneCh
   250  		<-errDoneCh
   251  
   252  		// If we have an error, return it out now that we've cleaned up
   253  		if err != nil {
   254  			return err
   255  		}
   256  	}
   257  
   258  	return nil
   259  }
   260  
   261  func (p *ResourceProvisioner) copyOutput(
   262  	o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) {
   263  	defer close(doneCh)
   264  	lr := linereader.New(r)
   265  	for line := range lr.Ch {
   266  		o.Output(line)
   267  	}
   268  }
   269  
   270  // retryFunc is used to retry a function for a given duration
   271  func retryFunc(timeout time.Duration, f func() error) error {
   272  	finish := time.After(timeout)
   273  	for {
   274  		err := f()
   275  		if err == nil {
   276  			return nil
   277  		}
   278  		log.Printf("Retryable error: %v", err)
   279  
   280  		select {
   281  		case <-finish:
   282  			return err
   283  		case <-time.After(3 * time.Second):
   284  		}
   285  	}
   286  }