github.com/chalford/terraform@v0.3.7-0.20150113080010-a78c69a8c81f/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  
   176  	o.Output(fmt.Sprintf(
   177  		"Connecting to remote host via SSH...\n"+
   178  			"  Host: %s\n"+
   179  			"  User: %s\n"+
   180  			"  Password: %v\n"+
   181  			"  Private key: %v",
   182  		conf.Host, conf.User,
   183  		conf.Password != "",
   184  		conf.KeyFile != ""))
   185  
   186  	// Wait and retry until we establish the SSH connection
   187  	var comm *helper.SSHCommunicator
   188  	err = retryFunc(conf.TimeoutVal, func() error {
   189  		host := fmt.Sprintf("%s:%d", conf.Host, conf.Port)
   190  		comm, err = helper.New(host, config)
   191  		if err != nil {
   192  			o.Output(fmt.Sprintf("Connection error, will retry: %s", err))
   193  		}
   194  
   195  		return err
   196  	})
   197  	if err != nil {
   198  		return err
   199  	}
   200  
   201  	o.Output("Connected! Executing scripts...")
   202  	for _, script := range scripts {
   203  		var cmd *helper.RemoteCmd
   204  		outR, outW := io.Pipe()
   205  		errR, errW := io.Pipe()
   206  		outDoneCh := make(chan struct{})
   207  		errDoneCh := make(chan struct{})
   208  		go p.copyOutput(o, outR, outDoneCh)
   209  		go p.copyOutput(o, errR, errDoneCh)
   210  
   211  		err := retryFunc(conf.TimeoutVal, func() error {
   212  			if err := comm.Upload(conf.ScriptPath, script); err != nil {
   213  				return fmt.Errorf("Failed to upload script: %v", err)
   214  			}
   215  			cmd = &helper.RemoteCmd{
   216  				Command: fmt.Sprintf("chmod 0777 %s", conf.ScriptPath),
   217  			}
   218  			if err := comm.Start(cmd); err != nil {
   219  				return fmt.Errorf(
   220  					"Error chmodding script file to 0777 in remote "+
   221  						"machine: %s", err)
   222  			}
   223  			cmd.Wait()
   224  
   225  			cmd = &helper.RemoteCmd{
   226  				Command: conf.ScriptPath,
   227  				Stdout:  outW,
   228  				Stderr:  errW,
   229  			}
   230  			if err := comm.Start(cmd); err != nil {
   231  				return fmt.Errorf("Error starting script: %v", err)
   232  			}
   233  			return nil
   234  		})
   235  		if err == nil {
   236  			cmd.Wait()
   237  			if cmd.ExitStatus != 0 {
   238  				err = fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus)
   239  			}
   240  		}
   241  
   242  		// Wait for output to clean up
   243  		outW.Close()
   244  		errW.Close()
   245  		<-outDoneCh
   246  		<-errDoneCh
   247  
   248  		// If we have an error, return it out now that we've cleaned up
   249  		if err != nil {
   250  			return err
   251  		}
   252  	}
   253  
   254  	return nil
   255  }
   256  
   257  func (p *ResourceProvisioner) copyOutput(
   258  	o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) {
   259  	defer close(doneCh)
   260  	lr := linereader.New(r)
   261  	for line := range lr.Ch {
   262  		o.Output(line)
   263  	}
   264  }
   265  
   266  // retryFunc is used to retry a function for a given duration
   267  func retryFunc(timeout time.Duration, f func() error) error {
   268  	finish := time.After(timeout)
   269  	for {
   270  		err := f()
   271  		if err == nil {
   272  			return nil
   273  		}
   274  		log.Printf("Retryable error: %v", err)
   275  
   276  		select {
   277  		case <-finish:
   278  			return err
   279  		case <-time.After(3 * time.Second):
   280  		}
   281  	}
   282  }