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