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 }