github.com/phobos182/packer@v0.2.3-0.20130819023704-c84d2aeffc68/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/ioutil" 12 "log" 13 "os" 14 "strings" 15 "time" 16 ) 17 18 const DefaultRemotePath = "/tmp/script.sh" 19 20 type config struct { 21 common.PackerConfig `mapstructure:",squash"` 22 23 // An inline script to execute. Multiple strings are all executed 24 // in the context of a single shell. 25 Inline []string 26 27 // The shebang value used when running inline scripts. 28 InlineShebang string `mapstructure:"inline_shebang"` 29 30 // The local path of the shell script to upload and execute. 31 Script string 32 33 // An array of multiple scripts to run. 34 Scripts []string 35 36 // An array of environment variables that will be injected before 37 // your command(s) are executed. 38 Vars []string `mapstructure:"environment_vars"` 39 40 // The remote path where the local shell script will be uploaded to. 41 // This should be set to a writable file that is in a pre-existing directory. 42 RemotePath string `mapstructure:"remote_path"` 43 44 // The command used to execute the script. The '{{ .Path }}' variable 45 // should be used to specify where the script goes, {{ .Vars }} 46 // can be used to inject the environment_vars into the environment. 47 ExecuteCommand string `mapstructure:"execute_command"` 48 49 // The timeout for retrying to start the process. Until this timeout 50 // is reached, if the provisioner can't start a process, it retries. 51 // This can be set high to allow for reboots. 52 RawStartRetryTimeout string `mapstructure:"start_retry_timeout"` 53 54 startRetryTimeout time.Duration 55 tpl *packer.ConfigTemplate 56 } 57 58 type Provisioner struct { 59 config config 60 } 61 62 type ExecuteCommandTemplate struct { 63 Vars string 64 Path string 65 } 66 67 func (p *Provisioner) Prepare(raws ...interface{}) error { 68 md, err := common.DecodeConfig(&p.config, raws...) 69 if err != nil { 70 return err 71 } 72 73 p.config.tpl, err = packer.NewConfigTemplate() 74 if err != nil { 75 return err 76 } 77 p.config.tpl.UserVars = p.config.PackerUserVars 78 79 // Accumulate any errors 80 errs := common.CheckUnusedConfig(md) 81 82 if p.config.ExecuteCommand == "" { 83 p.config.ExecuteCommand = "chmod +x {{.Path}}; {{.Vars}} {{.Path}}" 84 } 85 86 if p.config.Inline != nil && len(p.config.Inline) == 0 { 87 p.config.Inline = nil 88 } 89 90 if p.config.InlineShebang == "" { 91 p.config.InlineShebang = "/bin/sh" 92 } 93 94 if p.config.RawStartRetryTimeout == "" { 95 p.config.RawStartRetryTimeout = "5m" 96 } 97 98 if p.config.RemotePath == "" { 99 p.config.RemotePath = DefaultRemotePath 100 } 101 102 if p.config.Scripts == nil { 103 p.config.Scripts = make([]string, 0) 104 } 105 106 if p.config.Vars == nil { 107 p.config.Vars = make([]string, 0) 108 } 109 110 if p.config.Script != "" && len(p.config.Scripts) > 0 { 111 errs = packer.MultiErrorAppend(errs, 112 errors.New("Only one of script or scripts can be specified.")) 113 } 114 115 if p.config.Script != "" { 116 p.config.Scripts = []string{p.config.Script} 117 } 118 119 templates := map[string]*string{ 120 "inline_shebang": &p.config.InlineShebang, 121 "script": &p.config.Script, 122 "start_retry_timeout": &p.config.RawStartRetryTimeout, 123 "remote_path": &p.config.RemotePath, 124 } 125 126 for n, ptr := range templates { 127 var err error 128 *ptr, err = p.config.tpl.Process(*ptr, nil) 129 if err != nil { 130 errs = packer.MultiErrorAppend( 131 errs, fmt.Errorf("Error processing %s: %s", n, err)) 132 } 133 } 134 135 sliceTemplates := map[string][]string{ 136 "inline": p.config.Inline, 137 "scripts": p.config.Scripts, 138 "environment_vars": p.config.Vars, 139 } 140 141 for n, slice := range sliceTemplates { 142 for i, elem := range slice { 143 var err error 144 slice[i], err = p.config.tpl.Process(elem, nil) 145 if err != nil { 146 errs = packer.MultiErrorAppend( 147 errs, fmt.Errorf("Error processing %s[%d]: %s", n, i, err)) 148 } 149 } 150 } 151 152 if len(p.config.Scripts) == 0 && p.config.Inline == nil { 153 errs = packer.MultiErrorAppend(errs, 154 errors.New("Either a script file or inline script must be specified.")) 155 } else if len(p.config.Scripts) > 0 && p.config.Inline != nil { 156 errs = packer.MultiErrorAppend(errs, 157 errors.New("Only a script file or an inline script can be specified, not both.")) 158 } 159 160 for _, path := range p.config.Scripts { 161 if _, err := os.Stat(path); err != nil { 162 errs = packer.MultiErrorAppend(errs, 163 fmt.Errorf("Bad script '%s': %s", path, err)) 164 } 165 } 166 167 // Do a check for bad environment variables, such as '=foo', 'foobar' 168 for _, kv := range p.config.Vars { 169 vs := strings.Split(kv, "=") 170 if len(vs) != 2 || vs[0] == "" { 171 errs = packer.MultiErrorAppend(errs, 172 fmt.Errorf("Environment variable not in format 'key=value': %s", kv)) 173 } 174 } 175 176 if p.config.RawStartRetryTimeout != "" { 177 p.config.startRetryTimeout, err = time.ParseDuration(p.config.RawStartRetryTimeout) 178 if err != nil { 179 errs = packer.MultiErrorAppend( 180 errs, fmt.Errorf("Failed parsing start_retry_timeout: %s", err)) 181 } 182 } 183 184 if errs != nil && len(errs.Errors) > 0 { 185 return errs 186 } 187 188 return nil 189 } 190 191 func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { 192 scripts := make([]string, len(p.config.Scripts)) 193 copy(scripts, p.config.Scripts) 194 195 // If we have an inline script, then turn that into a temporary 196 // shell script and use that. 197 if p.config.Inline != nil { 198 tf, err := ioutil.TempFile("", "packer-shell") 199 if err != nil { 200 return fmt.Errorf("Error preparing shell script: %s", err) 201 } 202 defer os.Remove(tf.Name()) 203 204 // Set the path to the temporary file 205 scripts = append(scripts, tf.Name()) 206 207 // Write our contents to it 208 writer := bufio.NewWriter(tf) 209 writer.WriteString(fmt.Sprintf("#!%s\n", p.config.InlineShebang)) 210 for _, command := range p.config.Inline { 211 if _, err := writer.WriteString(command + "\n"); err != nil { 212 return fmt.Errorf("Error preparing shell script: %s", err) 213 } 214 } 215 216 if err := writer.Flush(); err != nil { 217 return fmt.Errorf("Error preparing shell script: %s", err) 218 } 219 220 tf.Close() 221 } 222 223 // Build our variables up by adding in the build name and builder type 224 envVars := make([]string, len(p.config.Vars)+2) 225 envVars[0] = "PACKER_BUILD_NAME=" + p.config.PackerBuildName 226 envVars[1] = "PACKER_BUILDER_TYPE=" + p.config.PackerBuilderType 227 copy(envVars[2:], p.config.Vars) 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 log.Printf("Uploading %s => %s", path, p.config.RemotePath) 240 err = comm.Upload(p.config.RemotePath, f) 241 if err != nil { 242 return fmt.Errorf("Error uploading shell script: %s", err) 243 } 244 245 // Close the original file since we copied it 246 f.Close() 247 248 // Flatten the environment variables 249 flattendVars := strings.Join(envVars, " ") 250 251 // Compile the command 252 command, err := p.config.tpl.Process(p.config.ExecuteCommand, &ExecuteCommandTemplate{ 253 Vars: flattendVars, 254 Path: p.config.RemotePath, 255 }) 256 if err != nil { 257 return fmt.Errorf("Error processing command: %s", err) 258 } 259 260 cmd := &packer.RemoteCmd{Command: command} 261 startTimeout := time.After(p.config.startRetryTimeout) 262 log.Printf("Executing command: %s", cmd.Command) 263 for { 264 if err := cmd.StartWithUi(comm, ui); err == nil { 265 break 266 } 267 268 // Create an error and log it 269 err = fmt.Errorf("Error executing command: %s", err) 270 log.Printf(err.Error()) 271 272 // Check if we timed out, otherwise we retry. It is safe to 273 // retry since the only error case above is if the command 274 // failed to START. 275 select { 276 case <-startTimeout: 277 return err 278 default: 279 time.Sleep(2 * time.Second) 280 } 281 } 282 283 if cmd.ExitStatus != 0 { 284 return fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus) 285 } 286 } 287 288 return nil 289 }