github.com/mmcquillan/packer@v1.1.1-0.20171009221028-c85cf0483a5d/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.ExpectDisconnect == nil { 108 t := false 109 p.config.ExpectDisconnect = &t 110 } 111 112 if p.config.Inline != nil && len(p.config.Inline) == 0 { 113 p.config.Inline = nil 114 } 115 116 if p.config.InlineShebang == "" { 117 p.config.InlineShebang = "/bin/sh -e" 118 } 119 120 if p.config.RawStartRetryTimeout == "" { 121 p.config.RawStartRetryTimeout = "5m" 122 } 123 124 if p.config.RemoteFolder == "" { 125 p.config.RemoteFolder = "/tmp" 126 } 127 128 if p.config.RemoteFile == "" { 129 p.config.RemoteFile = fmt.Sprintf("script_%d.sh", rand.Intn(9999)) 130 } 131 132 if p.config.RemotePath == "" { 133 p.config.RemotePath = fmt.Sprintf( 134 "%s/%s", p.config.RemoteFolder, p.config.RemoteFile) 135 } 136 137 if p.config.Scripts == nil { 138 p.config.Scripts = make([]string, 0) 139 } 140 141 if p.config.Vars == nil { 142 p.config.Vars = make([]string, 0) 143 } 144 145 var errs *packer.MultiError 146 if p.config.Script != "" && len(p.config.Scripts) > 0 { 147 errs = packer.MultiErrorAppend(errs, 148 errors.New("Only one of script or scripts can be specified.")) 149 } 150 151 if p.config.Script != "" { 152 p.config.Scripts = []string{p.config.Script} 153 } 154 155 if len(p.config.Scripts) == 0 && p.config.Inline == nil { 156 errs = packer.MultiErrorAppend(errs, 157 errors.New("Either a script file or inline script must be specified.")) 158 } else if len(p.config.Scripts) > 0 && p.config.Inline != nil { 159 errs = packer.MultiErrorAppend(errs, 160 errors.New("Only a script file or an inline script can be specified, not both.")) 161 } 162 163 for _, path := range p.config.Scripts { 164 if _, err := os.Stat(path); err != nil { 165 errs = packer.MultiErrorAppend(errs, 166 fmt.Errorf("Bad script '%s': %s", path, err)) 167 } 168 } 169 170 // Do a check for bad environment variables, such as '=foo', 'foobar' 171 for _, kv := range p.config.Vars { 172 vs := strings.SplitN(kv, "=", 2) 173 if len(vs) != 2 || vs[0] == "" { 174 errs = packer.MultiErrorAppend(errs, 175 fmt.Errorf("Environment variable not in format 'key=value': %s", kv)) 176 } 177 } 178 179 if p.config.RawStartRetryTimeout != "" { 180 p.config.startRetryTimeout, err = time.ParseDuration(p.config.RawStartRetryTimeout) 181 if err != nil { 182 errs = packer.MultiErrorAppend( 183 errs, fmt.Errorf("Failed parsing start_retry_timeout: %s", err)) 184 } 185 } 186 187 if errs != nil && len(errs.Errors) > 0 { 188 return errs 189 } 190 191 return nil 192 } 193 194 func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { 195 scripts := make([]string, len(p.config.Scripts)) 196 copy(scripts, p.config.Scripts) 197 198 // If we have an inline script, then turn that into a temporary 199 // shell script and use that. 200 if p.config.Inline != nil { 201 tf, err := ioutil.TempFile("", "packer-shell") 202 if err != nil { 203 return fmt.Errorf("Error preparing shell script: %s", err) 204 } 205 defer os.Remove(tf.Name()) 206 207 // Set the path to the temporary file 208 scripts = append(scripts, tf.Name()) 209 210 // Write our contents to it 211 writer := bufio.NewWriter(tf) 212 writer.WriteString(fmt.Sprintf("#!%s\n", p.config.InlineShebang)) 213 for _, command := range p.config.Inline { 214 if _, err := writer.WriteString(command + "\n"); err != nil { 215 return fmt.Errorf("Error preparing shell script: %s", err) 216 } 217 } 218 219 if err := writer.Flush(); err != nil { 220 return fmt.Errorf("Error preparing shell script: %s", err) 221 } 222 223 tf.Close() 224 } 225 226 // Create environment variables to set before executing the command 227 flattenedEnvVars := p.createFlattenedEnvVars() 228 229 for _, path := range scripts { 230 ui.Say(fmt.Sprintf("Provisioning with shell script: %s", path)) 231 232 log.Printf("Opening %s for reading", path) 233 f, err := os.Open(path) 234 if err != nil { 235 return fmt.Errorf("Error opening shell script: %s", err) 236 } 237 defer f.Close() 238 239 // Compile the command 240 p.config.ctx.Data = &ExecuteCommandTemplate{ 241 Vars: flattenedEnvVars, 242 Path: p.config.RemotePath, 243 } 244 command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx) 245 if err != nil { 246 return fmt.Errorf("Error processing command: %s", err) 247 } 248 249 // Upload the file and run the command. Do this in the context of 250 // a single retryable function so that we don't end up with 251 // the case that the upload succeeded, a restart is initiated, 252 // and then the command is executed but the file doesn't exist 253 // any longer. 254 var cmd *packer.RemoteCmd 255 err = p.retryable(func() error { 256 if _, err := f.Seek(0, 0); err != nil { 257 return err 258 } 259 260 var r io.Reader = f 261 if !p.config.Binary { 262 r = &UnixReader{Reader: r} 263 } 264 265 if err := comm.Upload(p.config.RemotePath, r, nil); err != nil { 266 return fmt.Errorf("Error uploading script: %s", err) 267 } 268 269 cmd = &packer.RemoteCmd{ 270 Command: fmt.Sprintf("chmod 0755 %s", p.config.RemotePath), 271 } 272 if err := comm.Start(cmd); err != nil { 273 return fmt.Errorf( 274 "Error chmodding script file to 0755 in remote "+ 275 "machine: %s", err) 276 } 277 cmd.Wait() 278 279 cmd = &packer.RemoteCmd{Command: command} 280 return cmd.StartWithUi(comm, ui) 281 }) 282 283 if err != nil { 284 return err 285 } 286 287 // If the exit code indicates a remote disconnect, fail unless 288 // we were expecting it. 289 if cmd.ExitStatus == packer.CmdDisconnect { 290 if !*p.config.ExpectDisconnect { 291 return fmt.Errorf("Script disconnected unexpectedly.") 292 } 293 } else if cmd.ExitStatus != 0 { 294 return fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus) 295 } 296 297 if !p.config.SkipClean { 298 299 // Delete the temporary file we created. We retry this a few times 300 // since if the above rebooted we have to wait until the reboot 301 // completes. 302 err = p.retryable(func() error { 303 cmd = &packer.RemoteCmd{ 304 Command: fmt.Sprintf("rm -f %s", p.config.RemotePath), 305 } 306 if err := comm.Start(cmd); err != nil { 307 return fmt.Errorf( 308 "Error removing temporary script at %s: %s", 309 p.config.RemotePath, err) 310 } 311 cmd.Wait() 312 // treat disconnects as retryable by returning an error 313 if cmd.ExitStatus == packer.CmdDisconnect { 314 return fmt.Errorf("Disconnect while removing temporary script.") 315 } 316 return nil 317 }) 318 if err != nil { 319 return err 320 } 321 322 if cmd.ExitStatus != 0 { 323 return fmt.Errorf( 324 "Error removing temporary script at %s!", 325 p.config.RemotePath) 326 } 327 } 328 } 329 330 return nil 331 } 332 333 func (p *Provisioner) Cancel() { 334 // Just hard quit. It isn't a big deal if what we're doing keeps 335 // running on the other side. 336 os.Exit(0) 337 } 338 339 // retryable will retry the given function over and over until a 340 // non-error is returned. 341 func (p *Provisioner) retryable(f func() error) error { 342 startTimeout := time.After(p.config.startRetryTimeout) 343 for { 344 var err error 345 if err = f(); err == nil { 346 return nil 347 } 348 349 // Create an error and log it 350 err = fmt.Errorf("Retryable error: %s", err) 351 log.Print(err.Error()) 352 353 // Check if we timed out, otherwise we retry. It is safe to 354 // retry since the only error case above is if the command 355 // failed to START. 356 select { 357 case <-startTimeout: 358 return err 359 default: 360 time.Sleep(2 * time.Second) 361 } 362 } 363 } 364 365 func (p *Provisioner) createFlattenedEnvVars() (flattened string) { 366 flattened = "" 367 envVars := make(map[string]string) 368 369 // Always available Packer provided env vars 370 envVars["PACKER_BUILD_NAME"] = fmt.Sprintf("%s", p.config.PackerBuildName) 371 envVars["PACKER_BUILDER_TYPE"] = fmt.Sprintf("%s", p.config.PackerBuilderType) 372 httpAddr := common.GetHTTPAddr() 373 if httpAddr != "" { 374 envVars["PACKER_HTTP_ADDR"] = fmt.Sprintf("%s", httpAddr) 375 } 376 377 // Split vars into key/value components 378 for _, envVar := range p.config.Vars { 379 keyValue := strings.SplitN(envVar, "=", 2) 380 // Store pair, replacing any single quotes in value so they parse 381 // correctly with required environment variable format 382 envVars[keyValue[0]] = strings.Replace(keyValue[1], "'", `'"'"'`, -1) 383 } 384 385 // Create a list of env var keys in sorted order 386 var keys []string 387 for k := range envVars { 388 keys = append(keys, k) 389 } 390 sort.Strings(keys) 391 392 // Re-assemble vars surrounding value with single quotes and flatten 393 for _, key := range keys { 394 flattened += fmt.Sprintf("%s='%s' ", key, envVars[key]) 395 } 396 return 397 }