github.com/daniellockard/packer@v0.7.6-0.20141210173435-5a9390934716/provisioner/shell/provisioner.go (about) 1 // This package implements a provisioner for Packer that executes 2 // shell scripts within the remote machine. 3 package shell 4 5 import ( 6 "bufio" 7 "errors" 8 "fmt" 9 "github.com/mitchellh/packer/common" 10 "github.com/mitchellh/packer/packer" 11 "io" 12 "io/ioutil" 13 "log" 14 "os" 15 "strings" 16 "time" 17 ) 18 19 const DefaultRemotePath = "/tmp/script.sh" 20 21 type config struct { 22 common.PackerConfig `mapstructure:",squash"` 23 24 // If true, the script contains binary and line endings will not be 25 // converted from Windows to Unix-style. 26 Binary bool 27 28 // An inline script to execute. Multiple strings are all executed 29 // in the context of a single shell. 30 Inline []string 31 32 // The shebang value used when running inline scripts. 33 InlineShebang string `mapstructure:"inline_shebang"` 34 35 // The local path of the shell script to upload and execute. 36 Script string 37 38 // An array of multiple scripts to run. 39 Scripts []string 40 41 // An array of environment variables that will be injected before 42 // your command(s) are executed. 43 Vars []string `mapstructure:"environment_vars"` 44 45 // The remote path where the local shell script will be uploaded to. 46 // This should be set to a writable file that is in a pre-existing directory. 47 RemotePath string `mapstructure:"remote_path"` 48 49 // The command used to execute the script. The '{{ .Path }}' variable 50 // should be used to specify where the script goes, {{ .Vars }} 51 // can be used to inject the environment_vars into the environment. 52 ExecuteCommand string `mapstructure:"execute_command"` 53 54 // The timeout for retrying to start the process. Until this timeout 55 // is reached, if the provisioner can't start a process, it retries. 56 // This can be set high to allow for reboots. 57 RawStartRetryTimeout string `mapstructure:"start_retry_timeout"` 58 59 startRetryTimeout time.Duration 60 tpl *packer.ConfigTemplate 61 } 62 63 type Provisioner struct { 64 config config 65 } 66 67 type ExecuteCommandTemplate struct { 68 Vars string 69 Path string 70 } 71 72 func (p *Provisioner) Prepare(raws ...interface{}) error { 73 md, err := common.DecodeConfig(&p.config, raws...) 74 if err != nil { 75 return err 76 } 77 78 p.config.tpl, err = packer.NewConfigTemplate() 79 if err != nil { 80 return err 81 } 82 p.config.tpl.UserVars = p.config.PackerUserVars 83 84 // Accumulate any errors 85 errs := common.CheckUnusedConfig(md) 86 87 if p.config.ExecuteCommand == "" { 88 p.config.ExecuteCommand = "chmod +x {{.Path}}; {{.Vars}} {{.Path}}" 89 } 90 91 if p.config.Inline != nil && len(p.config.Inline) == 0 { 92 p.config.Inline = nil 93 } 94 95 if p.config.InlineShebang == "" { 96 p.config.InlineShebang = "/bin/sh" 97 } 98 99 if p.config.RawStartRetryTimeout == "" { 100 p.config.RawStartRetryTimeout = "5m" 101 } 102 103 if p.config.RemotePath == "" { 104 p.config.RemotePath = DefaultRemotePath 105 } 106 107 if p.config.Scripts == nil { 108 p.config.Scripts = make([]string, 0) 109 } 110 111 if p.config.Vars == nil { 112 p.config.Vars = make([]string, 0) 113 } 114 115 if p.config.Script != "" && len(p.config.Scripts) > 0 { 116 errs = packer.MultiErrorAppend(errs, 117 errors.New("Only one of script or scripts can be specified.")) 118 } 119 120 if p.config.Script != "" { 121 p.config.Scripts = []string{p.config.Script} 122 } 123 124 templates := map[string]*string{ 125 "inline_shebang": &p.config.InlineShebang, 126 "script": &p.config.Script, 127 "start_retry_timeout": &p.config.RawStartRetryTimeout, 128 "remote_path": &p.config.RemotePath, 129 } 130 131 for n, ptr := range templates { 132 var err error 133 *ptr, err = p.config.tpl.Process(*ptr, nil) 134 if err != nil { 135 errs = packer.MultiErrorAppend( 136 errs, fmt.Errorf("Error processing %s: %s", n, err)) 137 } 138 } 139 140 sliceTemplates := map[string][]string{ 141 "inline": p.config.Inline, 142 "scripts": p.config.Scripts, 143 "environment_vars": p.config.Vars, 144 } 145 146 for n, slice := range sliceTemplates { 147 for i, elem := range slice { 148 var err error 149 slice[i], err = p.config.tpl.Process(elem, nil) 150 if err != nil { 151 errs = packer.MultiErrorAppend( 152 errs, fmt.Errorf("Error processing %s[%d]: %s", n, i, err)) 153 } 154 } 155 } 156 157 if len(p.config.Scripts) == 0 && p.config.Inline == nil { 158 errs = packer.MultiErrorAppend(errs, 159 errors.New("Either a script file or inline script must be specified.")) 160 } else if len(p.config.Scripts) > 0 && p.config.Inline != nil { 161 errs = packer.MultiErrorAppend(errs, 162 errors.New("Only a script file or an inline script can be specified, not both.")) 163 } 164 165 for _, path := range p.config.Scripts { 166 if _, err := os.Stat(path); err != nil { 167 errs = packer.MultiErrorAppend(errs, 168 fmt.Errorf("Bad script '%s': %s", path, err)) 169 } 170 } 171 172 // Do a check for bad environment variables, such as '=foo', 'foobar' 173 for idx, kv := range p.config.Vars { 174 vs := strings.SplitN(kv, "=", 2) 175 if len(vs) != 2 || vs[0] == "" { 176 errs = packer.MultiErrorAppend(errs, 177 fmt.Errorf("Environment variable not in format 'key=value': %s", kv)) 178 } else { 179 // Single quote env var values 180 p.config.Vars[idx] = fmt.Sprintf("%s='%s'", vs[0], vs[1]) 181 } 182 } 183 184 if p.config.RawStartRetryTimeout != "" { 185 p.config.startRetryTimeout, err = time.ParseDuration(p.config.RawStartRetryTimeout) 186 if err != nil { 187 errs = packer.MultiErrorAppend( 188 errs, fmt.Errorf("Failed parsing start_retry_timeout: %s", err)) 189 } 190 } 191 192 if errs != nil && len(errs.Errors) > 0 { 193 return errs 194 } 195 196 return nil 197 } 198 199 func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { 200 scripts := make([]string, len(p.config.Scripts)) 201 copy(scripts, p.config.Scripts) 202 203 // If we have an inline script, then turn that into a temporary 204 // shell script and use that. 205 if p.config.Inline != nil { 206 tf, err := ioutil.TempFile("", "packer-shell") 207 if err != nil { 208 return fmt.Errorf("Error preparing shell script: %s", err) 209 } 210 defer os.Remove(tf.Name()) 211 212 // Set the path to the temporary file 213 scripts = append(scripts, tf.Name()) 214 215 // Write our contents to it 216 writer := bufio.NewWriter(tf) 217 writer.WriteString(fmt.Sprintf("#!%s\n", p.config.InlineShebang)) 218 for _, command := range p.config.Inline { 219 if _, err := writer.WriteString(command + "\n"); err != nil { 220 return fmt.Errorf("Error preparing shell script: %s", err) 221 } 222 } 223 224 if err := writer.Flush(); err != nil { 225 return fmt.Errorf("Error preparing shell script: %s", err) 226 } 227 228 tf.Close() 229 } 230 231 // Build our variables up by adding in the build name and builder type 232 envVars := make([]string, len(p.config.Vars)+2) 233 envVars[0] = fmt.Sprintf("PACKER_BUILD_NAME='%s'", p.config.PackerBuildName) 234 envVars[1] = fmt.Sprintf("PACKER_BUILDER_TYPE='%s'", p.config.PackerBuilderType) 235 copy(envVars[2:], p.config.Vars) 236 237 for _, path := range scripts { 238 ui.Say(fmt.Sprintf("Provisioning with shell script: %s", path)) 239 240 log.Printf("Opening %s for reading", path) 241 f, err := os.Open(path) 242 if err != nil { 243 return fmt.Errorf("Error opening shell script: %s", err) 244 } 245 defer f.Close() 246 247 // Flatten the environment variables 248 flattendVars := strings.Join(envVars, " ") 249 250 // Compile the command 251 command, err := p.config.tpl.Process(p.config.ExecuteCommand, &ExecuteCommandTemplate{ 252 Vars: flattendVars, 253 Path: p.config.RemotePath, 254 }) 255 if err != nil { 256 return fmt.Errorf("Error processing command: %s", err) 257 } 258 259 // Upload the file and run the command. Do this in the context of 260 // a single retryable function so that we don't end up with 261 // the case that the upload succeeded, a restart is initiated, 262 // and then the command is executed but the file doesn't exist 263 // any longer. 264 var cmd *packer.RemoteCmd 265 err = p.retryable(func() error { 266 if _, err := f.Seek(0, 0); err != nil { 267 return err 268 } 269 270 var r io.Reader = f 271 if !p.config.Binary { 272 r = &UnixReader{Reader: r} 273 } 274 275 if err := comm.Upload(p.config.RemotePath, r, nil); err != nil { 276 return fmt.Errorf("Error uploading script: %s", err) 277 } 278 279 cmd = &packer.RemoteCmd{ 280 Command: fmt.Sprintf("chmod 0777 %s", p.config.RemotePath), 281 } 282 if err := comm.Start(cmd); err != nil { 283 return fmt.Errorf( 284 "Error chmodding script file to 0777 in remote "+ 285 "machine: %s", err) 286 } 287 cmd.Wait() 288 289 cmd = &packer.RemoteCmd{Command: command} 290 return cmd.StartWithUi(comm, ui) 291 }) 292 if err != nil { 293 return err 294 } 295 296 // Close the original file since we copied it 297 f.Close() 298 299 if cmd.ExitStatus != 0 { 300 return fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus) 301 } 302 } 303 304 return nil 305 } 306 307 func (p *Provisioner) Cancel() { 308 // Just hard quit. It isn't a big deal if what we're doing keeps 309 // running on the other side. 310 os.Exit(0) 311 } 312 313 // retryable will retry the given function over and over until a 314 // non-error is returned. 315 func (p *Provisioner) retryable(f func() error) error { 316 startTimeout := time.After(p.config.startRetryTimeout) 317 for { 318 var err error 319 if err = f(); err == nil { 320 return nil 321 } 322 323 // Create an error and log it 324 err = fmt.Errorf("Retryable error: %s", err) 325 log.Printf(err.Error()) 326 327 // Check if we timed out, otherwise we retry. It is safe to 328 // retry since the only error case above is if the command 329 // failed to START. 330 select { 331 case <-startTimeout: 332 return err 333 default: 334 time.Sleep(2 * time.Second) 335 } 336 } 337 }