github.com/StackPointCloud/packer@v0.10.2-0.20180716202532-b28098e0f79b/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 "sort" 15 "strings" 16 "time" 17 18 "github.com/hashicorp/packer/common" 19 "github.com/hashicorp/packer/helper/config" 20 "github.com/hashicorp/packer/packer" 21 "github.com/hashicorp/packer/template/interpolate" 22 ) 23 24 type Config struct { 25 common.PackerConfig `mapstructure:",squash"` 26 27 // If true, the script contains binary and line endings will not be 28 // converted from Windows to Unix-style. 29 Binary bool 30 31 // An inline script to execute. Multiple strings are all executed 32 // in the context of a single shell. 33 Inline []string 34 35 // The shebang value used when running inline scripts. 36 InlineShebang string `mapstructure:"inline_shebang"` 37 38 // The local path of the shell script to upload and execute. 39 Script string 40 41 // An array of multiple scripts to run. 42 Scripts []string 43 44 // An array of environment variables that will be injected before 45 // your command(s) are executed. 46 Vars []string `mapstructure:"environment_vars"` 47 48 // The remote folder where the local shell script will be uploaded to. 49 // This should be set to a pre-existing directory, it defaults to /tmp 50 RemoteFolder string `mapstructure:"remote_folder"` 51 52 // The remote file name of the local shell script. 53 // This defaults to script_nnn.sh 54 RemoteFile string `mapstructure:"remote_file"` 55 56 // The remote path where the local shell script will be uploaded to. 57 // This should be set to a writable file that is in a pre-existing directory. 58 // This defaults to remote_folder/remote_file 59 RemotePath string `mapstructure:"remote_path"` 60 61 // The command used to execute the script. The '{{ .Path }}' variable 62 // should be used to specify where the script goes, {{ .Vars }} 63 // can be used to inject the environment_vars into the environment. 64 ExecuteCommand string `mapstructure:"execute_command"` 65 66 // The timeout for retrying to start the process. Until this timeout 67 // is reached, if the provisioner can't start a process, it retries. 68 // This can be set high to allow for reboots. 69 RawStartRetryTimeout string `mapstructure:"start_retry_timeout"` 70 71 // Whether to clean scripts up 72 SkipClean bool `mapstructure:"skip_clean"` 73 74 ExpectDisconnect bool `mapstructure:"expect_disconnect"` 75 76 startRetryTimeout time.Duration 77 ctx interpolate.Context 78 } 79 80 type Provisioner struct { 81 config Config 82 } 83 84 type ExecuteCommandTemplate struct { 85 Vars string 86 Path string 87 } 88 89 func (p *Provisioner) Prepare(raws ...interface{}) error { 90 err := config.Decode(&p.config, &config.DecodeOpts{ 91 Interpolate: true, 92 InterpolateContext: &p.config.ctx, 93 InterpolateFilter: &interpolate.RenderFilter{ 94 Exclude: []string{ 95 "execute_command", 96 }, 97 }, 98 }, raws...) 99 if err != nil { 100 return err 101 } 102 103 if p.config.ExecuteCommand == "" { 104 p.config.ExecuteCommand = "chmod +x {{.Path}}; {{.Vars}} {{.Path}}" 105 } 106 107 if p.config.Inline != nil && len(p.config.Inline) == 0 { 108 p.config.Inline = nil 109 } 110 111 if p.config.InlineShebang == "" { 112 p.config.InlineShebang = "/bin/sh -e" 113 } 114 115 if p.config.RawStartRetryTimeout == "" { 116 p.config.RawStartRetryTimeout = "5m" 117 } 118 119 if p.config.RemoteFolder == "" { 120 p.config.RemoteFolder = "/tmp" 121 } 122 123 if p.config.RemoteFile == "" { 124 p.config.RemoteFile = fmt.Sprintf("script_%d.sh", rand.Intn(9999)) 125 } 126 127 if p.config.RemotePath == "" { 128 p.config.RemotePath = fmt.Sprintf( 129 "%s/%s", p.config.RemoteFolder, p.config.RemoteFile) 130 } 131 132 if p.config.Scripts == nil { 133 p.config.Scripts = make([]string, 0) 134 } 135 136 if p.config.Vars == nil { 137 p.config.Vars = make([]string, 0) 138 } 139 140 var errs *packer.MultiError 141 if p.config.Script != "" && len(p.config.Scripts) > 0 { 142 errs = packer.MultiErrorAppend(errs, 143 errors.New("Only one of script or scripts can be specified.")) 144 } 145 146 if p.config.Script != "" { 147 p.config.Scripts = []string{p.config.Script} 148 } 149 150 if len(p.config.Scripts) == 0 && p.config.Inline == nil { 151 errs = packer.MultiErrorAppend(errs, 152 errors.New("Either a script file or inline script must be specified.")) 153 } else if len(p.config.Scripts) > 0 && p.config.Inline != nil { 154 errs = packer.MultiErrorAppend(errs, 155 errors.New("Only a script file or an inline script can be specified, not both.")) 156 } 157 158 for _, path := range p.config.Scripts { 159 if _, err := os.Stat(path); err != nil { 160 errs = packer.MultiErrorAppend(errs, 161 fmt.Errorf("Bad script '%s': %s", path, err)) 162 } 163 } 164 165 // Do a check for bad environment variables, such as '=foo', 'foobar' 166 for _, kv := range p.config.Vars { 167 vs := strings.SplitN(kv, "=", 2) 168 if len(vs) != 2 || vs[0] == "" { 169 errs = packer.MultiErrorAppend(errs, 170 fmt.Errorf("Environment variable not in format 'key=value': %s", kv)) 171 } 172 } 173 174 if p.config.RawStartRetryTimeout != "" { 175 p.config.startRetryTimeout, err = time.ParseDuration(p.config.RawStartRetryTimeout) 176 if err != nil { 177 errs = packer.MultiErrorAppend( 178 errs, fmt.Errorf("Failed parsing start_retry_timeout: %s", err)) 179 } 180 } 181 182 if errs != nil && len(errs.Errors) > 0 { 183 return errs 184 } 185 186 return nil 187 } 188 189 func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { 190 scripts := make([]string, len(p.config.Scripts)) 191 copy(scripts, p.config.Scripts) 192 193 // If we have an inline script, then turn that into a temporary 194 // shell script and use that. 195 if p.config.Inline != nil { 196 tf, err := ioutil.TempFile("", "packer-shell") 197 if err != nil { 198 return fmt.Errorf("Error preparing shell script: %s", err) 199 } 200 defer os.Remove(tf.Name()) 201 202 // Set the path to the temporary file 203 scripts = append(scripts, tf.Name()) 204 205 // Write our contents to it 206 writer := bufio.NewWriter(tf) 207 writer.WriteString(fmt.Sprintf("#!%s\n", p.config.InlineShebang)) 208 for _, command := range p.config.Inline { 209 if _, err := writer.WriteString(command + "\n"); err != nil { 210 return fmt.Errorf("Error preparing shell script: %s", err) 211 } 212 } 213 214 if err := writer.Flush(); err != nil { 215 return fmt.Errorf("Error preparing shell script: %s", err) 216 } 217 218 tf.Close() 219 } 220 221 // Create environment variables to set before executing the command 222 flattenedEnvVars := p.createFlattenedEnvVars() 223 224 for _, path := range scripts { 225 ui.Say(fmt.Sprintf("Provisioning with shell script: %s", path)) 226 227 log.Printf("Opening %s for reading", path) 228 f, err := os.Open(path) 229 if err != nil { 230 return fmt.Errorf("Error opening shell script: %s", err) 231 } 232 defer f.Close() 233 234 // Compile the command 235 p.config.ctx.Data = &ExecuteCommandTemplate{ 236 Vars: flattenedEnvVars, 237 Path: p.config.RemotePath, 238 } 239 command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx) 240 if err != nil { 241 return fmt.Errorf("Error processing command: %s", err) 242 } 243 244 // Upload the file and run the command. Do this in the context of 245 // a single retryable function so that we don't end up with 246 // the case that the upload succeeded, a restart is initiated, 247 // and then the command is executed but the file doesn't exist 248 // any longer. 249 var cmd *packer.RemoteCmd 250 err = p.retryable(func() error { 251 if _, err := f.Seek(0, 0); err != nil { 252 return err 253 } 254 255 var r io.Reader = f 256 if !p.config.Binary { 257 r = &UnixReader{Reader: r} 258 } 259 260 if err := comm.Upload(p.config.RemotePath, r, nil); err != nil { 261 return fmt.Errorf("Error uploading script: %s", err) 262 } 263 264 cmd = &packer.RemoteCmd{ 265 Command: fmt.Sprintf("chmod 0755 %s", p.config.RemotePath), 266 } 267 if err := comm.Start(cmd); err != nil { 268 return fmt.Errorf( 269 "Error chmodding script file to 0755 in remote "+ 270 "machine: %s", err) 271 } 272 cmd.Wait() 273 274 cmd = &packer.RemoteCmd{Command: command} 275 return cmd.StartWithUi(comm, ui) 276 }) 277 278 if err != nil { 279 return err 280 } 281 282 // If the exit code indicates a remote disconnect, fail unless 283 // we were expecting it. 284 if cmd.ExitStatus == packer.CmdDisconnect { 285 if !p.config.ExpectDisconnect { 286 return fmt.Errorf("Script disconnected unexpectedly. " + 287 "If you expected your script to disconnect, i.e. from a " + 288 "restart, you can try adding `\"expect_disconnect\": true` " + 289 "to the shell provisioner parameters.") 290 } 291 } else if cmd.ExitStatus != 0 { 292 return fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus) 293 } 294 295 if !p.config.SkipClean { 296 297 // Delete the temporary file we created. We retry this a few times 298 // since if the above rebooted we have to wait until the reboot 299 // completes. 300 err = p.retryable(func() error { 301 cmd = &packer.RemoteCmd{ 302 Command: fmt.Sprintf("rm -f %s", p.config.RemotePath), 303 } 304 if err := comm.Start(cmd); err != nil { 305 return fmt.Errorf( 306 "Error removing temporary script at %s: %s", 307 p.config.RemotePath, err) 308 } 309 cmd.Wait() 310 // treat disconnects as retryable by returning an error 311 if cmd.ExitStatus == packer.CmdDisconnect { 312 return fmt.Errorf("Disconnect while removing temporary script.") 313 } 314 return nil 315 }) 316 if err != nil { 317 return err 318 } 319 320 if cmd.ExitStatus != 0 { 321 return fmt.Errorf( 322 "Error removing temporary script at %s!", 323 p.config.RemotePath) 324 } 325 } 326 } 327 328 return nil 329 } 330 331 func (p *Provisioner) Cancel() { 332 // Just hard quit. It isn't a big deal if what we're doing keeps 333 // running on the other side. 334 os.Exit(0) 335 } 336 337 // retryable will retry the given function over and over until a 338 // non-error is returned. 339 func (p *Provisioner) retryable(f func() error) error { 340 startTimeout := time.After(p.config.startRetryTimeout) 341 for { 342 var err error 343 if err = f(); err == nil { 344 return nil 345 } 346 347 // Create an error and log it 348 err = fmt.Errorf("Retryable error: %s", err) 349 log.Print(err.Error()) 350 351 // Check if we timed out, otherwise we retry. It is safe to 352 // retry since the only error case above is if the command 353 // failed to START. 354 select { 355 case <-startTimeout: 356 return err 357 default: 358 time.Sleep(2 * time.Second) 359 } 360 } 361 } 362 363 func (p *Provisioner) createFlattenedEnvVars() (flattened string) { 364 flattened = "" 365 envVars := make(map[string]string) 366 367 // Always available Packer provided env vars 368 envVars["PACKER_BUILD_NAME"] = fmt.Sprintf("%s", p.config.PackerBuildName) 369 envVars["PACKER_BUILDER_TYPE"] = fmt.Sprintf("%s", p.config.PackerBuilderType) 370 httpAddr := common.GetHTTPAddr() 371 if httpAddr != "" { 372 envVars["PACKER_HTTP_ADDR"] = fmt.Sprintf("%s", httpAddr) 373 } 374 375 // Split vars into key/value components 376 for _, envVar := range p.config.Vars { 377 keyValue := strings.SplitN(envVar, "=", 2) 378 // Store pair, replacing any single quotes in value so they parse 379 // correctly with required environment variable format 380 envVars[keyValue[0]] = strings.Replace(keyValue[1], "'", `'"'"'`, -1) 381 } 382 383 // Create a list of env var keys in sorted order 384 var keys []string 385 for k := range envVars { 386 keys = append(keys, k) 387 } 388 sort.Strings(keys) 389 390 // Re-assemble vars surrounding value with single quotes and flatten 391 for _, key := range keys { 392 flattened += fmt.Sprintf("%s='%s' ", key, envVars[key]) 393 } 394 return 395 }