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 }