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 }