github.com/amanya/packer@v0.12.1-0.20161117214323-902ac5ab2eb6/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 "io" 10 "io/ioutil" 11 "log" 12 "math/rand" 13 "os" 14 "strings" 15 "time" 16 17 "github.com/mitchellh/packer/common" 18 "github.com/mitchellh/packer/helper/config" 19 "github.com/mitchellh/packer/packer" 20 "github.com/mitchellh/packer/template/interpolate" 21 ) 22 23 type Config struct { 24 common.PackerConfig `mapstructure:",squash"` 25 26 // If true, the script contains binary and line endings will not be 27 // converted from Windows to Unix-style. 28 Binary bool 29 30 // An inline script to execute. Multiple strings are all executed 31 // in the context of a single shell. 32 Inline []string 33 34 // The shebang value used when running inline scripts. 35 InlineShebang string `mapstructure:"inline_shebang"` 36 37 // The local path of the shell script to upload and execute. 38 Script string 39 40 // An array of multiple scripts to run. 41 Scripts []string 42 43 // An array of environment variables that will be injected before 44 // your command(s) are executed. 45 Vars []string `mapstructure:"environment_vars"` 46 47 // The remote folder where the local shell script will be uploaded to. 48 // This should be set to a pre-existing directory, it defaults to /tmp 49 RemoteFolder string `mapstructure:"remote_folder"` 50 51 // The remote file name of the local shell script. 52 // This defaults to script_nnn.sh 53 RemoteFile string `mapstructure:"remote_file"` 54 55 // The remote path where the local shell script will be uploaded to. 56 // This should be set to a writable file that is in a pre-existing directory. 57 // This defaults to remote_folder/remote_file 58 RemotePath string `mapstructure:"remote_path"` 59 60 // The command used to execute the script. The '{{ .Path }}' variable 61 // should be used to specify where the script goes, {{ .Vars }} 62 // can be used to inject the environment_vars into the environment. 63 ExecuteCommand string `mapstructure:"execute_command"` 64 65 // The timeout for retrying to start the process. Until this timeout 66 // is reached, if the provisioner can't start a process, it retries. 67 // This can be set high to allow for reboots. 68 RawStartRetryTimeout string `mapstructure:"start_retry_timeout"` 69 70 // Whether to clean scripts up 71 SkipClean bool `mapstructure:"skip_clean"` 72 73 ExpectDisconnect *bool `mapstructure:"expect_disconnect"` 74 75 startRetryTimeout time.Duration 76 ctx interpolate.Context 77 } 78 79 type Provisioner struct { 80 config Config 81 } 82 83 type ExecuteCommandTemplate struct { 84 Vars string 85 Path string 86 } 87 88 func (p *Provisioner) Prepare(raws ...interface{}) error { 89 err := config.Decode(&p.config, &config.DecodeOpts{ 90 Interpolate: true, 91 InterpolateContext: &p.config.ctx, 92 InterpolateFilter: &interpolate.RenderFilter{ 93 Exclude: []string{ 94 "execute_command", 95 }, 96 }, 97 }, raws...) 98 if err != nil { 99 return err 100 } 101 102 if p.config.ExecuteCommand == "" { 103 p.config.ExecuteCommand = "chmod +x {{.Path}}; {{.Vars}} {{.Path}}" 104 } 105 106 if p.config.ExpectDisconnect == nil { 107 t := true 108 p.config.ExpectDisconnect = &t 109 } 110 111 if p.config.Inline != nil && len(p.config.Inline) == 0 { 112 p.config.Inline = nil 113 } 114 115 if p.config.InlineShebang == "" { 116 p.config.InlineShebang = "/bin/sh -e" 117 } 118 119 if p.config.RawStartRetryTimeout == "" { 120 p.config.RawStartRetryTimeout = "5m" 121 } 122 123 if p.config.RemoteFolder == "" { 124 p.config.RemoteFolder = "/tmp" 125 } 126 127 if p.config.RemoteFile == "" { 128 p.config.RemoteFile = fmt.Sprintf("script_%d.sh", rand.Intn(9999)) 129 } 130 131 if p.config.RemotePath == "" { 132 p.config.RemotePath = fmt.Sprintf( 133 "%s/%s", p.config.RemoteFolder, p.config.RemoteFile) 134 } 135 136 if p.config.Scripts == nil { 137 p.config.Scripts = make([]string, 0) 138 } 139 140 if p.config.Vars == nil { 141 p.config.Vars = make([]string, 0) 142 } 143 144 var errs *packer.MultiError 145 if p.config.Script != "" && len(p.config.Scripts) > 0 { 146 errs = packer.MultiErrorAppend(errs, 147 errors.New("Only one of script or scripts can be specified.")) 148 } 149 150 if p.config.Script != "" { 151 p.config.Scripts = []string{p.config.Script} 152 } 153 154 if len(p.config.Scripts) == 0 && p.config.Inline == nil { 155 errs = packer.MultiErrorAppend(errs, 156 errors.New("Either a script file or inline script must be specified.")) 157 } else if len(p.config.Scripts) > 0 && p.config.Inline != nil { 158 errs = packer.MultiErrorAppend(errs, 159 errors.New("Only a script file or an inline script can be specified, not both.")) 160 } 161 162 for _, path := range p.config.Scripts { 163 if _, err := os.Stat(path); err != nil { 164 errs = packer.MultiErrorAppend(errs, 165 fmt.Errorf("Bad script '%s': %s", path, err)) 166 } 167 } 168 169 // Do a check for bad environment variables, such as '=foo', 'foobar' 170 for idx, kv := range p.config.Vars { 171 vs := strings.SplitN(kv, "=", 2) 172 if len(vs) != 2 || vs[0] == "" { 173 errs = packer.MultiErrorAppend(errs, 174 fmt.Errorf("Environment variable not in format 'key=value': %s", kv)) 175 } else { 176 // Replace single quotes so they parse 177 vs[1] = strings.Replace(vs[1], "'", `'"'"'`, -1) 178 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 p.config.ctx.Data = &ExecuteCommandTemplate{ 252 Vars: flattendVars, 253 Path: p.config.RemotePath, 254 } 255 command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx) 256 if err != nil { 257 return fmt.Errorf("Error processing command: %s", err) 258 } 259 260 // Upload the file and run the command. Do this in the context of 261 // a single retryable function so that we don't end up with 262 // the case that the upload succeeded, a restart is initiated, 263 // and then the command is executed but the file doesn't exist 264 // any longer. 265 var cmd *packer.RemoteCmd 266 err = p.retryable(func() error { 267 if _, err := f.Seek(0, 0); err != nil { 268 return err 269 } 270 271 var r io.Reader = f 272 if !p.config.Binary { 273 r = &UnixReader{Reader: r} 274 } 275 276 if err := comm.Upload(p.config.RemotePath, r, nil); err != nil { 277 return fmt.Errorf("Error uploading script: %s", err) 278 } 279 280 cmd = &packer.RemoteCmd{ 281 Command: fmt.Sprintf("chmod 0755 %s", p.config.RemotePath), 282 } 283 if err := comm.Start(cmd); err != nil { 284 return fmt.Errorf( 285 "Error chmodding script file to 0755 in remote "+ 286 "machine: %s", err) 287 } 288 cmd.Wait() 289 290 cmd = &packer.RemoteCmd{Command: command} 291 return cmd.StartWithUi(comm, ui) 292 }) 293 294 if err != nil { 295 return err 296 } 297 298 // If the exit code indicates a remote disconnect, fail unless 299 // we were expecting it. 300 if cmd.ExitStatus == packer.CmdDisconnect { 301 if !*p.config.ExpectDisconnect { 302 return fmt.Errorf("Script disconnected unexpectedly.") 303 } 304 } else if cmd.ExitStatus != 0 { 305 return fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus) 306 } 307 308 if !p.config.SkipClean { 309 310 // Delete the temporary file we created. We retry this a few times 311 // since if the above rebooted we have to wait until the reboot 312 // completes. 313 err = p.retryable(func() error { 314 cmd = &packer.RemoteCmd{ 315 Command: fmt.Sprintf("rm -f %s", p.config.RemotePath), 316 } 317 if err := comm.Start(cmd); err != nil { 318 return fmt.Errorf( 319 "Error removing temporary script at %s: %s", 320 p.config.RemotePath, err) 321 } 322 cmd.Wait() 323 return nil 324 }) 325 if err != nil { 326 return err 327 } 328 329 if cmd.ExitStatus != 0 { 330 return fmt.Errorf( 331 "Error removing temporary script at %s!", 332 p.config.RemotePath) 333 } 334 } 335 } 336 337 return nil 338 } 339 340 func (p *Provisioner) Cancel() { 341 // Just hard quit. It isn't a big deal if what we're doing keeps 342 // running on the other side. 343 os.Exit(0) 344 } 345 346 // retryable will retry the given function over and over until a 347 // non-error is returned. 348 func (p *Provisioner) retryable(f func() error) error { 349 startTimeout := time.After(p.config.startRetryTimeout) 350 for { 351 var err error 352 if err = f(); err == nil { 353 return nil 354 } 355 356 // Create an error and log it 357 err = fmt.Errorf("Retryable error: %s", err) 358 log.Printf(err.Error()) 359 360 // Check if we timed out, otherwise we retry. It is safe to 361 // retry since the only error case above is if the command 362 // failed to START. 363 select { 364 case <-startTimeout: 365 return err 366 default: 367 time.Sleep(2 * time.Second) 368 } 369 } 370 }