github.com/ezbercih/terraform@v0.1.1-0.20140729011846-3c33865e0839/builtin/provisioners/remote-exec/resource_provisioner.go (about)

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