github.com/jbronn/packer@v0.1.6-0.20140120165540-8a1364dbd817/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 _, kv := range p.config.Vars { 174 vs := strings.Split(kv, "=") 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 } 179 } 180 181 if p.config.RawStartRetryTimeout != "" { 182 p.config.startRetryTimeout, err = time.ParseDuration(p.config.RawStartRetryTimeout) 183 if err != nil { 184 errs = packer.MultiErrorAppend( 185 errs, fmt.Errorf("Failed parsing start_retry_timeout: %s", err)) 186 } 187 } 188 189 if errs != nil && len(errs.Errors) > 0 { 190 return errs 191 } 192 193 return nil 194 } 195 196 func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { 197 scripts := make([]string, len(p.config.Scripts)) 198 copy(scripts, p.config.Scripts) 199 200 // If we have an inline script, then turn that into a temporary 201 // shell script and use that. 202 if p.config.Inline != nil { 203 tf, err := ioutil.TempFile("", "packer-shell") 204 if err != nil { 205 return fmt.Errorf("Error preparing shell script: %s", err) 206 } 207 defer os.Remove(tf.Name()) 208 209 // Set the path to the temporary file 210 scripts = append(scripts, tf.Name()) 211 212 // Write our contents to it 213 writer := bufio.NewWriter(tf) 214 writer.WriteString(fmt.Sprintf("#!%s\n", p.config.InlineShebang)) 215 for _, command := range p.config.Inline { 216 if _, err := writer.WriteString(command + "\n"); err != nil { 217 return fmt.Errorf("Error preparing shell script: %s", err) 218 } 219 } 220 221 if err := writer.Flush(); err != nil { 222 return fmt.Errorf("Error preparing shell script: %s", err) 223 } 224 225 tf.Close() 226 } 227 228 // Build our variables up by adding in the build name and builder type 229 envVars := make([]string, len(p.config.Vars)+2) 230 envVars[0] = "PACKER_BUILD_NAME=" + p.config.PackerBuildName 231 envVars[1] = "PACKER_BUILDER_TYPE=" + p.config.PackerBuilderType 232 copy(envVars[2:], p.config.Vars) 233 234 for _, path := range scripts { 235 ui.Say(fmt.Sprintf("Provisioning with shell script: %s", path)) 236 237 log.Printf("Opening %s for reading", path) 238 f, err := os.Open(path) 239 if err != nil { 240 return fmt.Errorf("Error opening shell script: %s", err) 241 } 242 defer f.Close() 243 244 // Flatten the environment variables 245 flattendVars := strings.Join(envVars, " ") 246 247 // Compile the command 248 command, err := p.config.tpl.Process(p.config.ExecuteCommand, &ExecuteCommandTemplate{ 249 Vars: flattendVars, 250 Path: p.config.RemotePath, 251 }) 252 if err != nil { 253 return fmt.Errorf("Error processing command: %s", err) 254 } 255 256 // Upload the file and run the command. Do this in the context of 257 // a single retryable function so that we don't end up with 258 // the case that the upload succeeded, a restart is initiated, 259 // and then the command is executed but the file doesn't exist 260 // any longer. 261 var cmd *packer.RemoteCmd 262 err = p.retryable(func() error { 263 if _, err := f.Seek(0, 0); err != nil { 264 return err 265 } 266 267 var r io.Reader = f 268 if !p.config.Binary { 269 r = &UnixReader{Reader: r} 270 } 271 272 if err := comm.Upload(p.config.RemotePath, r); err != nil { 273 return fmt.Errorf("Error uploading script: %s", err) 274 } 275 276 cmd = &packer.RemoteCmd{Command: command} 277 return cmd.StartWithUi(comm, ui) 278 }) 279 if err != nil { 280 return err 281 } 282 283 // Close the original file since we copied it 284 f.Close() 285 286 if cmd.ExitStatus != 0 { 287 return fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus) 288 } 289 } 290 291 return nil 292 } 293 294 func (p *Provisioner) Cancel() { 295 // Just hard quit. It isn't a big deal if what we're doing keeps 296 // running on the other side. 297 os.Exit(0) 298 } 299 300 // retryable will retry the given function over and over until a 301 // non-error is returned. 302 func (p *Provisioner) retryable(f func() error) error { 303 startTimeout := time.After(p.config.startRetryTimeout) 304 for { 305 var err error 306 if err = f(); err == nil { 307 return nil 308 } 309 310 // Create an error and log it 311 err = fmt.Errorf("Retryable error: %s", err) 312 log.Printf(err.Error()) 313 314 // Check if we timed out, otherwise we retry. It is safe to 315 // retry since the only error case above is if the command 316 // failed to START. 317 select { 318 case <-startTimeout: 319 return err 320 default: 321 time.Sleep(2 * time.Second) 322 } 323 } 324 }